[PATCH 4 of 4] keyring: add new extension keyring

Markus Zapke-Gründemann markuszapke at gmx.net
Fri Sep 14 09:46:51 CDT 2012


# HG changeset patch
# User Markus Zapke-Gründemann <markus at keimlink.de>
# Date 1347621853 -7200
# Node ID b76790118dbbc94eeb6527a5fc6b5cc7f58ef9c9
# Parent  c24677b7153da58a0cfac8b062cc1a68536b9be9
keyring: add new extension keyring

keyring is an improved version of the mercurial_keyting extension. It
can be used to securely save HTTP authentication details.

diff --git a/hgext/keyring.py b/hgext/keyring.py
--- a/hgext/keyring.py
+++ b/hgext/keyring.py
@@ -1,452 +1,434 @@
-# -*- coding: utf-8 -*-
+# keyring.py - secure password databases for mercurial
 #
-# mercurial_keyring: save passwords in password database
+# Based on the mercurial_keyring extension.
 #
 # Copyright 2009 Marcin Kasperski <Marcin.Kasperski at mekk.waw.pl>
-#
-# This software may be used and distributed according to the terms
-# of the GNU General Public License, incorporated herein by reference.
-#
-# See README.txt for more details.
+# Copyright 2012 Markus Zapke-Gruendemann <markus at keimlink.de>
+"""securely save HTTP authentication details
 
-''' securely save HTTP and SMTP authentication details
-mercurial_keyring is a Mercurial extension used to securely save
-HTTP and SMTP authentication passwords in password databases (Gnome
-Keyring, KDE KWallet, OSXKeyChain, specific solutions for Win32 and
-command line). This extension uses and wraps services of the keyring
-library.
-'''
+keyring is a Mercurial extension used to securely save HTTP
+authentication passwords in password databases (GNOME Keyring, KDE
+KWallet, OS X Keychain, specific solutions for Win32 and command line).
+This extension uses and wraps services of the python keyring library.
 
-from mercurial import hg, repo, util
+How it works
+------------
+
+The extension prompts for the password on the first pull/push, just like
+it is done by default, but saves the password. On successive runs it
+checks for the username in the configuration, then for the suitable
+password in the password database, and uses those credentials (if
+found).
+
+In case password turns out to be incorrect (either because it was
+invalid, or because it was changed on the server) or missing it just
+prompts the user again.
+
+Passwords are identified by the combination of username and remote
+address, so they can be reused between repositories if they access the
+same remote repository.
+
+Installation
+------------
+
+Prerequisites
+.............
+
+Install the python keyring library using ``pip``::
+
+    pip install keyring
+
+or ``easy_install``::
+
+    easy_install keyring
+
+On Debian the library can be also installed from the official archive
+(packages ``python-keyring`` and either ``python-keyring-gnome`` or
+``python-keyring-kwallet``).
+
+Note: keyring >= 0.3 is strongly recommended, especially in case text
+backend is to be used.
+
+Extension installation
+......................
+
+Enable the extension in the configuration::
+
+    [extensions]
+    keyring =
+
+Password backend configuration
+------------------------------
+
+The library should usually pick the most appropriate password backend
+without configuration. Still, if necessary, it can be configured using
+the ``~/keyringrc.cfg`` file (``keyringrc.cfg`` in the home directory of
+the current user). Refer to keyring documentation for more details.
+
+Repository configuration
+------------------------
+
+Edit repository-local ``hgrc`` and save there the remote repository path
+and the username, but do not save the password. For example::
+
+    [paths]
+    default = https://hg.example.com/repo/someproject
+
+    [auth]
+    myremote.schemes = http https
+    myremote.prefix = hg.example.com/repo
+    myremote.username = alice
+
+Simpler form with url-embedded name can also be used::
+
+    [paths]
+    bitbucket = https://bob@bitbucket.org/bob/project_name/
+
+If prefix is specified, it is used to identify the password (so all
+repositories with the same prefix and the same username will share the
+same password). Otherwise the full repository URL is used for this
+purpose.
+
+Note: if both username and password are given in the configuration, the
+extension will use them without using the password database. If username
+is not given, extension will prompt for credentials every time, also
+without saving the password.
+
+Finally, if you are consistent about remote repository nicknames, you
+can configure the username in your global configuration. For example,
+write there::
+
+    [auth]
+    acme.schemes = http https
+    acme.prefix = hg.acme.com/repositories
+    acme.username = clara
+
+As long as you will be using alias `acme` for repositories like
+`https://hg.acme.com/repositories/my_beautiful_app`, username `clara`
+will be used, and the same password reused.
+
+The advantage of this method is that it works also for :hg:`clone`.
+
+Usage
+-----
+
+Configure the repository as above, then just :hg:`pull`, :hg:`push`,
+etc. You should be asked for the password only once (per every username
+and remote repository prefix or url combination).
+"""
+import urllib2
+
+from mercurial import httpconnection, url, util
 from mercurial.i18n import _
-try:
-    from mercurial.url import passwordmgr
-except:
-    from mercurial.httprepo import passwordmgr
-from mercurial.httprepo import httprepository
-from mercurial import mail
-from urllib2 import AbstractBasicAuthHandler, AbstractDigestAuthHandler
+# mercurial.demandimport incompatibility workaround, otherwise
+# gnomekeyring, one of the possible keyring backends, would not to work.
+from mercurial.demandimport import ignore
+if 'gobject._gobject' not in ignore:
+    ignore.append('gobject._gobject')
+pykeyring = __import__('keyring')
 
-# mercurial.demandimport incompatibility workaround,
-# would cause gnomekeyring, one of the possible
-# keyring backends, not to work.
-from mercurial.demandimport import ignore
-if "gobject._gobject" not in ignore:
-    ignore.append("gobject._gobject")
 
-import keyring
-from urlparse import urlparse
-import urllib2
-import smtplib, socket
-import os
+class passwordstore(object):
+    """Helper object handling python keyring usage.
 
-KEYRING_SERVICE = "Mercurial"
+    Controls how passwords are saved and read and the way they are keyed
+    in the keyring.
+    """
+    KEYRING_SERVICE = 'Mercurial'
 
-############################################################
+    @staticmethod
+    def _formatkey(uri, username):
+        """Creates a key for http passwords."""
+        return '%s@@%s' % (username, uri)
 
-def monkeypatch_method(cls,fname=None):
-    def decorator(func):
-        local_fname = fname
-        if local_fname is None:
-            local_fname = func.__name__
-        setattr(func, "orig", getattr(cls, local_fname, None))
-        setattr(cls, local_fname, func)
-        return func
-    return decorator
+    def getpassword(self, uri, username):
+        """Reads the password from the keyring."""
+        return pykeyring.get_password(self.KEYRING_SERVICE,
+            self._formatkey(uri, username))  # pragma: no cover
 
-############################################################
+    def setpassword(self, uri, username, password):
+        """Saves the password in the keyring."""
+        pykeyring.set_password(self.KEYRING_SERVICE,
+            self._formatkey(uri, username), password)  # pragma: no cover
 
-class PasswordStore(object):
+    def clearpassword(self, uri, username):
+        """Clears a user's password for an uri."""
+        self.setpassword(uri, username, '')  # pragma: no cover
+
+password_store = passwordstore()
+
+
+def canonicalurl(uri):
+    """Strips query and fragment from uri.
+
+    Example:
+
+    >>> url = 'https://hg.example.com/repos/module?cmd=capabilities'
+    >>> canonicalurl(url)
+    'https://hg.example.com/repos/module'
     """
-    Helper object handling keyring usage (password save&restore,
-    the way passwords are keyed in the keyring).
+    u = util.url(uri)
+    u.query = None
+    u.fragment = None
+    return str(u)
+
+
+def expandprefix(prefix, uri):
+    """Expands auth.prefix.
+
+    Examples:
+
+    >>> prefix = '*'
+    >>> url = 'https://hg.example.com/repo'
+    >>> expandprefix(prefix, url)
+    'https://hg.example.com/repo'
+    >>> prefix = None
+    >>> expandprefix(prefix, url)
+    'https://hg.example.com/repo'
+    >>> prefix= 'hg.example.com'
+    >>> expandprefix(prefix, url)
+    'https://hg.example.com'
+    >>> expandprefix(prefix, 'example.com')
+    'http://hg.example.com'
+    >>> prefix= 'https://hg.example.com'
+    >>> expandprefix(prefix, url)
+    'https://hg.example.com'
+    """
+    if not prefix or prefix == '*':
+        return uri
+    if util.hasscheme(prefix):
+        return prefix
+    try:
+        scheme, hostpath = uri.split('://')
+    except ValueError:
+        scheme = 'http'
+    return '%s://%s' % (scheme, prefix)
+
+
+class requesthistory(object):
+    """Stores information about different requests.
+
+    Use it to store and compare request information.
+
+    Examples:
+
+    >>> from urllib2 import Request
+    >>> uri = 'https://hg.example.com/'
+    >>> r = Request(uri)
+    >>> h = requesthistory()
+    >>> h == (uri, None, r)
+    False
+    >>> h.update(uri, None, r)
+    >>> h == (uri, None, r)
+    True
+    >>> h == (uri, 'auth', r)
+    False
+    >>> r.add_header('User-agent', 'mercurial/proto-1.0')
+    >>> h == (uri, None, r)
+    False
+    >>> h.update(uri)
+    >>> h == (uri, None, None)
+    True
     """
     def __init__(self):
-        self.cache = dict()
-    def get_http_password(self, url, username):
-        return keyring.get_password(KEYRING_SERVICE,
-                                    self._format_http_key(url, username))
-    def set_http_password(self, url, username, password):
-        keyring.set_password(KEYRING_SERVICE,
-                             self._format_http_key(url, username),
-                             password)
-    def clear_http_password(self, url, username):
-        self.set_http_password(url, username, "")
-    def _format_http_key(self, url, username):
-        return "%s@@%s" % (username, url)
-    def get_smtp_password(self, machine, port, username):
-        return keyring.get_password(
-            KEYRING_SERVICE,
-            self._format_smtp_key(machine, port, username))
-    def set_smtp_password(self, machine, port, username, password):
-        keyring.set_password(
-            KEYRING_SERVICE,
-            self._format_smtp_key(machine, port, username),
-            password)
-    def clear_smtp_password(self, machine, port, username):
-        self.set_smtp_password(url, username, "")
-    def _format_smtp_key(self, machine, port, username):
-        return "%s@@%s:%s" % (username, machine, str(port))
+        self.uri = self.realm = self.headers = None
 
-password_store = PasswordStore()
+    @staticmethod
+    def getheaders(request):
+        """Extracts headers from a Request object."""
+        try:
+            headers = request.header_items()
+        except AttributeError:
+            headers = None
+        return headers
 
-############################################################
+    def update(self, uri, realm=None, request=None):
+        """Updates requesthistory instance."""
+        self.uri = uri
+        self.realm = realm
+        self.headers = self.getheaders(request)
 
-def _debug(ui, msg):
-    ui.debug("[HgKeyring] " + msg + "\n")
+    def __eq__(self, data):
+        """Compares if requesthistory instance and a data tuple are equal.
 
-def _debug_reply(ui, msg, url, user, pwd):
-    _debug(ui, "%s. Url: %s, user: %s, passwd: %s" % (
-            msg, url, user, pwd and '*' * len(pwd) or 'not set'))
+        The tuple must have three elements: (uri, realm, request)
+        """
+        return (self.uri == data[0] and self.realm == data[1]
+            and self.headers == self.getheaders(data[2]))
 
 
-############################################################
+class httppasswordhandler(urllib2.HTTPPasswordMgrWithDefaultRealm):
+    """Actual implementation of password handling.
 
-class HTTPPasswordHandler(object):
+    An instance of this class can be used as passwordmgr in mercurial.url.
     """
-    Actual implementation of password handling (user prompting,
-    configuration file searching, keyring save&restore).
+    def __init__(self, ui):
+        urllib2.HTTPPasswordMgrWithDefaultRealm.__init__(self)
+        self.pwdcache = {}
+        self.req = None
+        self.reqhistory = requesthistory()
+        self.ui = ui
 
-    Object of this class is bound as passwordmgr attribute.
-    """
-    def __init__(self):
-        self.pwd_cache = {}
-        self.last_reply = None
+    def _writedebug(self, msg, uri=None, user=None, pwd=None):
+        """Writes debugging message to stdout.
 
-    def find_auth(self, pwmgr, realm, authuri, req):
+        If one or all of uri, user or pwd are set they are appended
+        to the debug message on a new line.
         """
-        Actual implementation of find_user_password - different
-        ways of obtaining the username and password.
+        extramsg = []
+        if uri:
+            extramsg.append('url: %s' % util.hidepassword(uri))
+        if user:
+            extramsg.append('user: %s' % user)
+        if pwd:
+            extramsg.append('password: %s' % '***')
+        self.ui.debug('%s\n' % msg)
+        if len(extramsg):
+            self.ui.debug('\t%s\n' % ', '.join(extramsg))
+
+    def _debugbadauth(self, uri, realm):
+        """Print debugging information after bad authentication."""
+        self._writedebug(
+            'detected bad authentication, cached passwords not used')
+        self._writedebug('current request:')
+        self._writedebug('\turi: %s' % uri)
+        if self.req:
+            self._writedebug('\trequest headers:')
+            for key, value in self.req.header_items():
+                if key == 'Authorization':
+                    value = '***'
+                self._writedebug('\t\t%s: %s' % (key, value))
+        self._writedebug('\trealm: %s' % realm)
+        self._writedebug('previous request:')
+        self._writedebug('\turi: %s' % self.reqhistory.uri)
+        if self.reqhistory.headers:
+            self._writedebug('\trequest headers:')
+            for key, value in self.reqhistory.headers:
+                if key == 'Authorization':
+                    value = '***'
+                self._writedebug('\t\t%s: %s' % (key, value))
+        self._writedebug('\trealm: %s' % self.reqhistory.realm)
+
+    def setrequest(self, request):
+        """Stores a Request instance for later analysis."""
+        self.req = request
+
+    def find_stored_password(self, authuri):
+        return self.find_user_password(None, authuri)  # pragma: no cover
+
+    def find_user_password(self, realm, authuri):
+        """keyring-based implementation of username/password query.
+
+        Passwords are saved in GNOME Keyring, KDE KWallet, OS X Keychain
+        or other platform specific storage and keyed by the repository
+        url.
         """
-        ui = pwmgr.ui
-
         # If we are called again just after identical previous
         # request, then the previously returned auth must have been
         # wrong. So we note this to force password prompt (and avoid
         # reusing bad password indifinitely).
-        after_bad_auth = (self.last_reply \
-                          and (self.last_reply['realm'] == realm) \
-                          and (self.last_reply['authuri'] == authuri) \
-                          and (self.last_reply['req'] == req))
-        if after_bad_auth:
-            _debug(ui, _("Working after bad authentication, cached passwords not used %s") % str(self.last_reply))
-
-        # Strip arguments to get actual remote repository url.
-        base_url = self.canonical_url(authuri)
-
-        # Extracting possible username (or password)
-        # stored directly in repository url
+        afterbadauth = self.reqhistory == (authuri, realm, self.req)
+        if afterbadauth:
+            self._debugbadauth(authuri, realm)
+        baseurl = canonicalurl(authuri)
+        # Extracting possible username (and password if available)
+        # stored directly in repository url.
         user, pwd = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
-            pwmgr, realm, authuri)
+            self, realm, authuri)
         if user and pwd:
-            _debug_reply(ui, _("Auth data found in repository URL"),
-                         base_url, user, pwd)
-            self.last_reply = dict(realm=realm,authuri=authuri,user=user,req=req)
+            self._writedebug('auth data found in repository url', baseurl,
+                user, pwd)
+            self.reqhistory.update(authuri, realm, self.req)
             return user, pwd
-
-        # Loading .hg/hgrc [auth] section contents. If prefix is given,
-        # it will be used as a key to lookup password in the keyring.
-        auth_user, pwd, prefix_url = self.load_hgrc_auth(ui, base_url, user)
-        if prefix_url:
-            keyring_url = prefix_url
-        else:
-            keyring_url = base_url
-        _debug(ui, _("Keyring URL: %s") % keyring_url)
-
+        if user:
+            self._writedebug('stripped username "%s" from url' % user)
+        cleanurl = util.removeauth(baseurl)
+        group, authuser, pwd, prefix = self.loadauthconf(cleanurl, user)
+        keyringurl = expandprefix(prefix, cleanurl)
+        self._writedebug('keyring url: %s' % keyringurl)
         # Checking the memory cache (there may be many http calls per command)
-        cache_key = (realm, keyring_url)
-        if not after_bad_auth:
-            cached_auth = self.pwd_cache.get(cache_key)
-            if cached_auth:
-                user, pwd = cached_auth
-                _debug_reply(ui, _("Cached auth data found"),
-                             base_url, user, pwd)
-                self.last_reply = dict(realm=realm,authuri=authuri,user=user,req=req)
+        cachekey = (realm, keyringurl)
+        if not afterbadauth:
+            cachedauth = self.pwdcache.get(cachekey)
+            if cachedauth:
+                user, pwd = cachedauth
+                self._writedebug('cached auth data found', baseurl, user, pwd)
+                self.reqhistory.update(authuri, realm, self.req)
                 return user, pwd
-
-        if auth_user:
-            if user and (user != auth_user):
-                raise util.Abort(_('mercurial_keyring: username for %s specified both in repository path (%s) and in .hg/hgrc/[auth] (%s). Please, leave only one of those' % (base_url, user, auth_user)))
-            user = auth_user
+        if authuser:
+            if user and (user == authuser):
+                msg = 'keyring: username %s for %s specified both in '
+                msg += 'url/paths and in auth - please, leave only one of those'
+                raise util.Abort(_(msg % (user, keyringurl)))
+            user = authuser
+            self._writedebug('using auth.%s.* for authentication' % group)
             if pwd:
-                self.pwd_cache[cache_key] = user, pwd
-                _debug_reply(ui, _("Auth data set in .hg/hgrc"),
-                             base_url, user, pwd)
-                self.last_reply = dict(realm=realm,authuri=authuri,user=user,req=req)
+                self.pwdcache[cachekey] = user, pwd
+                self.reqhistory.update(authuri, realm, self.req)
+                return user, pwd
+        # Loading password from keyring. Only if username is known (so
+        # we know the key) and we are not after failure (so we don't
+        # reuse the bad password).
+        if user and not afterbadauth:
+            self._writedebug('looking for password for user %s and url %s'
+                % (user, keyringurl))
+            pwd = password_store.getpassword(keyringurl, user)
+            if pwd:
+                self.pwdcache[cachekey] = user, pwd
+                self._writedebug('password found on keyring', baseurl, user,
+                    pwd)
+                self.reqhistory.update(authuri, realm, self.req)
                 return user, pwd
             else:
-                _debug(ui, _("Username found in .hg/hgrc: %s") % user)
-
-        # Loading password from keyring.
-        # Only if username is known (so we know the key) and we are
-        # not after failure (so we don't reuse the bad password).
-        if user and not after_bad_auth:
-            _debug(ui, _("Looking for password for user %s and url %s") % (user, keyring_url))
-            pwd = password_store.get_http_password(keyring_url, user)
-            if pwd:
-                self.pwd_cache[cache_key] = user, pwd
-                _debug_reply(ui, _("Keyring password found"),
-                             base_url, user, pwd)
-                self.last_reply = dict(realm=realm,authuri=authuri,user=user,req=req)
-                return user, pwd
-            else:
-                _debug(ui, _("Password not present in the keyring"))
-
+                self._writedebug('password not present in keyring')
         # Is the username permanently set?
-        fixed_user = (user and True or False)
-
+        fixeduser = (user and True or False)
         # Last resort: interactive prompt
-        if not ui.interactive():
-            raise util.Abort(_('mercurial_keyring: http authorization required but program used in non-interactive mode'))
-
-        if not fixed_user:
-            ui.status(_("Username not specified in .hg/hgrc. Keyring will not be used.\n"))
-
-        ui.write(_("http authorization required\n"))
-        ui.status(_("realm: %s\n") % realm)
-        if fixed_user:
-            ui.write(_("user: %s (fixed in .hg/hgrc)\n" % user))
+        if not self.ui.interactive():
+            msg = 'keyring: http authorization required but program '
+            msg += 'used in non-interactive mode'
+            raise util.Abort(_(msg))
+        if not fixeduser:
+            msg = 'username not set in configuration - keyring is not used\n'
+            self.ui.status(_(msg))
+        self.ui.write(_('http authorization required\n'))
+        self.ui.status(_('realm: %s\n') % realm)
+        if fixeduser:
+            self.ui.write(_('user: %s (fixed in configuration)\n') % user)
         else:
-            user = ui.prompt(_("user:"), default=None)
-        pwd = ui.getpass(_("password: "))
-
-        if fixed_user:
-            # Saving password to the keyring.
-            # It is done only if username is permanently set.
-            # Otherwise we won't be able to find the password so it
-            # does not make much sense to preserve it
-            _debug(ui, _("Saving password for %s to keyring") % user)
-            password_store.set_http_password(keyring_url, user, pwd)
-
+            user = self.ui.prompt(_('user:'), default=None)
+        pwd = self.ui.getpass(_('password: '))
+        if fixeduser:
+            # Saving password to the keyring. It is done only if the
+            # username is permanently set. Otherwise we won't be able to
+            # find the password so it does not make much sense to
+            # preserve it.
+            self._writedebug('saving password for %s to keyring' % user)
+            password_store.setpassword(keyringurl, user, pwd)
         # Saving password to the memory cache
-        self.pwd_cache[cache_key] = user, pwd
-
-        _debug_reply(ui, _("Manually entered password"),
-                     base_url, user, pwd)
-        self.last_reply = dict(realm=realm,authuri=authuri,user=user,req=req)
+        self.pwdcache[cachekey] = user, pwd
+        self._writedebug('manually entered password', baseurl, user, pwd)
+        self.reqhistory.update(authuri, realm, self.req)
         return user, pwd
 
-    def load_hgrc_auth(self, ui, base_url, user):
+    def loadauthconf(self, uri, user):
+        """Loading auth section contents from local configuration.
+
+        Returns (group, username, password, prefix) tuple (every element
+        can be None).
         """
-        Loading [auth] section contents from local .hgrc
+        authinfo = httpconnection.readauthforuri(self.ui, uri, user)
+        if authinfo:
+            group, auth = authinfo
+            username = auth.get('username')
+            pwd = auth.get('password')
+            prefix = auth.get('prefix')
+        else:
+            group = username = pwd = prefix = None
+        return group, username, pwd, prefix
 
-        Returns (username, password, prefix) tuple (every
-        element can be None)
-        """
-        # Theoretically 3 lines below should do:
 
-        #auth_token = self.readauthtoken(base_url)
-        #if auth_token:
-        #   user, pwd = auth.get('username'), auth.get('password')
+def uisetup(ui):
+    url.pwmgrclass = httppasswordhandler
 
-        # Unfortunately they do not work, readauthtoken always return
-        # None. Why? Because ui (self.ui of passwordmgr) describes the
-        # *remote* repository, so does *not* contain any option from
-        # local .hg/hgrc.
-
-        # TODO: mercurial 1.4.2 is claimed to resolve this problem
-        # (thanks to: http://hg.xavamedia.nl/mercurial/crew/rev/fb45c1e4396f)
-        # so since this version workaround implemented below should
-        # not be necessary. As it will take some time until people
-        # migrate to >= 1.4.2, it would be best to implement
-        # workaround conditionally.
-
-        # Workaround: we recreate the repository object
-        repo_root = ui.config("bundle", "mainreporoot")
-
-        from mercurial.ui import ui as _ui
-        local_ui = _ui(ui)
-        if repo_root:
-            local_ui.readconfig(os.path.join(repo_root, ".hg", "hgrc"))
-        try:
-            local_passwordmgr = passwordmgr(local_ui)
-            auth_token = local_passwordmgr.readauthtoken(base_url)
-        except AttributeError:
-            try:
-                # hg 1.8
-                import mercurial.url
-                readauthforuri = mercurial.url.readauthforuri
-            except (ImportError, AttributeError):
-                # hg 1.9
-                import mercurial.httpconnection
-                readauthforuri = mercurial.httpconnection.readauthforuri
-            if readauthforuri.func_code.co_argcount == 3:
-                # Since hg.0593e8f81c71
-                res = readauthforuri(local_ui, base_url, user)
-            else:
-                res = readauthforuri(local_ui, base_url)
-            if res:
-                group, auth_token = res
-            else:
-                auth_token = None
-        if auth_token:
-            username = auth_token.get('username')
-            password = auth_token.get('password')
-            prefix = auth_token.get('prefix')
-            shortest_url = self.shortest_url(base_url, prefix)
-            return username, password, shortest_url
-
-        return None, None, None
-
-    def shortest_url(self, base_url, prefix):
-        if not prefix or prefix == '*':
-            return base_url
-        scheme, hostpath = base_url.split('://', 1)
-        p = prefix.split('://', 1)
-        if len(p) > 1:
-            prefix_host_path = p[1]
-        else:
-            prefix_host_path = prefix
-        shortest_url = scheme + '://' + prefix_host_path
-        return shortest_url
-
-    def canonical_url(self, authuri):
-        """
-        Strips query params from url. Used to convert urls like
-        https://repo.machine.com/repos/apps/module?pairs=0000000000000000000000000000000000000000-0000000000000000000000000000000000000000&cmd=between
-        to
-        https://repo.machine.com/repos/apps/module
-        """
-        parsed_url = urlparse(authuri)
-        return "%s://%s%s" % (parsed_url.scheme, parsed_url.netloc,
-                              parsed_url.path)
-
-############################################################
-
- at monkeypatch_method(passwordmgr)
-def find_user_password(self, realm, authuri):
-    """
-    keyring-based implementation of username/password query
-    for HTTP(S) connections
-
-    Passwords are saved in gnome keyring, OSX/Chain or other platform
-    specific storage and keyed by the repository url
-    """
-    # Extend object attributes
-    if not hasattr(self, '_pwd_handler'):
-        self._pwd_handler = HTTPPasswordHandler()
-
-    if hasattr(self, '_http_req'):
-        req = self._http_req
-    else:
-        req = None
-
-    return self._pwd_handler.find_auth(self, realm, authuri, req)
-
- at monkeypatch_method(AbstractBasicAuthHandler, "http_error_auth_reqed")
-def basic_http_error_auth_reqed(self, authreq, host, req, headers):
-    self.passwd._http_req = req
-    try:
-        return basic_http_error_auth_reqed.orig(self, authreq, host, req, headers)
-    finally:
-        self.passwd._http_req = None
-
- at monkeypatch_method(AbstractDigestAuthHandler, "http_error_auth_reqed")
-def digest_http_error_auth_reqed(self, authreq, host, req, headers):
-    self.passwd._http_req = req
-    try:
-        return digest_http_error_auth_reqed.orig(self, authreq, host, req, headers)
-    finally:
-        self.passwd._http_req = None
-
-############################################################
-
-def try_smtp_login(ui, smtp_obj, username, password):
-    """
-    Attempts smtp login on smtp_obj (smtplib.SMTP) using username and
-    password.
-
-    Returns:
-    - True if login succeeded
-    - False if login failed due to the wrong credentials
-
-    Throws Abort exception if login failed for any other reason.
-
-    Immediately returns False if password is empty
-    """
-    if not password:
-        return False
-    try:
-        ui.note(_('(authenticating to mail server as %s)\n') %
-                 (username))
-        smtp_obj.login(username, password)
-        return True
-    except smtplib.SMTPException, inst:
-        if inst.smtp_code == 535:
-            ui.status(_("SMTP login failed: %s\n\n") % inst.smtp_error)
-            return False
-        else:
-            raise util.Abort(inst)
-
-def keyring_supported_smtp(ui, username):
-    """
-    keyring-integrated replacement for mercurial.mail._smtp
-    Used only when configuration file contains username, but
-    does not contain the password.
-
-    Most of the routine below is copied as-is from
-    mercurial.mail._smtp. The only changed part is
-    marked with #>>>>> and #<<<<< markers
-    """
-    local_hostname = ui.config('smtp', 'local_hostname')
-    s = smtplib.SMTP(local_hostname=local_hostname)
-    mailhost = ui.config('smtp', 'host')
-    if not mailhost:
-        raise util.Abort(_('no [smtp]host in hgrc - cannot send mail'))
-    mailport = int(ui.config('smtp', 'port', 25))
-    ui.note(_('sending mail: smtp host %s, port %s\n') %
-            (mailhost, mailport))
-    s.connect(host=mailhost, port=mailport)
-    if ui.configbool('smtp', 'tls'):
-        if not hasattr(socket, 'ssl'):
-            raise util.Abort(_("can't use TLS: Python SSL support "
-                               "not installed"))
-        ui.note(_('(using tls)\n'))
-        s.ehlo()
-        s.starttls()
-        s.ehlo()
-
-    #>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
-    stored = password = password_store.get_smtp_password(
-        mailhost, mailport, username)
-    # No need to check whether password was found as try_smtp_login
-    # just returns False if it is absent.
-    while not try_smtp_login(ui, s, username, password):
-        password = ui.getpass(_("Password for %s on %s:%d: ") % (username, mailhost, mailport))
-
-    if stored != password:
-        password_store.set_smtp_password(
-            mailhost, mailport, username, password)
-    #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
-
-    def send(sender, recipients, msg):
-        try:
-            return s.sendmail(sender, recipients, msg)
-        except smtplib.SMTPRecipientsRefused, inst:
-            recipients = [r[1] for r in inst.recipients.values()]
-            raise util.Abort('\n' + '\n'.join(recipients))
-        except smtplib.SMTPException, inst:
-            raise util.Abort(inst)
-
-    return send
-
-############################################################
-
-orig_smtp = mail._smtp
-
- at monkeypatch_method(mail)
-def _smtp(ui):
-    """
-    build an smtp connection and return a function to send email
-
-    This is the monkeypatched version of _smtp(ui) function from
-    mercurial/mail.py. It calls the original unless username
-    without password is given in the configuration.
-    """
-    username = ui.config('smtp', 'username')
-    password = ui.config('smtp', 'password')
-
-    if username and not password:
-        return keyring_supported_smtp(ui, username)
-    else:
-        return orig_smtp(ui)
+testedwith = '2.3'
diff --git a/tests/hghave.py b/tests/hghave.py
--- a/tests/hghave.py
+++ b/tests/hghave.py
@@ -107,6 +107,13 @@ def has_inotify():
     os.unlink(name)
     return True
 
+def has_keyring():
+    try:
+        import keyring
+        return True
+    except ImportError:
+        return False
+
 def has_fifo():
     if getattr(os, "mkfifo", None) is None:
         return False
@@ -289,6 +296,7 @@ checks = {
     "hardlink": (has_hardlink, "hardlinks"),
     "icasefs": (has_icasefs, "case insensitive file system"),
     "inotify": (has_inotify, "inotify extension support"),
+    "keyring": (has_keyring, "python keyring library"),
     "killdaemons": (has_killdaemons, 'killdaemons.py support'),
     "lsprof": (has_lsprof, "python lsprof module"),
     "mtn": (has_mtn, "monotone client (>= 1.0)"),
diff --git a/tests/test-keyring.py b/tests/test-keyring.py
new file mode 100644
--- /dev/null
+++ b/tests/test-keyring.py
@@ -0,0 +1,234 @@
+"""Unit tests and doctests for the keyring extension."""
+import os
+from urllib2 import Request
+import sys
+
+from mercurial import ui, util
+
+try:
+    import keyring
+except ImportError:
+    # missing feature: python keyring library
+    sys.exit(80)
+
+from hgext import keyring
+
+
+class passwordstorefake(keyring.passwordstore):
+    """A passwordstore fake using an internal cache."""
+    def __init__(self):
+        self.cache = {}
+
+    def getpassword(self, uri, username):
+        testclient.debug('reading password from keyring for %s\n' % uri)
+        cache_key = self._formatkey(uri, username)
+        try:
+            password = self.cache[cache_key]
+        except KeyError:
+            password = ''
+        return password
+
+    def setpassword(self, uri, username, password):
+        cache_key = self._formatkey(uri, username)
+        self.cache[cache_key] = password
+        testclient.debug('stored username %s and password %s\n'
+            % (username, password))
+
+
+keyring.password_store = passwordstorefake()
+
+
+class uifake(ui.ui):
+    """A ui fake class for authentication tests."""
+    def __init__(self, user, password, interactive):
+        """Creates a new ui instance.
+
+        Set user and password to the desired values.
+        interactive is a boolean to control the interactive shell.
+        """
+        self._user = user
+        self._password = password
+        self._interactive = interactive
+        super(uifake, self).__init__()
+
+    def interactive(self):
+        """Returns True if the ui instance is interactive."""
+        return self._interactive
+
+    def prompt(self, msg, default='y'):
+        """"Prompt the user for a username.
+
+        Because this is a SUT the method returns always the username set
+        at instantiation.
+        """
+        return self._user
+
+    def getpass(self, prompt=None, default=None):
+        """"Prompt the user for a password.
+
+        Because this is a SUT the method returns always the password set
+        at instantiation.
+        """
+        return self._password
+
+
+class testclient(object):
+    """Test client for keyring extension."""
+    def __init__(self, user, password=None, interactive=True):
+        """Creates a test client.
+
+        You must set a username. If no password is set a default
+        password is used. The interactive shell is enabled by default.
+        """
+        self.user = user
+        if password is None:
+            self.password = 'secret'
+        else:
+            self.password = password
+        self.ui = uifake(self.user, self.password, interactive=interactive)
+        self.pwhandler = keyring.httppasswordhandler(self.ui)
+
+    @staticmethod
+    def debug(msg):
+        sys.stdout.write('[test] %s' % msg)
+
+    def testuri(self, uri, msg=None, headers=None, realm=None, debug=True):
+        """Tests an uri with username and password.
+
+        Use msg to print a message when the test starts.
+        Add additional headers using the headers kwarg.
+        Set the realm using the realm kwarg.
+        Set debug to False to disable keyring debug output.
+        """
+        if headers is None:
+            headers = {}
+        if msg is not None:
+            self.debug(msg + '\n')
+        if debug:
+            debug_orig = self.ui.config('ui', 'debug')
+            self.ui.setconfig('ui', 'debug', 'True')
+        self.debug('testing user %s at %s\n' % (self.user, uri))
+        request = Request(uri, headers=headers)
+        if headers:
+            self.debug('request headers:\n')
+            for key, value in headers.items():
+                self.debug('\t%s: %s\n' % (key, value))
+        self.pwhandler.setrequest(request)
+        try:
+            uriobj = util.url(uri)
+            if uriobj.user:
+                self.pwhandler.add_password(realm, uri, uriobj.user,
+                    uriobj.passwd)
+            user, pwd = self.pwhandler.find_user_password(realm, uri)
+            assert user == self.user, 'user "%s" did not match "%s"' \
+                % (user, self.user)
+            assert pwd == self.password, 'password "%s" did not match "%s"' \
+                % (pwd, self.password)
+        except util.Abort, err:
+            self.debug('abort: %s\n' % err)
+        except AssertionError, err:
+            self.debug('warning: %s\n' % err)
+        sys.stdout.write('\n')
+        if debug:
+            self.ui.setconfig('ui', 'debug', debug_orig)
+
+
+t = testclient('alice')
+t.testuri('https://hg.example.com/repo', msg='ask for password',
+    headers={'Authorization': 'Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==',
+        'User-agent': 'mercurial/proto-1.0'})
+t.testuri('https://hg.example.com/repo',
+    msg='ignore password cache because of identical request',
+    headers={'Authorization': 'Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==',
+        'User-agent': 'mercurial/proto-1.0'})
+t.testuri('https://alice@hg.example.com/repo',
+    msg='read username from URI, use cached password')
+t.testuri('https://alice:secret@hg.example.com/repo',
+    msg='read username and password from URI, cache is not used')
+t.testuri('https://hg.example.com/repo', msg='use cached password')
+
+# add auth section to .hgrc
+hgrc = open(os.environ["HGRCPATH"], 'w')
+hgrc.write('[auth]\n')
+hgrc.write('myremote.prefix = hg.example.com\n')
+hgrc.write('myremote.username = bob\n')
+hgrc.close()
+
+t = testclient('bob')
+t.testuri('https://hg.example.com/repo', msg='read password from keyring')
+t.testuri('https://hg.example.com/repo/module?cmd=capabilities',
+        msg='use cached password and strip off query params')
+
+pwd = 'cookies'
+# add password to .hgrc
+hgrc = open(os.environ["HGRCPATH"], 'a')
+hgrc.write('myremote.password = %s\n' % pwd)
+hgrc.close()
+
+t = testclient('bob', pwd)
+t.testuri('https://bob@hg.example.com/repo',
+    msg='aborts because username is set in url and .hgrc')
+t.testuri('https://hg.example.com/repo',
+    msg='password is read from .hgrc')
+t.testuri('https://hg.example.com/repo?cmd=capabilities',
+    msg='use cached auth data')
+t.testuri('https://clara@hg.example.com/repo',
+    msg='store password for clara on keyring as user bob')
+
+# delete password from .hgrc
+hgrc = open(os.environ["HGRCPATH"], 'a')
+hgrc.write('myremote.password =\n')
+hgrc.close()
+
+t = testclient('bob')
+t.testuri('https://hg.example.com/repo',
+    msg='username is taken from .hgrc and password is read from keyring')
+
+t = testclient('clara', pwd)
+t.testuri('https://clara@hg.example.com/repo',
+    msg='read password for clara on keyring')
+
+# add catch all prefix to .hgrc
+hgrc = open(os.environ["HGRCPATH"], 'a')
+hgrc.write('myremote.prefix = *\n')
+hgrc.close()
+
+t = testclient('bob')
+t.testuri('https://hg.example.com/repo',
+    msg='read password from keyring, use full URI because of catch all prefix')
+
+# add not matching prefix to .hgrc
+hgrc = open(os.environ["HGRCPATH"], 'a')
+hgrc.write('myremote.prefix = code.example.com\n')
+hgrc.close()
+
+t = testclient('bob')
+t.testuri('https://hg.example.com/repo',
+    msg='will prompt for password because prefix does not match')
+
+t = testclient('bob', interactive=False)
+t.testuri('https://hg.example.com/repo',
+    msg='will abort because terminal is non-interactive', debug=False)
+
+# test (simulated) clone operation
+t = testclient('bob')
+t.testuri('https://hg.example.com/repo/?cmd=capabilities',
+    msg='test with different http headers')
+t.testuri('https://hg.example.com/repo/?cmd=batch',
+    headers={'x-hgarg-1': 'cmds=heads+%3Bknown+nodes%3D'})
+revs = ('0000000000000000000000000000000000000000',
+    'b066b1ec09fb7b604b548375170e78bc008bb6b2')
+t.testuri('https://hg.example.com/repo/?cmd=getbundle',
+    headers={'x-hgarg-1': 'common=%s&heads=%s' % revs})
+t.testuri('https://hg.example.com/repo/?cmd=listkeys',
+    headers={'x-hgarg-1': 'namespace=phases'})
+t.testuri('https://hg.example.com/repo/?cmd=listkeys',
+    headers={'x-hgarg-1': 'namespace=bookmarks'})
+
+# this is hack to make sure no escape characters are inserted into the
+# output
+if 'TERM' in os.environ:
+    del os.environ['TERM']
+
+import doctest
+doctest.testmod(keyring)
diff --git a/tests/test-keyring.py.out b/tests/test-keyring.py.out
new file mode 100644
--- /dev/null
+++ b/tests/test-keyring.py.out
@@ -0,0 +1,192 @@
+[test] ask for password
+[test] testing user alice at https://hg.example.com/repo
+[test] request headers:
+[test] 	Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
+[test] 	User-agent: mercurial/proto-1.0
+keyring url: https://hg.example.com/repo
+username not set in configuration - keyring is not used
+http authorization required
+realm: None
+manually entered password
+	url: https://hg.example.com/repo, user: alice, password: ***
+
+[test] ignore password cache because of identical request
+[test] testing user alice at https://hg.example.com/repo
+[test] request headers:
+[test] 	Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
+[test] 	User-agent: mercurial/proto-1.0
+detected bad authentication, cached passwords not used
+current request:
+	uri: https://hg.example.com/repo
+	request headers:
+		Authorization: ***
+		User-agent: mercurial/proto-1.0
+	realm: None
+previous request:
+	uri: https://hg.example.com/repo
+	request headers:
+		Authorization: ***
+		User-agent: mercurial/proto-1.0
+	realm: None
+keyring url: https://hg.example.com/repo
+username not set in configuration - keyring is not used
+http authorization required
+realm: None
+manually entered password
+	url: https://hg.example.com/repo, user: alice, password: ***
+
+[test] read username from URI, use cached password
+[test] testing user alice at https://alice@hg.example.com/repo
+stripped username "alice" from url
+keyring url: https://hg.example.com/repo
+cached auth data found
+	url: https://alice@hg.example.com/repo, user: alice, password: ***
+
+[test] read username and password from URI, cache is not used
+[test] testing user alice at https://alice:secret@hg.example.com/repo
+auth data found in repository url
+	url: https://alice:***@hg.example.com/repo, user: alice, password: ***
+
+[test] use cached password
+[test] testing user alice at https://hg.example.com/repo
+keyring url: https://hg.example.com/repo
+cached auth data found
+	url: https://hg.example.com/repo, user: alice, password: ***
+
+[test] read password from keyring
+[test] testing user bob at https://hg.example.com/repo
+keyring url: https://hg.example.com
+using auth.myremote.* for authentication
+looking for password for user bob and url https://hg.example.com
+[test] reading password from keyring for https://hg.example.com
+password not present in keyring
+http authorization required
+realm: None
+user: bob (fixed in configuration)
+saving password for bob to keyring
+[test] stored username bob and password secret
+manually entered password
+	url: https://hg.example.com/repo, user: bob, password: ***
+
+[test] use cached password and strip off query params
+[test] testing user bob at https://hg.example.com/repo/module?cmd=capabilities
+keyring url: https://hg.example.com
+cached auth data found
+	url: https://hg.example.com/repo/module, user: bob, password: ***
+
+[test] aborts because username is set in url and .hgrc
+[test] testing user bob at https://bob@hg.example.com/repo
+stripped username "bob" from url
+keyring url: https://hg.example.com
+[test] abort: keyring: username bob for https://hg.example.com specified both in url/paths and in auth - please, leave only one of those
+
+[test] password is read from .hgrc
+[test] testing user bob at https://hg.example.com/repo
+keyring url: https://hg.example.com
+using auth.myremote.* for authentication
+
+[test] use cached auth data
+[test] testing user bob at https://hg.example.com/repo?cmd=capabilities
+keyring url: https://hg.example.com
+cached auth data found
+	url: https://hg.example.com/repo, user: bob, password: ***
+
+[test] store password for clara on keyring as user bob
+[test] testing user bob at https://clara@hg.example.com/repo
+stripped username "clara" from url
+keyring url: https://hg.example.com/repo
+looking for password for user clara and url https://hg.example.com/repo
+[test] reading password from keyring for https://hg.example.com/repo
+password not present in keyring
+http authorization required
+realm: None
+user: clara (fixed in configuration)
+saving password for clara to keyring
+[test] stored username clara and password cookies
+manually entered password
+	url: https://clara@hg.example.com/repo, user: clara, password: ***
+[test] warning: user "clara" did not match "bob"
+
+[test] username is taken from .hgrc and password is read from keyring
+[test] testing user bob at https://hg.example.com/repo
+keyring url: https://hg.example.com
+using auth.myremote.* for authentication
+looking for password for user bob and url https://hg.example.com
+[test] reading password from keyring for https://hg.example.com
+password found on keyring
+	url: https://hg.example.com/repo, user: bob, password: ***
+
+[test] read password for clara on keyring
+[test] testing user clara at https://clara@hg.example.com/repo
+stripped username "clara" from url
+keyring url: https://hg.example.com/repo
+looking for password for user clara and url https://hg.example.com/repo
+[test] reading password from keyring for https://hg.example.com/repo
+password found on keyring
+	url: https://clara@hg.example.com/repo, user: clara, password: ***
+
+[test] read password from keyring, use full URI because of catch all prefix
+[test] testing user bob at https://hg.example.com/repo
+keyring url: https://hg.example.com/repo
+using auth.myremote.* for authentication
+looking for password for user bob and url https://hg.example.com/repo
+[test] reading password from keyring for https://hg.example.com/repo
+password not present in keyring
+http authorization required
+realm: None
+user: bob (fixed in configuration)
+saving password for bob to keyring
+[test] stored username bob and password secret
+manually entered password
+	url: https://hg.example.com/repo, user: bob, password: ***
+
+[test] will prompt for password because prefix does not match
+[test] testing user bob at https://hg.example.com/repo
+keyring url: https://hg.example.com/repo
+username not set in configuration - keyring is not used
+http authorization required
+realm: None
+manually entered password
+	url: https://hg.example.com/repo, user: bob, password: ***
+
+[test] will abort because terminal is non-interactive
+[test] testing user bob at https://hg.example.com/repo
+[test] abort: keyring: http authorization required but program used in non-interactive mode
+
+[test] test with different http headers
+[test] testing user bob at https://hg.example.com/repo/?cmd=capabilities
+keyring url: https://hg.example.com/repo/
+username not set in configuration - keyring is not used
+http authorization required
+realm: None
+manually entered password
+	url: https://hg.example.com/repo/, user: bob, password: ***
+
+[test] testing user bob at https://hg.example.com/repo/?cmd=batch
+[test] request headers:
+[test] 	x-hgarg-1: cmds=heads+%3Bknown+nodes%3D
+keyring url: https://hg.example.com/repo/
+cached auth data found
+	url: https://hg.example.com/repo/, user: bob, password: ***
+
+[test] testing user bob at https://hg.example.com/repo/?cmd=getbundle
+[test] request headers:
+[test] 	x-hgarg-1: common=0000000000000000000000000000000000000000&heads=b066b1ec09fb7b604b548375170e78bc008bb6b2
+keyring url: https://hg.example.com/repo/
+cached auth data found
+	url: https://hg.example.com/repo/, user: bob, password: ***
+
+[test] testing user bob at https://hg.example.com/repo/?cmd=listkeys
+[test] request headers:
+[test] 	x-hgarg-1: namespace=phases
+keyring url: https://hg.example.com/repo/
+cached auth data found
+	url: https://hg.example.com/repo/, user: bob, password: ***
+
+[test] testing user bob at https://hg.example.com/repo/?cmd=listkeys
+[test] request headers:
+[test] 	x-hgarg-1: namespace=bookmarks
+keyring url: https://hg.example.com/repo/
+cached auth data found
+	url: https://hg.example.com/repo/, user: bob, password: ***
+
diff --git a/tests/test-keyring.t b/tests/test-keyring.t
new file mode 100644
--- /dev/null
+++ b/tests/test-keyring.t
@@ -0,0 +1,79 @@
+  $ "$TESTDIR/hghave" serve || exit 80
+  $ "$TESTDIR/hghave" keyring || exit 80
+
+  $ hg init test
+  $ cd test
+  $ echo foo>foo
+  $ mkdir foo.d foo.d/bAr.hg.d foo.d/baR.d.hg
+  $ echo foo>foo.d/foo
+  $ echo bar>foo.d/bAr.hg.d/BaR
+  $ echo bar>foo.d/baR.d.hg/bAR
+  $ hg commit -A -m 1
+  adding foo
+  adding foo.d/bAr.hg.d/BaR
+  adding foo.d/baR.d.hg/bAR
+  adding foo.d/foo
+  $ hg phase -fpr 0:tip
+
+test http authentication with keyring extension
+
+  $ cat << EOT > userpass.py
+  > import base64
+  > from mercurial.hgweb import common
+  > def perform_authentication(hgweb, req, op):
+  >     auth = req.env.get('HTTP_AUTHORIZATION')
+  >     if not auth:
+  >         raise common.ErrorResponse(common.HTTP_UNAUTHORIZED, 'who',
+  >             [('WWW-Authenticate', 'Basic Realm="mercurial"')])
+  >     authinfo = base64.b64decode(auth.split()[1]).split(':', 1)
+  >     if authinfo != ['user', 'pass']:
+  >         raise common.ErrorResponse(common.HTTP_FORBIDDEN, 'no')
+  > def extsetup():
+  >     common.permhooks.insert(0, perform_authentication)
+  > EOT
+  $ hg --config extensions.x=userpass.py serve -p $HGPORT -d --pid-file=pid \
+  >    --config server.preferuncompressed=True
+  $ cat pid >> $DAEMON_PIDS
+
+  $ echo '[extensions]' >> .hg/hgrc
+  $ echo 'keyring=' >> .hg/hgrc
+  $ hg id http://localhost:$HGPORT/
+  abort: keyring: http authorization required but program used in non-interactive mode
+  [255]
+  $ hg id http://user@localhost:$HGPORT/
+  abort: keyring: http authorization required but program used in non-interactive mode
+  [255]
+  $ hg id http://user:pass@localhost:$HGPORT/
+  8b6053c928fe
+  $ echo '[auth]' >> .hg/hgrc
+  $ echo 'l.schemes=http' >> .hg/hgrc
+  $ echo 'l.prefix=*' >> .hg/hgrc
+  $ echo 'l.username=user' >> .hg/hgrc
+  $ echo 'l.password=pass' >> .hg/hgrc
+  $ hg id http://localhost:$HGPORT/
+  8b6053c928fe
+  $ hg id http://user@localhost:$HGPORT/
+  abort: keyring: username user for http://localhost:$HGPORT/ specified both in url/paths and in auth - please, leave only one of those
+  [255]
+  $ hg id http://user:pass@localhost:$HGPORT/
+  8b6053c928fe
+  $ hg id http://user2@localhost:$HGPORT/
+  abort: keyring: http authorization required but program used in non-interactive mode
+  [255]
+  $ hg clone http://user:pass@localhost:$HGPORT/ dest
+  streaming all changes
+  6 files to transfer, 606 bytes of data
+  transferred 606 bytes in * seconds (* KB/sec) (glob)
+  updating to branch default
+  4 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd dest
+  $ cp ../.hg/hgrc .hg
+  $ hg pull http://localhost:$HGPORT/
+  pulling from http://localhost:$HGPORT/
+  searching for changes
+  no changes found
+  $ hg push http://localhost:$HGPORT/
+  pushing to http://localhost:$HGPORT/
+  searching for changes
+  no changes found
+  [1]


More information about the Mercurial-devel mailing list