Browse Source

Imported Upstream version 0.11.2

Jelmer Vernooij 9 years ago
parent
commit
2f6535e170

+ 14 - 0
NEWS

@@ -1,3 +1,17 @@
+0.11.2	2015-09-18
+
+ IMPROVEMENTS
+
+  * Add support for agent= capability. (Jelmer Vernooij, #298)
+
+  * Add support for quiet capability. (Jelmer Vernooij)
+
+ CHANGES
+
+  * The ParamikoSSHVendor class has been moved to
+  * dulwich.contrib.paramiko_vendor, as it's currently untested.
+    (Jelmer Vernooij, #364)
+
 0.11.1	2015-09-13
 
  Fix-up release to exclude broken blame.py file.

+ 1 - 1
PKG-INFO

@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: dulwich
-Version: 0.11.1
+Version: 0.11.2
 Summary: Python Git Library
 Home-page: https://www.dulwich.io/
 Author: Jelmer Vernooij

+ 1 - 1
dulwich.egg-info/PKG-INFO

@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: dulwich
-Version: 0.11.1
+Version: 0.11.2
 Summary: Python Git Library
 Home-page: https://www.dulwich.io/
 Author: Jelmer Vernooij

+ 1 - 0
dulwich.egg-info/SOURCES.txt

@@ -63,6 +63,7 @@ dulwich.egg-info/dependency_links.txt
 dulwich.egg-info/pbr.json
 dulwich.egg-info/top_level.txt
 dulwich/contrib/__init__.py
+dulwich/contrib/paramiko_vendor.py
 dulwich/contrib/swift.py
 dulwich/contrib/test_swift.py
 dulwich/contrib/test_swift_smoke.py

+ 1 - 1
dulwich.egg-info/pbr.json

@@ -1 +1 @@
-{"is_release": false, "git_version": "b732930"}
+{"is_release": false, "git_version": "8f25842"}

+ 1 - 1
dulwich/__init__.py

@@ -21,4 +21,4 @@
 
 """Python implementation of the Git file formats and protocols."""
 
-__version__ = (0, 11, 1)
+__version__ = (0, 11, 2)

+ 30 - 125
dulwich/client.py

@@ -1,6 +1,5 @@
-# client.py -- Implementation of the server side git protocols
+# client.py -- Implementation of the client side git protocols
 # Copyright (C) 2008-2013 Jelmer Vernooij <jelmer@samba.org>
-# Copyright (C) 2008 John Carr
 #
 # This program is free software; you can redistribute it and/or
 # modify it under the terms of the GNU General Public License
@@ -26,6 +25,7 @@ The Dulwich client supports the following capabilities:
  * multi_ack
  * side-band-64k
  * ofs-delta
+ * quiet
  * report-status
  * delete-refs
 
@@ -62,10 +62,12 @@ from dulwich.errors import (
     )
 from dulwich.protocol import (
     _RBUFSIZE,
+    capability_agent,
     CAPABILITY_DELETE_REFS,
     CAPABILITY_MULTI_ACK,
     CAPABILITY_MULTI_ACK_DETAILED,
     CAPABILITY_OFS_DELTA,
+    CAPABILITY_QUIET,
     CAPABILITY_REPORT_STATUS,
     CAPABILITY_SIDE_BAND_64K,
     CAPABILITY_THIN_PACK,
@@ -94,6 +96,7 @@ def _fileno_can_read(fileno):
     """Check if a file descriptor is readable."""
     return len(select.select([fileno], [], [], 0)[0]) > 0
 
+
 COMMON_CAPABILITIES = [CAPABILITY_OFS_DELTA, CAPABILITY_SIDE_BAND_64K]
 FETCH_CAPABILITIES = ([CAPABILITY_THIN_PACK, CAPABILITY_MULTI_ACK,
                        CAPABILITY_MULTI_ACK_DETAILED] +
@@ -184,7 +187,7 @@ class GitClient(object):
 
     """
 
-    def __init__(self, thin_packs=True, report_activity=None):
+    def __init__(self, thin_packs=True, report_activity=None, quiet=False):
         """Create a new GitClient instance.
 
         :param thin_packs: Whether or not thin packs should be retrieved
@@ -194,7 +197,11 @@ class GitClient(object):
         self._report_activity = report_activity
         self._report_status_parser = None
         self._fetch_capabilities = set(FETCH_CAPABILITIES)
+        self._fetch_capabilities.add(capability_agent())
         self._send_capabilities = set(SEND_CAPABILITIES)
+        self._send_capabilities.add(capability_agent())
+        if quiet:
+            self._send_capabilities.add(CAPABILITY_QUIET)
         if not thin_packs:
             self._fetch_capabilities.remove(CAPABILITY_THIN_PACK)
 
@@ -597,12 +604,12 @@ class TraditionalGitClient(GitClient):
 class TCPGitClient(TraditionalGitClient):
     """A Git Client that works over TCP directly (i.e. git://)."""
 
-    def __init__(self, host, port=None, *args, **kwargs):
+    def __init__(self, host, port=None, **kwargs):
         if port is None:
             port = TCP_GIT_PORT
         self._host = host
         self._port = port
-        TraditionalGitClient.__init__(self, *args, **kwargs)
+        TraditionalGitClient.__init__(self, **kwargs)
 
     def _connect(self, cmd, path):
         if type(cmd) is not bytes:
@@ -690,13 +697,13 @@ def find_git_command():
 class SubprocessGitClient(TraditionalGitClient):
     """Git client that talks to a server using a subprocess."""
 
-    def __init__(self, *args, **kwargs):
+    def __init__(self, **kwargs):
         self._connection = None
         self._stderr = None
         self._stderr = kwargs.get('stderr')
         if 'stderr' in kwargs:
             del kwargs['stderr']
-        TraditionalGitClient.__init__(self, *args, **kwargs)
+        TraditionalGitClient.__init__(self, **kwargs)
 
     git_command = None
 
@@ -705,7 +712,6 @@ class SubprocessGitClient(TraditionalGitClient):
             raise TypeError(path)
         if type(path) is not bytes:
             raise TypeError(path)
-        import subprocess
         if self.git_command is None:
             git_command = find_git_command()
         argv = git_command + [service.decode('ascii'), path]
@@ -848,7 +854,6 @@ class SubprocessSSHVendor(SSHVendor):
             not all([isinstance(b, bytes) for b in command])):
             raise TypeError(command)
 
-        import subprocess
         #FIXME: This has no way to deal with passwords..
         args = ['ssh', '-x']
         if port is not None:
@@ -862,116 +867,13 @@ class SubprocessSSHVendor(SSHVendor):
         return SubprocessWrapper(proc)
 
 
-try:
-    import paramiko
-except ImportError:
-    pass
-else:
-    import threading
-
-    class ParamikoWrapper(object):
-        STDERR_READ_N = 2048  # 2k
-
-        def __init__(self, client, channel, progress_stderr=None):
-            self.client = client
-            self.channel = channel
-            self.progress_stderr = progress_stderr
-            self.should_monitor = bool(progress_stderr) or True
-            self.monitor_thread = None
-            self.stderr = ''
-
-            # Channel must block
-            self.channel.setblocking(True)
-
-            # Start
-            if self.should_monitor:
-                self.monitor_thread = threading.Thread(
-                    target=self.monitor_stderr)
-                self.monitor_thread.start()
-
-        def monitor_stderr(self):
-            while self.should_monitor:
-                # Block and read
-                data = self.read_stderr(self.STDERR_READ_N)
-
-                # Socket closed
-                if not data:
-                    self.should_monitor = False
-                    break
-
-                # Emit data
-                if self.progress_stderr:
-                    self.progress_stderr(data)
-
-                # Append to buffer
-                self.stderr += data
-
-        def stop_monitoring(self):
-            # Stop StdErr thread
-            if self.should_monitor:
-                self.should_monitor = False
-                self.monitor_thread.join()
-
-                # Get left over data
-                data = self.channel.in_stderr_buffer.empty()
-                self.stderr += data
-
-        def can_read(self):
-            return self.channel.recv_ready()
-
-        def write(self, data):
-            return self.channel.sendall(data)
-
-        def read_stderr(self, n):
-            return self.channel.recv_stderr(n)
-
-        def read(self, n=None):
-            data = self.channel.recv(n)
-            data_len = len(data)
-
-            # Closed socket
-            if not data:
-                return
-
-            # Read more if needed
-            if n and data_len < n:
-                diff_len = n - data_len
-                return data + self.read(diff_len)
-            return data
-
-        def close(self):
-            self.channel.close()
-            self.stop_monitoring()
-
-    class ParamikoSSHVendor(object):
-
-        def __init__(self):
-            self.ssh_kwargs = {}
-
-        def run_command(self, host, command, username=None, port=None,
-                        progress_stderr=None):
-            if (type(command) is not list or
-                not all([isinstance(b, bytes) for b in command])):
-                raise TypeError(command)
-            # Paramiko needs an explicit port. None is not valid
-            if port is None:
-                port = 22
-
-            client = paramiko.SSHClient()
-
-            policy = paramiko.client.MissingHostKeyPolicy()
-            client.set_missing_host_key_policy(policy)
-            client.connect(host, username=username, port=port,
-                           **self.ssh_kwargs)
-
-            # Open SSH session
-            channel = client.get_transport().open_session()
-
-            # Run commands
-            channel.exec_command(subprocess.list2cmdline(command))
-
-            return ParamikoWrapper(
-                client, channel, progress_stderr=progress_stderr)
+def ParamikoSSHVendor(**kwargs):
+    import warnings
+    warnings.warn(
+        "ParamikoSSHVendor has been moved to dulwich.contrib.paramiko_vendor.",
+        DeprecationWarning)
+    from dulwich.contrib.paramiko_vendor import ParamikoSSHVendor
+    return ParamikoSSHVendor(**kwargs)
 
 
 # Can be overridden by users
@@ -980,12 +882,16 @@ get_ssh_vendor = SubprocessSSHVendor
 
 class SSHGitClient(TraditionalGitClient):
 
-    def __init__(self, host, port=None, username=None, *args, **kwargs):
+    def __init__(self, host, port=None, username=None, vendor=None, **kwargs):
         self.host = host
         self.port = port
         self.username = username
-        TraditionalGitClient.__init__(self, *args, **kwargs)
+        TraditionalGitClient.__init__(self, **kwargs)
         self.alternative_paths = {}
+        if vendor is not None:
+            self.ssh_vendor = vendor
+        else:
+            self.ssh_vendor = get_ssh_vendor()
 
     def _get_cmd_path(self, cmd):
         cmd = self.alternative_paths.get(cmd, b'git-' + cmd)
@@ -1004,7 +910,7 @@ class SSHGitClient(TraditionalGitClient):
         if path.startswith(b"/~"):
             path = path[1:]
         argv = self._get_cmd_path(cmd) + [path]
-        con = get_ssh_vendor().run_command(
+        con = self.ssh_vendor.run_command(
             self.host, argv, port=self.port, username=self.username)
         return (Protocol(con.read, con.write, con.close,
                          report_activity=self._report_activity),
@@ -1036,15 +942,14 @@ def default_urllib2_opener(config):
 
 class HttpGitClient(GitClient):
 
-    def __init__(self, base_url, dumb=None, opener=None, config=None, *args,
-                 **kwargs):
+    def __init__(self, base_url, dumb=None, opener=None, config=None, **kwargs):
         self.base_url = base_url.rstrip("/") + "/"
         self.dumb = dumb
         if opener is None:
             self.opener = default_urllib2_opener(config)
         else:
             self.opener = opener
-        GitClient.__init__(self, *args, **kwargs)
+        GitClient.__init__(self, **kwargs)
 
     def __repr__(self):
         return "%s(%r, dumb=%r)" % (type(self).__name__, self.base_url, self.dumb)

+ 183 - 0
dulwich/contrib/paramiko_vendor.py

@@ -0,0 +1,183 @@
+# paramiko_vendor.py -- paramiko implementation of the SSHVendor interface
+# Copyright (C) 2013 Aaron O'Mullan <aaron.omullan@friendco.de>
+#
+# 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
+# or (at your option) a later version of the License.
+#
+# 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., 51 Franklin Street, Fifth Floor, Boston,
+# MA  02110-1301, USA.
+
+"""Paramiko SSH support for Dulwich.
+
+To use this implementation as the SSH implementation in Dulwich, override
+the dulwich.client.get_ssh_vendor attribute:
+
+  >>> from dulwich import client as _mod_client
+  >>> from dulwich.contrib.paramiko_vendor import ParamikoSSHVendor
+  >>> _mod_client.get_ssh_vendor = ParamikoSSHVendor
+
+This implementation is experimental and does not have any tests.
+"""
+
+import paramiko
+import paramiko.client
+import subprocess
+import threading
+
+class _ParamikoWrapper(object):
+    STDERR_READ_N = 2048  # 2k
+
+    def __init__(self, client, channel, progress_stderr=None):
+        self.client = client
+        self.channel = channel
+        self.progress_stderr = progress_stderr
+        self.should_monitor = bool(progress_stderr) or True
+        self.monitor_thread = None
+        self.stderr = b''
+
+        # Channel must block
+        self.channel.setblocking(True)
+
+        # Start
+        if self.should_monitor:
+            self.monitor_thread = threading.Thread(
+                target=self.monitor_stderr)
+            self.monitor_thread.start()
+
+    def monitor_stderr(self):
+        while self.should_monitor:
+            # Block and read
+            data = self.read_stderr(self.STDERR_READ_N)
+
+            # Socket closed
+            if not data:
+                self.should_monitor = False
+                break
+
+            # Emit data
+            if self.progress_stderr:
+                self.progress_stderr(data)
+
+            # Append to buffer
+            self.stderr += data
+
+    def stop_monitoring(self):
+        # Stop StdErr thread
+        if self.should_monitor:
+            self.should_monitor = False
+            self.monitor_thread.join()
+
+            # Get left over data
+            data = self.channel.in_stderr_buffer.empty()
+            self.stderr += data
+
+    def can_read(self):
+        return self.channel.recv_ready()
+
+    def write(self, data):
+        return self.channel.sendall(data)
+
+    def read_stderr(self, n):
+        return self.channel.recv_stderr(n)
+
+    def read(self, n=None):
+        data = self.channel.recv(n)
+        data_len = len(data)
+
+        # Closed socket
+        if not data:
+            return
+
+        # Read more if needed
+        if n and data_len < n:
+            diff_len = n - data_len
+            return data + self.read(diff_len)
+        return data
+
+    def close(self):
+        self.channel.close()
+        self.stop_monitoring()
+
+
+# {{{ shell quoting
+
+# Adapted from
+# https://github.com/python/cpython/blob/8cd133c63f156451eb3388b9308734f699f4f1af/Lib/shlex.py#L278
+
+def is_shell_safe(s):
+    import re
+    import sys
+
+    flags = 0
+    if sys.version_info >= (3,):
+        flags = re.ASCII
+
+    unsafe_re = re.compile(br'[^\w@%+=:,./-]', flags)
+
+    return unsafe_re.search(s) is None
+
+
+def shell_quote(s):
+    """Return a shell-escaped version of the byte string *s*."""
+
+    # Unconditionally quotes because that's apparently git's behavior, too,
+    # and some code hosting sites (notably Bitbucket) appear to rely on that.
+
+    if not s:
+        return b"''"
+
+    # use single quotes, and put single quotes into double quotes
+    # the string $'b is then quoted as '$'"'"'b'
+    return b"'" + s.replace(b"'", b"'\"'\"'") + b"'"
+
+# }}}
+
+
+class ParamikoSSHVendor(object):
+
+    def __init__(self):
+        self.ssh_kwargs = {}
+
+    def run_command(self, host, command, username=None, port=None,
+                    progress_stderr=None):
+        if (type(command) is not list or
+            not all([isinstance(b, bytes) for b in command])):
+            raise TypeError(command)
+        # Paramiko needs an explicit port. None is not valid
+        if port is None:
+            port = 22
+
+        client = paramiko.SSHClient()
+
+        policy = paramiko.client.MissingHostKeyPolicy()
+        client.set_missing_host_key_policy(policy)
+        client.connect(host, username=username, port=port,
+                       **self.ssh_kwargs)
+
+        # Open SSH session
+        channel = client.get_transport().open_session()
+
+        # Quote command
+        assert command
+        assert is_shell_safe(command[0])
+
+        quoted_command = (
+            command[0]
+            + b' '
+            + b' '.join(
+                shell_quote(c) for c in command[1:]))
+
+        # Run commands
+        channel.exec_command(quoted_command)
+
+        return _ParamikoWrapper(
+            client, channel, progress_stderr=progress_stderr)

+ 11 - 0
dulwich/protocol.py

@@ -25,6 +25,7 @@ from os import (
     )
 import socket
 
+import dulwich
 from dulwich.errors import (
     HangupException,
     GitProtocolError,
@@ -57,6 +58,16 @@ CAPABILITY_REPORT_STATUS = b'report-status'
 CAPABILITY_SHALLOW = b'shallow'
 CAPABILITY_SIDE_BAND_64K = b'side-band-64k'
 CAPABILITY_THIN_PACK = b'thin-pack'
+CAPABILITY_AGENT = b'agent'
+
+
+def agent_string():
+    return ('dulwich/%d.%d.%d' % dulwich.__version__).encode('ascii')
+
+
+def capability_agent():
+    return CAPABILITY_AGENT + b'=' + agent_string()
+
 
 COMMAND_DEEPEN = b'deepen'
 COMMAND_SHALLOW = b'shallow'

+ 5 - 2
dulwich/server.py

@@ -68,6 +68,7 @@ from dulwich.pack import (
     )
 from dulwich.protocol import (
     BufferedPktLineWriter,
+    capability_agent,
     CAPABILITY_DELETE_REFS,
     CAPABILITY_INCLUDE_TAG,
     CAPABILITY_MULTI_ACK_DETAILED,
@@ -75,6 +76,7 @@ from dulwich.protocol import (
     CAPABILITY_NO_DONE,
     CAPABILITY_NO_PROGRESS,
     CAPABILITY_OFS_DELTA,
+    CAPABILITY_QUIET,
     CAPABILITY_REPORT_STATUS,
     CAPABILITY_SHALLOW,
     CAPABILITY_SIDE_BAND_64K,
@@ -220,7 +222,8 @@ class Handler(object):
     @classmethod
     def innocuous_capabilities(cls):
         return (CAPABILITY_INCLUDE_TAG, CAPABILITY_THIN_PACK,
-                CAPABILITY_NO_PROGRESS, CAPABILITY_OFS_DELTA)
+                CAPABILITY_NO_PROGRESS, CAPABILITY_OFS_DELTA,
+                capability_agent())
 
     @classmethod
     def required_capabilities(cls):
@@ -837,7 +840,7 @@ class ReceivePackHandler(Handler):
 
     @classmethod
     def capabilities(cls):
-        return (CAPABILITY_REPORT_STATUS, CAPABILITY_DELETE_REFS,
+        return (CAPABILITY_REPORT_STATUS, CAPABILITY_DELETE_REFS, CAPABILITY_QUIET,
                 CAPABILITY_OFS_DELTA, CAPABILITY_SIDE_BAND_64K, CAPABILITY_NO_DONE)
 
     def _apply_pack(self, refs):

+ 6 - 2
dulwich/tests/test_client.py

@@ -23,6 +23,7 @@ import shutil
 import tempfile
 
 
+import dulwich
 from dulwich import (
     client,
     )
@@ -87,10 +88,13 @@ class GitClientTests(TestCase):
                                   self.rout.write)
 
     def test_caps(self):
+        agent_cap = ('agent=dulwich/%d.%d.%d' % dulwich.__version__).encode('ascii')
         self.assertEqual(set([b'multi_ack', b'side-band-64k', b'ofs-delta',
-                               b'thin-pack', b'multi_ack_detailed']),
+                               b'thin-pack', b'multi_ack_detailed',
+                               agent_cap]),
                           set(self.client._fetch_capabilities))
-        self.assertEqual(set([b'ofs-delta', b'report-status', b'side-band-64k']),
+        self.assertEqual(set([b'ofs-delta', b'report-status', b'side-band-64k',
+                              agent_cap]),
                           set(self.client._send_capabilities))
 
     def test_archive_ack(self):

+ 2 - 2
dulwich/tests/test_porcelain.py

@@ -652,8 +652,8 @@ class ReceivePackTests(PorcelainTestCase):
         exitcode = porcelain.receive_pack(self.repo.path, BytesIO(b"0000"), outf)
         outlines = outf.getvalue().splitlines()
         self.assertEqual([
-            b'006d9e65bdcf4a22cdd4f3700604a275cd2aaf146b23 HEAD\x00 report-status '
-            b'delete-refs ofs-delta side-band-64k no-done',
+            b'00739e65bdcf4a22cdd4f3700604a275cd2aaf146b23 HEAD\x00 report-status '
+            b'delete-refs quiet ofs-delta side-band-64k no-done',
             b'003f9e65bdcf4a22cdd4f3700604a275cd2aaf146b23 refs/heads/master',
             b'0000'], outlines)
         self.assertEqual(0, exitcode)

+ 1 - 1
setup.py

@@ -8,7 +8,7 @@ except ImportError:
     from distutils.core import setup, Extension
 from distutils.core import Distribution
 
-dulwich_version_string = '0.11.1'
+dulwich_version_string = '0.11.2'
 
 include_dirs = []
 # Windows MSVC support