Rev 2328: (Dmitry Vasiliev) Support for Putty SSH, and improved vendor support in http://bzr.arbash-meinel.com/branches/bzr/jam-integration

John Arbash Meinel john at arbash-meinel.com
Fri Mar 9 00:45:32 GMT 2007


At http://bzr.arbash-meinel.com/branches/bzr/jam-integration

------------------------------------------------------------
revno: 2328
revision-id: john at arbash-meinel.com-20070309004524-fz8byjfqklnzvi6h
parent: pqm at pqm.ubuntu.com-20070309003830-14e9b007ad653905
parent: dima at hlabs.spb.ru-20070307134747-clcmwlfck4g9yqh3
committer: John Arbash Meinel <john at arbash-meinel.com>
branch nick: jam-integration
timestamp: Thu 2007-03-08 18:45:24 -0600
message:
  (Dmitry Vasiliev) Support for Putty SSH, and improved vendor support
added:
  bzrlib/tests/test_ssh_transport.py test_ssh_transport.p-20070105153201-f7iq2bosvgjbdgc3-1
modified:
  NEWS                           NEWS-20050323055033-4e00b5db738777ff
  bzrlib/errors.py               errors.py-20050309040759-20512168c4e14fbd
  bzrlib/tests/__init__.py       selftest.py-20050531073622-8d0e3c8845c97a64
  bzrlib/tests/blackbox/test_pull.py test_pull.py-20051201144907-64959364f629947f
  bzrlib/tests/test_sftp_transport.py testsftp.py-20051027032739-247570325fec7e7e
  bzrlib/transport/sftp.py       sftp.py-20051019050329-ab48ce71b7e32dfe
  bzrlib/transport/ssh.py        ssh.py-20060824042150-0s9787kng6zv1nwq-1
    ------------------------------------------------------------
    revno: 2221.5.22
    merged: dima at hlabs.spb.ru-20070307134747-clcmwlfck4g9yqh3
    parent: dima at hlabs.spb.ru-20070307132025-kgv55i7xvp7rp7pv
    committer: Dmitry Vasiliev <dima at hlabs.spb.ru>
    branch nick: Putty SSH implementation support
    timestamp: Wed 2007-03-07 16:47:47 +0300
    message:
      Updated note about registry.Registry
    ------------------------------------------------------------
    revno: 2221.5.21
    merged: dima at hlabs.spb.ru-20070307132025-kgv55i7xvp7rp7pv
    parent: dima at hlabs.spb.ru-20070307124531-yom9s6p3rb1npeet
    committer: Dmitry Vasiliev <dima at hlabs.spb.ru>
    branch nick: Putty SSH implementation support
    timestamp: Wed 2007-03-07 16:20:25 +0300
    message:
      Reverted trailing whitespace removal
    ------------------------------------------------------------
    revno: 2221.5.20
    merged: dima at hlabs.spb.ru-20070307124531-yom9s6p3rb1npeet
    parent: dima at hlabs.spb.ru-20070307121040-ekqnz95gtv2iq4jb
    parent: pqm at pqm.ubuntu.com-20070307121852-b60a661123a5063d
    committer: Dmitry Vasiliev <dima at hlabs.spb.ru>
    branch nick: Putty SSH implementation support
    timestamp: Wed 2007-03-07 15:45:31 +0300
    message:
      Merged bzr.dev
    ------------------------------------------------------------
    revno: 2221.5.19
    merged: dima at hlabs.spb.ru-20070307121040-ekqnz95gtv2iq4jb
    parent: dima at hlabs.spb.ru-20070302153843-9hks8px50lwukdcm
    parent: pqm at pqm.ubuntu.com-20070307110538-3026a526f5178b00
    committer: Dmitry Vasiliev <dima at hlabs.spb.ru>
    branch nick: Putty SSH implementation support
    timestamp: Wed 2007-03-07 15:10:40 +0300
    message:
      Merged bzr.dev
    ------------------------------------------------------------
    revno: 2221.5.18
    merged: dima at hlabs.spb.ru-20070302153843-9hks8px50lwukdcm
    parent: dima at hlabs.spb.ru-20070302141015-sjn7kthbdgylt3bp
    committer: Dmitry Vasiliev <dima at hlabs.spb.ru>
    branch nick: Putty SSH implementation support
    timestamp: Fri 2007-03-02 18:38:43 +0300
    message:
      Fixed variable name
    ------------------------------------------------------------
    revno: 2221.5.17
    merged: dima at hlabs.spb.ru-20070302141015-sjn7kthbdgylt3bp
    parent: dima at hlabs.spb.ru-20070302134414-9mqpmmsii6llb9of
    committer: Dmitry Vasiliev <dima at hlabs.spb.ru>
    branch nick: Putty SSH implementation support
    timestamp: Fri 2007-03-02 17:10:15 +0300
    message:
      Added comments for test_get_vendor_search_order
    ------------------------------------------------------------
    revno: 2221.5.16
    merged: dima at hlabs.spb.ru-20070302134414-9mqpmmsii6llb9of
    parent: dima at hlabs.spb.ru-20070302132658-m844rgyc8smzhw3y
    committer: Dmitry Vasiliev <dima at hlabs.spb.ru>
    branch nick: Putty SSH implementation support
    timestamp: Fri 2007-03-02 16:44:14 +0300
    message:
      Added comments for test_cached_vendor
    ------------------------------------------------------------
    revno: 2221.5.15
    merged: dima at hlabs.spb.ru-20070302132658-m844rgyc8smzhw3y
    parent: dima at hlabs.spb.ru-20070302122819-c5cfkd4nnp5tzksp
    committer: Dmitry Vasiliev <dima at hlabs.spb.ru>
    branch nick: Putty SSH implementation support
    timestamp: Fri 2007-03-02 16:26:58 +0300
    message:
      Added docstrings for all SSHVendorManager's methods
    ------------------------------------------------------------
    revno: 2221.5.14
    merged: dima at hlabs.spb.ru-20070302122819-c5cfkd4nnp5tzksp
    parent: dima at hlabs.spb.ru-20070302122200-5oqpv72nae2tr8sl
    committer: Dmitry Vasiliev <dima at hlabs.spb.ru>
    branch nick: Putty SSH implementation support
    timestamp: Fri 2007-03-02 15:28:19 +0300
    message:
      Wrapped long lines
    ------------------------------------------------------------
    revno: 2221.5.13
    merged: dima at hlabs.spb.ru-20070302122200-5oqpv72nae2tr8sl
    parent: dima at hlabs.spb.ru-20070302120557-vasdqot44qytav50
    committer: Dmitry Vasiliev <dima at hlabs.spb.ru>
    branch nick: Putty SSH implementation support
    timestamp: Fri 2007-03-02 15:22:00 +0300
    message:
      Fixed expected message in test
    ------------------------------------------------------------
    revno: 2221.5.12
    merged: dima at hlabs.spb.ru-20070302120557-vasdqot44qytav50
    parent: dima at hlabs.spb.ru-20070302115124-xpya6q8087zrzb5z
    committer: Dmitry Vasiliev <dima at hlabs.spb.ru>
    branch nick: Putty SSH implementation support
    timestamp: Fri 2007-03-02 15:05:57 +0300
    message:
      Added note about SSHVendorManager
    ------------------------------------------------------------
    revno: 2221.5.11
    merged: dima at hlabs.spb.ru-20070302115124-xpya6q8087zrzb5z
    parent: dima at hlabs.spb.ru-20070206185152-qk1p2p0tjn9uf4nm
    parent: pqm at pqm.ubuntu.com-20070301073000-0bfe1394fee5e712
    committer: Dmitry Vasiliev <dima at hlabs.spb.ru>
    branch nick: Putty SSH implementation support
    timestamp: Fri 2007-03-02 14:51:24 +0300
    message:
      Merged bzr.dev
    ------------------------------------------------------------
    revno: 2221.5.10
    merged: dima at hlabs.spb.ru-20070206185152-qk1p2p0tjn9uf4nm
    parent: dima at hlabs.spb.ru-20070206184530-4rtd41xpm9ksgbo5
    committer: Dmitry Vasiliev <dima at hlabs.spb.ru>
    branch nick: Small Fixes
    timestamp: Tue 2007-02-06 21:51:52 +0300
    message:
      Imports placed in alphabetical order
    ------------------------------------------------------------
    revno: 2221.5.9
    merged: dima at hlabs.spb.ru-20070206184530-4rtd41xpm9ksgbo5
    parent: dima at hlabs.spb.ru-20070206182410-knuhr3u2s4xm3upb
    committer: Dmitry Vasiliev <dima at hlabs.spb.ru>
    branch nick: Small Fixes
    timestamp: Tue 2007-02-06 21:45:30 +0300
    message:
      Removed trailing whitespaces and wrapped all long lines
    ------------------------------------------------------------
    revno: 2221.5.8
    merged: dima at hlabs.spb.ru-20070206182410-knuhr3u2s4xm3upb
    parent: dima at hlabs.spb.ru-20070203080847-3mnn4704lz6f2djs
    committer: Dmitry Vasiliev <dima at hlabs.spb.ru>
    branch nick: Small Fixes
    timestamp: Tue 2007-02-06 21:24:10 +0300
    message:
      Added SSHVendorManager.clear_cache() method
    ------------------------------------------------------------
    revno: 2221.5.7
    merged: dima at hlabs.spb.ru-20070203080847-3mnn4704lz6f2djs
    parent: dima at hlabs.spb.ru-20070113135005-he6e52rif5rym8hv
    parent: pqm at pqm.ubuntu.com-20070202204950-910381483d737306
    committer: Dmitry Vasiliev <dima at hlabs.spb.ru>
    branch nick: Small Fixes
    timestamp: Sat 2007-02-03 11:08:47 +0300
    message:
      Merged bzr.dev
    ------------------------------------------------------------
    revno: 2221.5.6
    merged: dima at hlabs.spb.ru-20070113135005-he6e52rif5rym8hv
    parent: dima at hlabs.spb.ru-20070113132723-7b42wbkb488nf725
    committer: Dmitry Vasiliev <dima at hlabs.spb.ru>
    branch nick: Small Fixes
    timestamp: Sat 2007-01-13 16:50:05 +0300
    message:
      Changed tests to make sure the vendor manager returns the same object as was registered
    ------------------------------------------------------------
    revno: 2221.5.5
    merged: dima at hlabs.spb.ru-20070113132723-7b42wbkb488nf725
    parent: dima at hlabs.spb.ru-20070113131305-ncebrfkhqacbbnjf
    committer: Dmitry Vasiliev <dima at hlabs.spb.ru>
    branch nick: Small Fixes
    timestamp: Sat 2007-01-13 16:27:23 +0300
    message:
      Added 'register_default_vendor' method to the SSHVendorManager
    ------------------------------------------------------------
    revno: 2221.5.4
    merged: dima at hlabs.spb.ru-20070113131305-ncebrfkhqacbbnjf
    parent: dima at hlabs.spb.ru-20070113130821-78gaqgwsmyxprr12
    committer: Dmitry Vasiliev <dima at hlabs.spb.ru>
    branch nick: Small Fixes
    timestamp: Sat 2007-01-13 16:13:05 +0300
    message:
      Removed trailing whitespace
    ------------------------------------------------------------
    revno: 2221.5.3
    merged: dima at hlabs.spb.ru-20070113130821-78gaqgwsmyxprr12
    parent: dima at hlabs.spb.ru-20070105161006-r0x531easfpft0k6
    committer: Dmitry Vasiliev <dima at hlabs.spb.ru>
    branch nick: Small Fixes
    timestamp: Sat 2007-01-13 16:08:21 +0300
    message:
      Fixed plink's arguments order. Added tests for such a case.
    ------------------------------------------------------------
    revno: 2221.5.2
    merged: dima at hlabs.spb.ru-20070105161006-r0x531easfpft0k6
    parent: dima at hlabs.spb.ru-20070105154416-z0s48q431s86rm5j
    committer: Dmitry Vasiliev <dima at hlabs.spb.ru>
    branch nick: Small Fixes
    timestamp: Fri 2007-01-05 19:10:06 +0300
    message:
      Added note to the NEWS file
    ------------------------------------------------------------
    revno: 2221.5.1
    merged: dima at hlabs.spb.ru-20070105154416-z0s48q431s86rm5j
    parent: pqm at pqm.ubuntu.com-20070103073947-f9906c0b1f425aa9
    committer: Dmitry Vasiliev <dima at hlabs.spb.ru>
    branch nick: Small Fixes
    timestamp: Fri 2007-01-05 18:44:16 +0300
    message:
      Added support for Putty's SSH implementation
-------------- next part --------------
=== added file 'bzrlib/tests/test_ssh_transport.py'
--- a/bzrlib/tests/test_ssh_transport.py	1970-01-01 00:00:00 +0000
+++ b/bzrlib/tests/test_ssh_transport.py	2007-03-02 14:10:15 +0000
@@ -0,0 +1,202 @@
+# Copyright (C) 2004, 2005, 2006, 2007 Canonical Ltd
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+from bzrlib.tests import TestCase
+from bzrlib.errors import SSHVendorNotFound, UnknownSSH
+from bzrlib.transport.ssh import (
+    OpenSSHSubprocessVendor,
+    PLinkSubprocessVendor,
+    SSHCorpSubprocessVendor,
+    SSHVendorManager,
+    )
+
+
+class TestSSHVendorManager(SSHVendorManager):
+
+    _ssh_version_string = ""
+
+    def set_ssh_version_string(self, version):
+        self._ssh_version_string = version
+
+    def _get_ssh_version_string(self, args):
+        return self._ssh_version_string
+
+
+class SSHVendorManagerTests(TestCase):
+
+    def test_register_vendor(self):
+        manager = TestSSHVendorManager()
+        self.assertRaises(SSHVendorNotFound, manager.get_vendor, {})
+        vendor = object()
+        manager.register_vendor("vendor", vendor)
+        self.assertIs(manager.get_vendor({"BZR_SSH": "vendor"}), vendor)
+
+    def test_default_vendor(self):
+        manager = TestSSHVendorManager()
+        self.assertRaises(SSHVendorNotFound, manager.get_vendor, {})
+        vendor = object()
+        manager.register_default_vendor(vendor)
+        self.assertIs(manager.get_vendor({}), vendor)
+
+    def test_get_vendor_by_environment(self):
+        manager = TestSSHVendorManager()
+        self.assertRaises(SSHVendorNotFound, manager.get_vendor, {})
+        self.assertRaises(UnknownSSH,
+            manager.get_vendor, {"BZR_SSH": "vendor"})
+        vendor = object()
+        manager.register_vendor("vendor", vendor)
+        self.assertIs(manager.get_vendor({"BZR_SSH": "vendor"}), vendor)
+
+    def test_get_vendor_by_inspection_openssh(self):
+        manager = TestSSHVendorManager()
+        self.assertRaises(SSHVendorNotFound, manager.get_vendor, {})
+        manager.set_ssh_version_string("OpenSSH")
+        self.assertIsInstance(manager.get_vendor({}), OpenSSHSubprocessVendor)
+
+    def test_get_vendor_by_inspection_sshcorp(self):
+        manager = TestSSHVendorManager()
+        self.assertRaises(SSHVendorNotFound, manager.get_vendor, {})
+        manager.set_ssh_version_string("SSH Secure Shell")
+        self.assertIsInstance(manager.get_vendor({}), SSHCorpSubprocessVendor)
+
+    def test_get_vendor_by_inspection_plink(self):
+        manager = TestSSHVendorManager()
+        self.assertRaises(SSHVendorNotFound, manager.get_vendor, {})
+        manager.set_ssh_version_string("plink")
+        self.assertIsInstance(manager.get_vendor({}), PLinkSubprocessVendor)
+
+    def test_cached_vendor(self):
+        manager = TestSSHVendorManager()
+        self.assertRaises(SSHVendorNotFound, manager.get_vendor, {})
+        vendor = object()
+        manager.register_vendor("vendor", vendor)
+        self.assertRaises(SSHVendorNotFound, manager.get_vendor, {})
+        # Once the vendor is found the result is cached (mainly because of the
+        # 'get_vendor' sometimes can be an expensive operation) and later
+        # invocations of the 'get_vendor' just returns the cached value.
+        self.assertIs(manager.get_vendor({"BZR_SSH": "vendor"}), vendor)
+        self.assertIs(manager.get_vendor({}), vendor)
+        # The cache can be cleared by the 'clear_cache' method
+        manager.clear_cache()
+        self.assertRaises(SSHVendorNotFound, manager.get_vendor, {})
+
+    def test_get_vendor_search_order(self):
+        # The 'get_vendor' method search for SSH vendors as following:
+        #
+        #   1. Check previously cached value
+        #   2. Check BZR_SSH environment variable
+        #   3. Check the system for known SSH vendors
+        #   4. Fall back to the default vendor if registered
+        #
+        # Let's now check the each check method in the reverse order
+        # clearing the cache between each invocation:
+
+        manager = TestSSHVendorManager()
+        # At first no vendors are found
+        self.assertRaises(SSHVendorNotFound, manager.get_vendor, {})
+
+        # If the default vendor is registered it will be returned
+        default_vendor = object()
+        manager.register_default_vendor(default_vendor)
+        self.assertIs(manager.get_vendor({}), default_vendor)
+
+        # If the known vendor is found in the system it will be returned
+        manager.clear_cache()
+        manager.set_ssh_version_string("OpenSSH")
+        self.assertIsInstance(manager.get_vendor({}), OpenSSHSubprocessVendor)
+
+        # If the BZR_SSH environment variable is found it will be treated as
+        # the vendor name
+        manager.clear_cache()
+        vendor = object()
+        manager.register_vendor("vendor", vendor)
+        self.assertIs(manager.get_vendor({"BZR_SSH": "vendor"}), vendor)
+
+        # Last cached value always checked first
+        self.assertIs(manager.get_vendor({}), vendor)
+
+
+class SubprocessVendorsTests(TestCase):
+
+    def test_openssh_command_arguments(self):
+        vendor = OpenSSHSubprocessVendor()
+        self.assertEqual(
+            vendor._get_vendor_specific_argv(
+                "user", "host", 100, command=["bzr"]),
+            ["ssh", "-oForwardX11=no", "-oForwardAgent=no",
+                "-oClearAllForwardings=yes", "-oProtocol=2",
+                "-oNoHostAuthenticationForLocalhost=yes",
+                "-p", "100",
+                "-l", "user",
+                "host", "bzr"]
+            )
+
+    def test_openssh_subsystem_arguments(self):
+        vendor = OpenSSHSubprocessVendor()
+        self.assertEqual(
+            vendor._get_vendor_specific_argv(
+                "user", "host", 100, subsystem="sftp"),
+            ["ssh", "-oForwardX11=no", "-oForwardAgent=no",
+                "-oClearAllForwardings=yes", "-oProtocol=2",
+                "-oNoHostAuthenticationForLocalhost=yes",
+                "-p", "100",
+                "-l", "user",
+                "-s", "host", "sftp"]
+            )
+
+    def test_sshcorp_command_arguments(self):
+        vendor = SSHCorpSubprocessVendor()
+        self.assertEqual(
+            vendor._get_vendor_specific_argv(
+                "user", "host", 100, command=["bzr"]),
+            ["ssh", "-x",
+                "-p", "100",
+                "-l", "user",
+                "host", "bzr"]
+            )
+
+    def test_sshcorp_subsystem_arguments(self):
+        vendor = SSHCorpSubprocessVendor()
+        self.assertEqual(
+            vendor._get_vendor_specific_argv(
+                "user", "host", 100, subsystem="sftp"),
+            ["ssh", "-x",
+                "-p", "100",
+                "-l", "user",
+                "-s", "sftp", "host"]
+            )
+
+    def test_plink_command_arguments(self):
+        vendor = PLinkSubprocessVendor()
+        self.assertEqual(
+            vendor._get_vendor_specific_argv(
+                "user", "host", 100, command=["bzr"]),
+            ["plink", "-x", "-a", "-ssh", "-2",
+                "-P", "100",
+                "-l", "user",
+                "host", "bzr"]
+            )
+
+    def test_plink_subsystem_arguments(self):
+        vendor = PLinkSubprocessVendor()
+        self.assertEqual(
+            vendor._get_vendor_specific_argv(
+                "user", "host", 100, subsystem="sftp"),
+            ["plink", "-x", "-a", "-ssh", "-2",
+                "-P", "100",
+                "-l", "user",
+                "-s", "host", "sftp"]
+            )

=== modified file 'NEWS'
--- a/NEWS	2007-03-07 12:00:12 +0000
+++ b/NEWS	2007-03-07 13:20:25 +0000
@@ -30,6 +30,8 @@
 
   IMPROVEMENTS:
 
+    * Added support for Putty's SSH implementation. (Dmitry Vasiliev)
+
     * Support for OS Windows 98. Also .bzr.log on any windows system
       saved in My Documents folder. (Alexander Belchenko)
 
@@ -105,6 +107,9 @@
 
   INTERNALS:
 
+    * Refactored SSH vendor registration into SSHVendorManager class.
+      (Dmitry Vasiliev)
+
     * Internally revision ids and file ids are now passed around as utf-8
       bytestrings, rather than treating them as Unicode strings. This has
       performance benefits for Knits, since we no longer need to decode the

=== modified file 'bzrlib/errors.py'
--- a/bzrlib/errors.py	2007-03-05 04:55:34 +0000
+++ b/bzrlib/errors.py	2007-03-07 13:20:25 +0000
@@ -132,11 +132,12 @@
     # readable explanation
 
     def __init__(self, *args, **kwds):
-        # XXX: Use the underlying BzrError to always generate the args attribute
-        # if it doesn't exist.  We can't use super here, because exceptions are
-        # old-style classes in python2.4 (but new in 2.5).  --bmc, 20060426
+        # XXX: Use the underlying BzrError to always generate the args
+        # attribute if it doesn't exist.  We can't use super here, because
+        # exceptions are old-style classes in python2.4 (but new in 2.5).
+        # --bmc, 20060426
         symbol_versioning.warn('BzrNewError was deprecated in bzr 0.13; '
-             'please convert %s to use BzrError instead' 
+             'please convert %s to use BzrError instead'
              % self.__class__.__name__,
              DeprecationWarning,
              stacklevel=2)
@@ -223,8 +224,8 @@
 
 class InventoryModified(BzrError):
 
-    _fmt = ("The current inventory for the tree %(tree)r has been modified, "
-            "so a clean inventory cannot be read without data loss.")
+    _fmt = ("The current inventory for the tree %(tree)r has been modified,"
+            " so a clean inventory cannot be read without data loss.")
 
     internal_error = True
 
@@ -342,7 +343,7 @@
     """Used when renaming and both source and dest exist."""
 
     _fmt = ("Could not rename %(source)s => %(dest)s because both files exist."
-         "%(extra)s")
+            "%(extra)s")
 
     def __init__(self, source, dest, extra=None):
         BzrError.__init__(self)
@@ -425,7 +426,8 @@
 
 class ShortReadvError(PathError):
 
-    _fmt = "readv() read %(actual)s bytes rather than %(length)s bytes at %(offset)s for %(path)s%(extra)s"
+    _fmt = ("readv() read %(actual)s bytes rather than %(length)s bytes"
+            " at %(offset)s for %(path)s%(extra)s")
 
     internal_error = True
 
@@ -482,7 +484,8 @@
 
 class AtomicFileAlreadyClosed(PathError):
 
-    _fmt = "'%(function)s' called on an AtomicFile after it was closed: %(path)s"
+    _fmt = ("'%(function)s' called on an AtomicFile after it was closed:"
+            " %(path)s")
 
     def __init__(self, path, function):
         PathError.__init__(self, path=path, extra=None)
@@ -491,7 +494,8 @@
 
 class InaccessibleParent(PathError):
 
-    _fmt = "Parent not accessible given base %(base)s and relative path %(path)s"
+    _fmt = ("Parent not accessible given base %(base)s and"
+            " relative path %(path)s")
 
     def __init__(self, path, base):
         PathError.__init__(self, path)
@@ -686,7 +690,8 @@
 
 class OutSideTransaction(BzrError):
 
-    _fmt = "A transaction related operation was attempted after the transaction finished."
+    _fmt = ("A transaction related operation was attempted after"
+            " the transaction finished.")
 
 
 class ObjectNotLocked(LockError):
@@ -730,7 +735,8 @@
 
 class LockBroken(LockError):
 
-    _fmt = "Lock was broken while still open: %(lock)s - check storage consistency!"
+    _fmt = ("Lock was broken while still open: %(lock)s"
+            " - check storage consistency!")
 
     internal_error = False
 
@@ -740,7 +746,8 @@
 
 class LockBreakMismatch(LockError):
 
-    _fmt = "Lock was released and re-acquired before being broken: %(lock)s: held by %(holder)r, wanted to break %(target)r"
+    _fmt = ("Lock was released and re-acquired before being broken:"
+            " %(lock)s: held by %(holder)r, wanted to break %(target)r")
 
     internal_error = False
 
@@ -796,8 +803,8 @@
 
 class NotLeftParentDescendant(BzrError):
 
-    _fmt = "Revision %(old_revision)s is not the left parent of"\
-        " %(new_revision)s, but branch %(branch_location)s expects this"
+    _fmt = ("Revision %(old_revision)s is not the left parent of"
+            " %(new_revision)s, but branch %(branch_location)s expects this")
 
     internal_error = True
 
@@ -828,7 +835,8 @@
 
 class InvalidRevisionSpec(BzrError):
 
-    _fmt = "Requested revision: %(spec)r does not exist in branch: %(branch)s%(extra)s"
+    _fmt = ("Requested revision: %(spec)r does not exist in branch:"
+            " %(branch)s%(extra)s")
 
     def __init__(self, spec, branch, extra=None):
         BzrError.__init__(self, branch=branch, spec=spec)
@@ -845,9 +853,9 @@
 
 class AppendRevisionsOnlyViolation(BzrError):
 
-    _fmt = 'Operation denied because it would change the main history, '\
-           'which is not permitted by the append_revisions_only setting on'\
-           ' branch "%(location)s".'
+    _fmt = ('Operation denied because it would change the main history,'
+           ' which is not permitted by the append_revisions_only setting on'
+           ' branch "%(location)s".')
 
     def __init__(self, location):
        import bzrlib.urlutils as urlutils
@@ -856,8 +864,9 @@
 
 
 class DivergedBranches(BzrError):
-    
-    _fmt = "These branches have diverged.  Use the merge command to reconcile them."""
+
+    _fmt = ("These branches have diverged."
+            " Use the merge command to reconcile them.")
 
     internal_error = False
 
@@ -878,7 +887,8 @@
 
 class UnrelatedBranches(BzrError):
 
-    _fmt = "Branches have no common ancestor, and no merge base revision was specified."
+    _fmt = ("Branches have no common ancestor, and"
+            " no merge base revision was specified.")
 
     internal_error = False
 
@@ -894,8 +904,8 @@
 
 class NoCommonRoot(BzrError):
 
-    _fmt = "Revisions are not derived from the same root: " \
-           "%(revision_a)s %(revision_b)s."
+    _fmt = ("Revisions are not derived from the same root: "
+           "%(revision_a)s %(revision_b)s.")
 
     def __init__(self, revision_a, revision_b):
         BzrError.__init__(self, revision_a=revision_a, revision_b=revision_b)
@@ -924,8 +934,8 @@
     def __init__(self, bases):
         warn("BzrError AmbiguousBase has been deprecated as of bzrlib 0.8.",
                 DeprecationWarning)
-        msg = "The correct base is unclear, because %s are all equally close" %\
-            ", ".join(bases)
+        msg = ("The correct base is unclear, because %s are all equally close"
+                % ", ".join(bases))
         BzrError.__init__(self, msg)
         self.bases = bases
 
@@ -953,7 +963,8 @@
 
 class BoundBranchOutOfDate(BzrError):
 
-    _fmt = "Bound branch %(branch)s is out of date with master branch %(master)s."
+    _fmt = ("Bound branch %(branch)s is out of date"
+            " with master branch %(master)s.")
 
     def __init__(self, branch, master):
         BzrError.__init__(self)
@@ -963,7 +974,8 @@
         
 class CommitToDoubleBoundBranch(BzrError):
 
-    _fmt = "Cannot commit to branch %(branch)s. It is bound to %(master)s, which is bound to %(remote)s."
+    _fmt = ("Cannot commit to branch %(branch)s."
+            " It is bound to %(master)s, which is bound to %(remote)s.")
 
     def __init__(self, branch, master, remote):
         BzrError.__init__(self)
@@ -983,7 +995,8 @@
 
 class BoundBranchConnectionFailure(BzrError):
 
-    _fmt = "Unable to connect to target of bound branch %(branch)s => %(target)s: %(error)s"
+    _fmt = ("Unable to connect to target of bound branch %(branch)s"
+            " => %(target)s: %(error)s")
 
     def __init__(self, branch, target, error):
         BzrError.__init__(self)
@@ -1043,7 +1056,8 @@
 
 class WeaveTextDiffers(WeaveError):
 
-    _fmt = "Weaves differ on text content. Revision: {%(revision_id)s}, %(weave_a)s, %(weave_b)s"
+    _fmt = ("Weaves differ on text content. Revision:"
+            " {%(revision_id)s}, %(weave_a)s, %(weave_b)s")
 
     def __init__(self, revision_id, weave_a, weave_b):
         WeaveError.__init__(self)
@@ -1054,7 +1068,8 @@
 
 class WeaveTextDiffers(WeaveError):
 
-    _fmt = "Weaves differ on text content. Revision: {%(revision_id)s}, %(weave_a)s, %(weave_b)s"
+    _fmt = ("Weaves differ on text content. Revision:"
+            " {%(revision_id)s}, %(weave_a)s, %(weave_b)s")
 
     def __init__(self, revision_id, weave_a, weave_b):
         WeaveError.__init__(self)
@@ -1157,9 +1172,9 @@
 
 class TooManyConcurrentRequests(BzrError):
 
-    _fmt = ("The medium '%(medium)s' has reached its concurrent request limit. "
-            "Be sure to finish_writing and finish_reading on the "
-            "current request that is open.")
+    _fmt = ("The medium '%(medium)s' has reached its concurrent request limit."
+            " Be sure to finish_writing and finish_reading on the"
+            " current request that is open.")
 
     internal_error = True
 
@@ -1292,8 +1307,8 @@
 
 class CantReprocessAndShowBase(BzrError):
 
-    _fmt = "Can't reprocess and show base, because reprocessing obscures " \
-           "the relationship of conflicting lines to the base"
+    _fmt = ("Can't reprocess and show base, because reprocessing obscures "
+           "the relationship of conflicting lines to the base")
 
 
 class GraphCycleError(BzrError):
@@ -1348,9 +1363,9 @@
 
 
 class MustUseDecorated(Exception):
-    
-    _fmt = """A decorating function has requested its original command be used."""
-    
+
+    _fmt = "A decorating function has requested its original command be used."
+
 
 class NoBundleFound(BzrError):
 
@@ -1373,7 +1388,8 @@
 
 class MissingText(BzrError):
 
-    _fmt = "Branch %(base)s is missing revision %(text_revision)s of %(file_id)s"
+    _fmt = ("Branch %(base)s is missing revision"
+            " %(text_revision)s of %(file_id)s")
 
     def __init__(self, branch, text_revision, file_id):
         BzrError.__init__(self)
@@ -1496,7 +1512,8 @@
 
 class BzrBadParameterUnicode(BzrBadParameter):
 
-    _fmt = "Parameter %(param)s is unicode but only byte-strings are permitted."
+    _fmt = ("Parameter %(param)s is unicode but"
+            " only byte-strings are permitted.")
 
 
 class BzrBadParameterContainsNewline(BzrBadParameter):
@@ -1600,8 +1617,8 @@
 
 class CorruptRepository(BzrError):
 
-    _fmt = """An error has been detected in the repository %(repo_path)s.
-Please run bzr reconcile on this repository."""
+    _fmt = ("An error has been detected in the repository %(repo_path)s.\n"
+            "Please run bzr reconcile on this repository.")
 
     def __init__(self, repo):
         BzrError.__init__(self)
@@ -1629,8 +1646,8 @@
 
 class InvalidProgressBarType(BzrError):
 
-    _fmt = """Environment variable BZR_PROGRESS_BAR='%(bar_type)s is not a supported type
-Select one of: %(valid_types)s"""
+    _fmt = ("Environment variable BZR_PROGRESS_BAR='%(bar_type)s"
+            " is not a supported type Select one of: %(valid_types)s")
 
     def __init__(self, bar_type, valid_types):
         BzrError.__init__(self, bar_type=bar_type, valid_types=valid_types)
@@ -1638,7 +1655,8 @@
 
 class UnsupportedOperation(BzrError):
 
-    _fmt = "The method %(mname)s is not supported on objects of type %(tname)s."
+    _fmt = ("The method %(mname)s is not supported on"
+            " objects of type %(tname)s.")
 
     def __init__(self, method, method_self):
         self.method = method
@@ -1651,7 +1669,9 @@
 
 
 class NonAsciiRevisionId(UnsupportedOperation):
-    """Raised when a commit is attempting to set a non-ascii revision id but cant."""
+    """Raised when a commit is attempting to set a non-ascii revision id
+       but cant.
+    """
 
 
 class BinaryFile(BzrError):
@@ -1670,8 +1690,8 @@
 
 class TestamentMismatch(BzrError):
 
-    _fmt = """Testament did not match expected value.  
-       For revision_id {%(revision_id)s}, expected {%(expected)s}, measured 
+    _fmt = """Testament did not match expected value.
+       For revision_id {%(revision_id)s}, expected {%(expected)s}, measured
        {%(measured)s}"""
 
     def __init__(self, revision_id, expected, measured):
@@ -1778,6 +1798,12 @@
         self.vendor = vendor
 
 
+class SSHVendorNotFound(BzrError):
+
+    _fmt = ("Don't know how to handle SSH connections."
+            " Please set BZR_SSH environment variable.")
+
+
 class GhostRevisionUnusableHere(BzrError):
 
     _fmt = "Ghost revision {%(revision_id)s} cannot be used here."
@@ -1789,7 +1815,8 @@
 
 class IllegalUseOfScopeReplacer(BzrError):
 
-    _fmt = "ScopeReplacer object %(name)r was used incorrectly: %(msg)s%(extra)s"
+    _fmt = ("ScopeReplacer object %(name)r was used incorrectly:"
+            " %(msg)s%(extra)s")
 
     internal_error = True
 
@@ -1817,7 +1844,8 @@
 
 class ImportNameCollision(BzrError):
 
-    _fmt = "Tried to import an object to the same name as an existing object. %(name)s"
+    _fmt = ("Tried to import an object to the same name as"
+            " an existing object. %(name)s")
 
     internal_error = True
 
@@ -1874,7 +1902,8 @@
 
 class TagsNotSupported(BzrError):
 
-    _fmt = "Tags not supported by %(branch)s; you may be able to use bzr upgrade."
+    _fmt = ("Tags not supported by %(branch)s;"
+            " you may be able to use bzr upgrade.")
 
     def __init__(self, branch):
         self.branch = branch

=== modified file 'bzrlib/tests/__init__.py'
--- a/bzrlib/tests/__init__.py	2007-03-08 22:08:08 +0000
+++ b/bzrlib/tests/__init__.py	2007-03-09 00:45:24 +0000
@@ -1981,6 +1981,7 @@
                    'bzrlib.tests.test_smart_add',
                    'bzrlib.tests.test_smart_transport',
                    'bzrlib.tests.test_source',
+                   'bzrlib.tests.test_ssh_transport',
                    'bzrlib.tests.test_status',
                    'bzrlib.tests.test_store',
                    'bzrlib.tests.test_subsume',

=== modified file 'bzrlib/tests/blackbox/test_pull.py'
--- a/bzrlib/tests/blackbox/test_pull.py	2007-02-25 11:49:16 +0000
+++ b/bzrlib/tests/blackbox/test_pull.py	2007-03-02 12:22:00 +0000
@@ -260,7 +260,8 @@
         tree_b.commit('commit d')
         out = self.runbzr('pull ../branch_a', retcode=3)
         self.assertEquals(out,
-                ('','bzr: ERROR: These branches have diverged.  Use the merge command to reconcile them.\n'))
+                ('','bzr: ERROR: These branches have diverged.'
+                    ' Use the merge command to reconcile them.\n'))
         self.assertEquals(branch_b.get_parent(), parent)
         # test implicit --remember after resolving previous failure
         uncommit(branch=branch_b, tree=tree_b)

=== modified file 'bzrlib/tests/test_sftp_transport.py'
--- a/bzrlib/tests/test_sftp_transport.py	2007-01-02 19:35:19 +0000
+++ b/bzrlib/tests/test_sftp_transport.py	2007-03-02 15:38:43 +0000
@@ -1,4 +1,5 @@
-# Copyright (C) 2005 Robey Pointer <robey at lag.net>, Canonical Ltd
+# Copyright (C) 2005 Robey Pointer <robey at lag.net>
+# Copyright (C) 2005, 2006, 2007 Canonical Ltd
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -205,16 +206,16 @@
         """Test that if no 'ssh' is available we get builtin paramiko"""
         from bzrlib.transport import ssh
         # set '.' as the only location in the path, forcing no 'ssh' to exist
-        orig_vendor = ssh._ssh_vendor
+        orig_vendor = ssh._ssh_vendor_manager._cached_ssh_vendor
         orig_path = set_or_unset_env('PATH', '.')
         try:
             # No vendor defined yet, query for one
-            ssh._ssh_vendor = None
+            ssh._ssh_vendor_manager.clear_cache()
             vendor = ssh._get_ssh_vendor()
             self.assertIsInstance(vendor, ssh.ParamikoVendor)
         finally:
             set_or_unset_env('PATH', orig_path)
-            ssh._ssh_vendor = orig_vendor
+            ssh._ssh_vendor_manager._cached_ssh_vendor = orig_vendor
 
     def test_abspath_root_sibling_server(self):
         from bzrlib.transport.sftp import SFTPSiblingAbsoluteServer
@@ -338,15 +339,15 @@
         s.bind(('localhost', 0))
         self.bogus_url = 'sftp://%s:%s/' % s.getsockname()
 
-        orig_vendor = bzrlib.transport.ssh._ssh_vendor
+        orig_vendor = bzrlib.transport.ssh._ssh_vendor_manager._cached_ssh_vendor
         def reset():
-            bzrlib.transport.ssh._ssh_vendor = orig_vendor
+            bzrlib.transport.ssh._ssh_vendor_manager._cached_ssh_vendor = orig_vendor
             s.close()
         self.addCleanup(reset)
 
     def set_vendor(self, vendor):
         import bzrlib.transport.ssh
-        bzrlib.transport.ssh._ssh_vendor = vendor
+        bzrlib.transport.ssh._ssh_vendor_manager._cached_ssh_vendor = vendor
 
     def test_bad_connection_paramiko(self):
         """Test that a real connection attempt raises the right error"""

=== modified file 'bzrlib/transport/sftp.py'
--- a/bzrlib/transport/sftp.py	2007-01-02 19:35:19 +0000
+++ b/bzrlib/transport/sftp.py	2007-03-07 13:20:25 +0000
@@ -1,5 +1,5 @@
 # Copyright (C) 2005 Robey Pointer <robey at lag.net>
-# Copyright (C) 2005, 2006 Canonical Ltd
+# Copyright (C) 2005, 2006, 2007 Canonical Ltd
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -1069,8 +1069,8 @@
         event.wait(5.0)
     
     def setUp(self):
-        self._original_vendor = ssh._ssh_vendor
-        ssh._ssh_vendor = self._vendor
+        self._original_vendor = ssh._ssh_vendor_manager._cached_ssh_vendor
+        ssh._ssh_vendor_manager._cached_ssh_vendor = self._vendor
         if sys.platform == 'win32':
             # Win32 needs to use the UNICODE api
             self._homedir = getcwd()
@@ -1089,7 +1089,7 @@
     def tearDown(self):
         """See bzrlib.transport.Server.tearDown."""
         self._listener.stop()
-        ssh._ssh_vendor = self._original_vendor
+        ssh._ssh_vendor_manager._cached_ssh_vendor = self._original_vendor
 
     def get_bogus_url(self):
         """See bzrlib.transport.Server.get_bogus_url."""

=== modified file 'bzrlib/transport/ssh.py'
--- a/bzrlib/transport/ssh.py	2006-11-17 03:57:45 +0000
+++ b/bzrlib/transport/ssh.py	2007-03-07 13:47:47 +0000
@@ -1,5 +1,5 @@
 # Copyright (C) 2005 Robey Pointer <robey at lag.net>
-# Copyright (C) 2005, 2006 Canonical Ltd
+# Copyright (C) 2005, 2006, 2007 Canonical Ltd
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -28,6 +28,7 @@
 from bzrlib.errors import (ConnectionError,
                            ParamikoNotPresent,
                            SocketConnectionError,
+                           SSHVendorNotFound,
                            TransportError,
                            UnknownSSH,
                            )
@@ -58,55 +59,110 @@
 # connect to an agent if we are on win32 and using Paramiko older than 1.6
 _use_ssh_agent = (sys.platform != 'win32' or _paramiko_version >= (1, 6, 0))
 
-_ssh_vendors = {}
-
-def register_ssh_vendor(name, vendor):
-    """Register SSH vendor."""
-    _ssh_vendors[name] = vendor
-
-    
-_ssh_vendor = None
-def _get_ssh_vendor():
-    """Find out what version of SSH is on the system."""
-    global _ssh_vendor
-    if _ssh_vendor is not None:
-        return _ssh_vendor
-
-    if 'BZR_SSH' in os.environ:
-        vendor_name = os.environ['BZR_SSH']
+
+class SSHVendorManager(object):
+    """Manager for manage SSH vendors."""
+
+    # Note, although at first sign the class interface seems similar to
+    # bzrlib.registry.Registry it is not possible/convenient to directly use
+    # the Registry because the class just has "get()" interface instead of the
+    # Registry's "get(key)".
+
+    def __init__(self):
+        self._ssh_vendors = {}
+        self._cached_ssh_vendor = None
+        self._default_ssh_vendor = None
+
+    def register_default_vendor(self, vendor):
+        """Register default SSH vendor."""
+        self._default_ssh_vendor = vendor
+
+    def register_vendor(self, name, vendor):
+        """Register new SSH vendor by name."""
+        self._ssh_vendors[name] = vendor
+
+    def clear_cache(self):
+        """Clear previously cached lookup result."""
+        self._cached_ssh_vendor = None
+
+    def _get_vendor_by_environment(self, environment=None):
+        """Return the vendor or None based on BZR_SSH environment variable.
+
+        :raises UnknownSSH: if the BZR_SSH environment variable contains
+                            unknown vendor name
+        """
+        if environment is None:
+            environment = os.environ
+        if 'BZR_SSH' in environment:
+            vendor_name = environment['BZR_SSH']
+            try:
+                vendor = self._ssh_vendors[vendor_name]
+            except KeyError:
+                raise UnknownSSH(vendor_name)
+            return vendor
+        return None
+
+    def _get_ssh_version_string(self, args):
+        """Return SSH version string from the subprocess."""
         try:
-            _ssh_vendor = _ssh_vendors[vendor_name]
-        except KeyError:
-            raise UnknownSSH(vendor_name)
-        return _ssh_vendor
-
-    try:
-        p = subprocess.Popen(['ssh', '-V'],
-                             stdin=subprocess.PIPE,
-                             stdout=subprocess.PIPE,
-                             stderr=subprocess.PIPE,
-                             **os_specific_subprocess_params())
-        returncode = p.returncode
-        stdout, stderr = p.communicate()
-    except OSError:
-        returncode = -1
-        stdout = stderr = ''
-    if 'OpenSSH' in stderr:
-        mutter('ssh implementation is OpenSSH')
-        _ssh_vendor = OpenSSHSubprocessVendor()
-    elif 'SSH Secure Shell' in stderr:
-        mutter('ssh implementation is SSH Corp.')
-        _ssh_vendor = SSHCorpSubprocessVendor()
-
-    if _ssh_vendor is not None:
-        return _ssh_vendor
-
-    # XXX: 20051123 jamesh
-    # A check for putty's plink or lsh would go here.
-
-    mutter('falling back to paramiko implementation')
-    _ssh_vendor = ParamikoVendor()
-    return _ssh_vendor
+            p = subprocess.Popen(args,
+                                 stdout=subprocess.PIPE,
+                                 stderr=subprocess.PIPE,
+                                 **os_specific_subprocess_params())
+            stdout, stderr = p.communicate()
+        except OSError:
+            stdout = stderr = ''
+        return stdout + stderr
+
+    def _get_vendor_by_version_string(self, version):
+        """Return the vendor or None based on output from the subprocess.
+
+        :param version: The output of 'ssh -V' like command.
+        """
+        vendor = None
+        if 'OpenSSH' in version:
+            mutter('ssh implementation is OpenSSH')
+            vendor = OpenSSHSubprocessVendor()
+        elif 'SSH Secure Shell' in version:
+            mutter('ssh implementation is SSH Corp.')
+            vendor = SSHCorpSubprocessVendor()
+        elif 'plink' in version:
+            mutter("ssh implementation is Putty's plink.")
+            vendor = PLinkSubprocessVendor()
+        return vendor
+
+    def _get_vendor_by_inspection(self):
+        """Return the vendor or None by checking for known SSH implementations."""
+        for args in [['ssh', '-V'], ['plink', '-V']]:
+            version = self._get_ssh_version_string(args)
+            vendor = self._get_vendor_by_version_string(version)
+            if vendor is not None:
+                return vendor
+        return None
+
+    def get_vendor(self, environment=None):
+        """Find out what version of SSH is on the system.
+
+        :raises SSHVendorNotFound: if no any SSH vendor is found
+        :raises UnknownSSH: if the BZR_SSH environment variable contains
+                            unknown vendor name
+        """
+        if self._cached_ssh_vendor is None:
+            vendor = self._get_vendor_by_environment(environment)
+            if vendor is None:
+                vendor = self._get_vendor_by_inspection()
+                if vendor is None:
+                    mutter('falling back to default implementation')
+                    vendor = self._default_ssh_vendor
+                    if vendor is None:
+                        raise SSHVendorNotFound()
+            self._cached_ssh_vendor = vendor
+        return self._cached_ssh_vendor
+
+_ssh_vendor_manager = SSHVendorManager()
+_get_ssh_vendor = _ssh_vendor_manager.get_vendor
+register_default_ssh_vendor = _ssh_vendor_manager.register_default_vendor
+register_ssh_vendor = _ssh_vendor_manager.register_vendor
 
 
 def _ignore_sigint():
@@ -115,7 +171,6 @@
     # <https://launchpad.net/products/bzr/+bug/41433/+index>
     import signal
     signal.signal(signal.SIGINT, signal.SIG_IGN)
-    
 
 
 class LoopbackSFTP(object):
@@ -263,7 +318,11 @@
                                          msg='Unable to invoke remote bzr')
 
 if paramiko is not None:
-    register_ssh_vendor('paramiko', ParamikoVendor())
+    vendor = ParamikoVendor()
+    register_ssh_vendor('paramiko', vendor)
+    register_ssh_vendor('none', vendor)
+    register_default_ssh_vendor(vendor)
+    del vendor
 
 
 class SubprocessVendor(SSHVendor):
@@ -315,8 +374,6 @@
         """
         raise NotImplementedError(self._get_vendor_specific_argv)
 
-register_ssh_vendor('none', ParamikoVendor())
-
 
 class OpenSSHSubprocessVendor(SubprocessVendor):
     """SSH vendor that uses the 'ssh' executable from OpenSSH."""
@@ -369,6 +426,30 @@
 register_ssh_vendor('ssh', SSHCorpSubprocessVendor())
 
 
+class PLinkSubprocessVendor(SubprocessVendor):
+    """SSH vendor that uses the 'plink' executable from Putty."""
+
+    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
+                                  command=None):
+        assert subsystem is not None or command is not None, (
+            'Must specify a command or subsystem')
+        if subsystem is not None:
+            assert command is None, (
+                'subsystem and command are mutually exclusive')
+        args = ['plink', '-x', '-a', '-ssh', '-2']
+        if port is not None:
+            args.extend(['-P', str(port)])
+        if username is not None:
+            args.extend(['-l', username])
+        if subsystem is not None:
+            args.extend(['-s', host, subsystem])
+        else:
+            args.extend([host] + command)
+        return args
+
+register_ssh_vendor('plink', PLinkSubprocessVendor())
+
+
 def _paramiko_auth(username, password, host, paramiko_transport):
     # paramiko requires a username, but it might be none if nothing was supplied
     # use the local username, just in case.



More information about the bazaar-commits mailing list