Remove authors of trivial contributions or who have given permission to relicense...
[jelmer/dulwich.git] / dulwich / client.py
index 46fe9a59ac9eed77211b0b75893b71003bdf0c60..fb15f3fea9858ab9261a22b6a467f2a754338cb3 100644 (file)
@@ -38,13 +38,21 @@ Known capabilities that are not supported:
 
 __docformat__ = 'restructuredText'
 
-from io import BytesIO
+from contextlib import closing
+from io import BytesIO, BufferedReader
 import dulwich
 import select
+import shlex
 import socket
 import subprocess
-import urllib2
-import urlparse
+import sys
+
+try:
+    import urllib2
+    import urlparse
+except ImportError:
+    import urllib.request as urllib2
+    import urllib.parse as urlparse
 
 from dulwich.errors import (
     GitProtocolError,
@@ -54,6 +62,19 @@ from dulwich.errors import (
     )
 from dulwich.protocol import (
     _RBUFSIZE,
+    CAPABILITY_DELETE_REFS,
+    CAPABILITY_MULTI_ACK,
+    CAPABILITY_MULTI_ACK_DETAILED,
+    CAPABILITY_OFS_DELTA,
+    CAPABILITY_REPORT_STATUS,
+    CAPABILITY_SIDE_BAND_64K,
+    CAPABILITY_THIN_PACK,
+    COMMAND_DONE,
+    COMMAND_HAVE,
+    COMMAND_WANT,
+    SIDE_BAND_CHANNEL_DATA,
+    SIDE_BAND_CHANNEL_PROGRESS,
+    SIDE_BAND_CHANNEL_FATAL,
     PktLineParser,
     Protocol,
     ProtocolFile,
@@ -73,10 +94,11 @@ def _fileno_can_read(fileno):
     """Check if a file descriptor is readable."""
     return len(select.select([fileno], [], [], 0)[0]) > 0
 
-COMMON_CAPABILITIES = ['ofs-delta', 'side-band-64k']
-FETCH_CAPABILITIES = (['thin-pack', 'multi_ack', 'multi_ack_detailed'] +
+COMMON_CAPABILITIES = [CAPABILITY_OFS_DELTA, CAPABILITY_SIDE_BAND_64K]
+FETCH_CAPABILITIES = ([CAPABILITY_THIN_PACK, CAPABILITY_MULTI_ACK,
+                       CAPABILITY_MULTI_ACK_DETAILED] +
                       COMMON_CAPABILITIES)
-SEND_CAPABILITIES = ['report-status'] + COMMON_CAPABILITIES
+SEND_CAPABILITIES = [CAPABILITY_REPORT_STATUS] + COMMON_CAPABILITIES
 
 
 class ReportStatusParser(object):
@@ -95,27 +117,27 @@ class ReportStatusParser(object):
         :raise SendPackError: Raised when the server could not unpack
         :raise UpdateRefsError: Raised when refs could not be updated
         """
-        if self._pack_status not in ('unpack ok', None):
+        if self._pack_status not in (b'unpack ok', None):
             raise SendPackError(self._pack_status)
         if not self._ref_status_ok:
             ref_status = {}
             ok = set()
             for status in self._ref_statuses:
-                if ' ' not in status:
+                if b' ' not in status:
                     # malformed response, move on to the next one
                     continue
-                status, ref = status.split(' ', 1)
+                status, ref = status.split(b' ', 1)
 
-                if status == 'ng':
-                    if ' ' in ref:
-                        ref, status = ref.split(' ', 1)
+                if status == b'ng':
+                    if b' ' in ref:
+                        ref, status = ref.split(b' ', 1)
                 else:
                     ok.add(ref)
                 ref_status[ref] = status
-            raise UpdateRefsError('%s failed to update' %
-                                  ', '.join([ref for ref in ref_status
-                                             if ref not in ok]),
-                                  ref_status=ref_status)
+            # TODO(jelmer): don't assume encoding of refs is ascii.
+            raise UpdateRefsError(', '.join([
+                ref.decode('ascii') for ref in ref_status if ref not in ok]) +
+                ' failed to update', ref_status=ref_status)
 
     def handle_packet(self, pkt):
         """Handle a packet.
@@ -133,7 +155,7 @@ class ReportStatusParser(object):
         else:
             ref_status = pkt.strip()
             self._ref_statuses.append(ref_status)
-            if not ref_status.startswith('ok '):
+            if not ref_status.startswith(b'ok '):
                 self._ref_status_ok = False
 
 
@@ -142,8 +164,8 @@ def read_pkt_refs(proto):
     refs = {}
     # Receive refs from server
     for pkt in proto.read_pkt_seq():
-        (sha, ref) = pkt.rstrip('\n').split(None, 1)
-        if sha == 'ERR':
+        (sha, ref) = pkt.rstrip(b'\n').split(None, 1)
+        if sha == b'ERR':
             raise GitProtocolError(ref)
         if server_capabilities is None:
             (ref, server_capabilities) = extract_capabilities(ref)
@@ -174,16 +196,18 @@ class GitClient(object):
         self._fetch_capabilities = set(FETCH_CAPABILITIES)
         self._send_capabilities = set(SEND_CAPABILITIES)
         if not thin_packs:
-            self._fetch_capabilities.remove('thin-pack')
+            self._fetch_capabilities.remove(CAPABILITY_THIN_PACK)
 
     def send_pack(self, path, determine_wants, generate_pack_contents,
-                  progress=None):
+                  progress=None, write_pack=write_pack_objects):
         """Upload a pack to a remote repository.
 
         :param path: Repository path
         :param generate_pack_contents: Function that can return a sequence of
             the shas of the objects to upload.
         :param progress: Optional progress function
+        :param write_pack: Function called with (file, iterable of objects) to
+            write the objects returned by generate_pack_contents to the server.
 
         :raises SendPackError: if server rejects the pack data
         :raises UpdateRefsError: if the server supports report-status
@@ -203,6 +227,7 @@ class GitClient(object):
         """
         if determine_wants is None:
             determine_wants = target.object_store.determine_wants_all
+
         f, commit, abort = target.object_store.add_pack()
         try:
             result = self.fetch_pack(
@@ -226,9 +251,16 @@ class GitClient(object):
         """
         raise NotImplementedError(self.fetch_pack)
 
+    def get_refs(self, path):
+        """Retrieve the current refs from a git smart server.
+
+        :param path: Path to the repo to fetch from.
+        """
+        raise NotImplementedError(self.get_refs)
+
     def _parse_status_report(self, proto):
         unpack = proto.read_pkt_line().strip()
-        if unpack != 'unpack ok':
+        if unpack != b'unpack ok':
             st = True
             # flush remaining error data
             while st is not None:
@@ -240,7 +272,7 @@ class GitClient(object):
         while ref_status:
             ref_status = ref_status.strip()
             statuses.append(ref_status)
-            if not ref_status.startswith('ok '):
+            if not ref_status.startswith(b'ok '):
                 errs = True
             ref_status = proto.read_pkt_line()
 
@@ -248,20 +280,20 @@ class GitClient(object):
             ref_status = {}
             ok = set()
             for status in statuses:
-                if ' ' not in status:
+                if b' ' not in status:
                     # malformed response, move on to the next one
                     continue
-                status, ref = status.split(' ', 1)
+                status, ref = status.split(b' ', 1)
 
-                if status == 'ng':
-                    if ' ' in ref:
-                        ref, status = ref.split(' ', 1)
+                if status == b'ng':
+                    if b' ' in ref:
+                        ref, status = ref.split(b' ', 1)
                 else:
                     ok.add(ref)
                 ref_status[ref] = status
-            raise UpdateRefsError('%s failed to update' %
-                                  ', '.join([ref for ref in ref_status
-                                             if ref not in ok]),
+            raise UpdateRefsError(', '.join([ref for ref in ref_status
+                                             if ref not in ok]) +
+                                             b' failed to update',
                                   ref_status=ref_status)
 
     def _read_side_band64k_data(self, proto, channel_callbacks):
@@ -274,7 +306,7 @@ class GitClient(object):
             handlers to use. None for a callback discards channel data.
         """
         for pkt in proto.read_pkt_seq():
-            channel = ord(pkt[0])
+            channel = ord(pkt[:1])
             pkt = pkt[1:]
             try:
                 cb = channel_callbacks[channel]
@@ -298,18 +330,18 @@ class GitClient(object):
         have = [x for x in old_refs.values() if not x == ZERO_SHA]
         sent_capabilities = False
 
-        for refname in set(new_refs.keys() + old_refs.keys()):
+        all_refs = set(new_refs.keys()).union(set(old_refs.keys()))
+        for refname in all_refs:
             old_sha1 = old_refs.get(refname, ZERO_SHA)
             new_sha1 = new_refs.get(refname, ZERO_SHA)
 
             if old_sha1 != new_sha1:
                 if sent_capabilities:
-                    proto.write_pkt_line('%s %s %s' % (
-                        old_sha1, new_sha1, refname))
+                    proto.write_pkt_line(old_sha1 + b' ' + new_sha1 + b' ' + refname)
                 else:
                     proto.write_pkt_line(
-                        '%s %s %s\0%s' % (old_sha1, new_sha1, refname,
-                                          ' '.join(capabilities)))
+                        old_sha1 + b' ' + new_sha1 + b' ' + refname + b'\0' +
+                        b' '.join(capabilities))
                     sent_capabilities = True
             if new_sha1 not in have and new_sha1 != ZERO_SHA:
                 want.append(new_sha1)
@@ -323,16 +355,16 @@ class GitClient(object):
         :param capabilities: List of negotiated capabilities
         :param progress: Optional progress reporting function
         """
-        if "side-band-64k" in capabilities:
+        if b"side-band-64k" in capabilities:
             if progress is None:
                 progress = lambda x: None
             channel_callbacks = {2: progress}
-            if 'report-status' in capabilities:
+            if CAPABILITY_REPORT_STATUS in capabilities:
                 channel_callbacks[1] = PktLineParser(
                     self._report_status_parser.handle_packet).parse
             self._read_side_band64k_data(proto, channel_callbacks)
         else:
-            if 'report-status' in capabilities:
+            if CAPABILITY_REPORT_STATUS in capabilities:
                 for pkt in proto.read_pkt_seq():
                     self._report_status_parser.handle_packet(pkt)
         if self._report_status_parser is not None:
@@ -349,30 +381,29 @@ class GitClient(object):
         :param can_read: function that returns a boolean that indicates
             whether there is extra graph data to read on proto
         """
-        assert isinstance(wants, list) and isinstance(wants[0], str)
-        proto.write_pkt_line('want %s %s\n' % (
-            wants[0], ' '.join(capabilities)))
+        assert isinstance(wants, list) and isinstance(wants[0], bytes)
+        proto.write_pkt_line(COMMAND_WANT + b' ' + wants[0] + b' ' + b' '.join(capabilities) + b'\n')
         for want in wants[1:]:
-            proto.write_pkt_line('want %s\n' % want)
+            proto.write_pkt_line(COMMAND_WANT + b' ' + want + b'\n')
         proto.write_pkt_line(None)
         have = next(graph_walker)
         while have:
-            proto.write_pkt_line('have %s\n' % have)
+            proto.write_pkt_line(COMMAND_HAVE + b' ' + have + b'\n')
             if can_read():
                 pkt = proto.read_pkt_line()
-                parts = pkt.rstrip('\n').split(' ')
-                if parts[0] == 'ACK':
+                parts = pkt.rstrip(b'\n').split(b' ')
+                if parts[0] == b'ACK':
                     graph_walker.ack(parts[1])
-                    if parts[2] in ('continue', 'common'):
+                    if parts[2] in (b'continue', b'common'):
                         pass
-                    elif parts[2] == 'ready':
+                    elif parts[2] == b'ready':
                         break
                     else:
                         raise AssertionError(
                             "%s not in ('continue', 'ready', 'common)" %
                             parts[2])
             have = next(graph_walker)
-        proto.write_pkt_line('done\n')
+        proto.write_pkt_line(COMMAND_DONE + b'\n')
 
     def _handle_upload_pack_tail(self, proto, capabilities, graph_walker,
                                  pack_data, progress=None, rbufsize=_RBUFSIZE):
@@ -387,22 +418,25 @@ class GitClient(object):
         """
         pkt = proto.read_pkt_line()
         while pkt:
-            parts = pkt.rstrip('\n').split(' ')
-            if parts[0] == 'ACK':
-                graph_walker.ack(pkt.split(' ')[1])
+            parts = pkt.rstrip(b'\n').split(b' ')
+            if parts[0] == b'ACK':
+                graph_walker.ack(parts[1])
             if len(parts) < 3 or parts[2] not in (
-                    'ready', 'continue', 'common'):
+                    b'ready', b'continue', b'common'):
                 break
             pkt = proto.read_pkt_line()
-        if "side-band-64k" in capabilities:
+        if CAPABILITY_SIDE_BAND_64K in capabilities:
             if progress is None:
                 # Just ignore progress data
                 progress = lambda x: None
-            self._read_side_band64k_data(proto, {1: pack_data, 2: progress})
+            self._read_side_band64k_data(proto, {
+                SIDE_BAND_CHANNEL_DATA: pack_data,
+                SIDE_BAND_CHANNEL_PROGRESS: progress}
+            )
         else:
             while True:
                 data = proto.read(rbufsize)
-                if data == "":
+                if data == b"":
                     break
                 pack_data(data)
 
@@ -425,24 +459,26 @@ class TraditionalGitClient(GitClient):
         raise NotImplementedError()
 
     def send_pack(self, path, determine_wants, generate_pack_contents,
-                  progress=None):
+                  progress=None, write_pack=write_pack_objects):
         """Upload a pack to a remote repository.
 
         :param path: Repository path
         :param generate_pack_contents: Function that can return a sequence of
             the shas of the objects to upload.
         :param progress: Optional callback called with progress updates
+        :param write_pack: Function called with (file, iterable of objects) to
+            write the objects returned by generate_pack_contents to the server.
 
         :raises SendPackError: if server rejects the pack data
         :raises UpdateRefsError: if the server supports report-status
                                  and rejects ref updates
         """
-        proto, unused_can_read = self._connect('receive-pack', path)
+        proto, unused_can_read = self._connect(b'receive-pack', path)
         with proto:
             old_refs, server_capabilities = read_pkt_refs(proto)
             negotiated_capabilities = self._send_capabilities & server_capabilities
 
-            if 'report-status' in negotiated_capabilities:
+            if CAPABILITY_REPORT_STATUS in negotiated_capabilities:
                 self._report_status_parser = ReportStatusParser()
             report_status_parser = self._report_status_parser
 
@@ -452,23 +488,16 @@ class TraditionalGitClient(GitClient):
                 proto.write_pkt_line(None)
                 raise
 
-            if not 'delete-refs' in server_capabilities:
+            if not CAPABILITY_DELETE_REFS in server_capabilities:
                 # Server does not support deletions. Fail later.
-                def remove_del(pair):
-                    if pair[1] == ZERO_SHA:
-                        if 'report-status' in negotiated_capabilities:
+                new_refs = dict(orig_new_refs)
+                for ref, sha in orig_new_refs.items():
+                    if sha == ZERO_SHA:
+                        if CAPABILITY_REPORT_STATUS in negotiated_capabilities:
                             report_status_parser._ref_statuses.append(
-                                'ng %s remote does not support deleting refs'
-                                % pair[1])
+                                b'ng ' + sha + b' remote does not support deleting refs')
                             report_status_parser._ref_status_ok = False
-                        return False
-                    else:
-                        return True
-
-                new_refs = dict(
-                    filter(
-                        remove_del,
-                        [(ref, sha) for ref, sha in new_refs.iteritems()]))
+                        del new_refs[ref]
 
             if new_refs is None:
                 proto.write_pkt_line(None)
@@ -477,8 +506,8 @@ class TraditionalGitClient(GitClient):
             if len(new_refs) == 0 and len(orig_new_refs):
                 # NOOP - Original new refs filtered out by policy
                 proto.write_pkt_line(None)
-                if self._report_status_parser is not None:
-                    self._report_status_parser.check()
+                if report_status_parser is not None:
+                    report_status_parser.check()
                 return old_refs
 
             (have, want) = self._handle_receive_pack_head(
@@ -486,16 +515,13 @@ class TraditionalGitClient(GitClient):
             if not want and old_refs == new_refs:
                 return new_refs
             objects = generate_pack_contents(have, want)
-            if len(objects) > 0:
-                entries, sha = write_pack_objects(proto.write_file(), objects)
-            elif len(set(new_refs.values()) - set([ZERO_SHA])) > 0:
-                # Check for valid create/update refs
-                filtered_new_refs = \
-                    dict([(ref, sha) for ref, sha in new_refs.iteritems()
-                         if sha != ZERO_SHA])
-                if len(set(filtered_new_refs.iteritems()) -
-                        set(old_refs.iteritems())) > 0:
-                    entries, sha = write_pack_objects(proto.write_file(), objects)
+
+            dowrite = len(objects) > 0
+            dowrite = dowrite or any(old_refs.get(ref) != sha
+                                     for (ref, sha) in new_refs.items()
+                                     if sha != ZERO_SHA)
+            if dowrite:
+                write_pack(proto.write_file(), objects)
 
             self._handle_receive_pack_tail(
                 proto, negotiated_capabilities, progress)
@@ -510,7 +536,7 @@ class TraditionalGitClient(GitClient):
         :param pack_data: Callback called for each bit of data in the pack
         :param progress: Callback for progress reports (strings)
         """
-        proto, can_read = self._connect('upload-pack', path)
+        proto, can_read = self._connect(b'upload-pack', path)
         with proto:
             refs, server_capabilities = read_pkt_refs(proto)
             negotiated_capabilities = (
@@ -536,24 +562,36 @@ class TraditionalGitClient(GitClient):
                 proto, negotiated_capabilities, graph_walker, pack_data, progress)
             return refs
 
-    def archive(self, path, committish, write_data, progress=None):
+    def get_refs(self, path):
+        """Retrieve the current refs from a git smart server."""
+        # stock `git ls-remote` uses upload-pack
+        proto, _ = self._connect(b'upload-pack', path)
+        with proto:
+            refs, _ = read_pkt_refs(proto)
+            return refs
+
+    def archive(self, path, committish, write_data, progress=None,
+                write_error=None):
         proto, can_read = self._connect(b'upload-archive', path)
         with proto:
-            proto.write_pkt_line("argument %s" % committish)
+            proto.write_pkt_line(b"argument " + committish)
             proto.write_pkt_line(None)
             pkt = proto.read_pkt_line()
-            if pkt == "NACK\n":
+            if pkt == b"NACK\n":
                 return
-            elif pkt == "ACK\n":
+            elif pkt == b"ACK\n":
                 pass
-            elif pkt.startswith("ERR "):
-                raise GitProtocolError(pkt[4:].rstrip("\n"))
+            elif pkt.startswith(b"ERR "):
+                raise GitProtocolError(pkt[4:].rstrip(b"\n"))
             else:
                 raise AssertionError("invalid response %r" % pkt)
             ret = proto.read_pkt_line()
             if ret is not None:
                 raise AssertionError("expected pkt tail")
-            self._read_side_band64k_data(proto, {1: write_data, 2: progress})
+            self._read_side_band64k_data(proto, {
+                SIDE_BAND_CHANNEL_DATA: write_data,
+                SIDE_BAND_CHANNEL_PROGRESS: progress,
+                SIDE_BAND_CHANNEL_FATAL: write_error})
 
 
 class TCPGitClient(TraditionalGitClient):
@@ -567,6 +605,10 @@ class TCPGitClient(TraditionalGitClient):
         TraditionalGitClient.__init__(self, *args, **kwargs)
 
     def _connect(self, cmd, path):
+        if type(cmd) is not bytes:
+            raise TypeError(path)
+        if type(path) is not bytes:
+            raise TypeError(path)
         sockaddrs = socket.getaddrinfo(
             self._host, self._port, socket.AF_UNSPEC, socket.SOCK_STREAM)
         s = None
@@ -594,9 +636,10 @@ class TCPGitClient(TraditionalGitClient):
 
         proto = Protocol(rfile.read, wfile.write, close,
                          report_activity=self._report_activity)
-        if path.startswith("/~"):
+        if path.startswith(b"/~"):
             path = path[1:]
-        proto.send_cmd('git-%s' % cmd, path, 'host=%s' % self._host)
+        # TODO(jelmer): Alternative to ascii?
+        proto.send_cmd(b'git-' + cmd, path, b'host=' + self._host.encode('ascii'))
         return proto, lambda: _fileno_can_read(s)
 
 
@@ -605,7 +648,10 @@ class SubprocessWrapper(object):
 
     def __init__(self, proc):
         self.proc = proc
-        self.read = proc.stdout.read
+        if sys.version_info[0] == 2:
+            self.read = proc.stdout.read
+        else:
+            self.read = BufferedReader(proc.stdout).read
         self.write = proc.stdin.write
 
     def can_read(self):
@@ -613,7 +659,8 @@ class SubprocessWrapper(object):
             from msvcrt import get_osfhandle
             from win32pipe import PeekNamedPipe
             handle = get_osfhandle(self.proc.stdout.fileno())
-            return PeekNamedPipe(handle, 0)[2] != 0
+            data, total_bytes_avail, msg_bytes_left = PeekNamedPipe(handle, 0)
+            return total_bytes_avail != 0
         else:
             return _fileno_can_read(self.proc.stdout.fileno())
 
@@ -625,6 +672,21 @@ class SubprocessWrapper(object):
         self.proc.wait()
 
 
+def find_git_command():
+    """Find command to run for system Git (usually C Git).
+    """
+    if sys.platform == 'win32': # support .exe, .bat and .cmd
+        try: # to avoid overhead
+            import win32api
+        except ImportError: # run through cmd.exe with some overhead
+            return ['cmd', '/c', 'git']
+        else:
+            status, git = win32api.FindExecutable('git')
+            return [git]
+    else:
+        return ['git']
+
+
 class SubprocessGitClient(TraditionalGitClient):
     """Git client that talks to a server using a subprocess."""
 
@@ -636,9 +698,17 @@ class SubprocessGitClient(TraditionalGitClient):
             del kwargs['stderr']
         TraditionalGitClient.__init__(self, *args, **kwargs)
 
+    git_command = None
+
     def _connect(self, service, path):
+        if type(service) is not bytes:
+            raise TypeError(path)
+        if type(path) is not bytes:
+            raise TypeError(path)
         import subprocess
-        argv = ['git', service, path]
+        if self.git_command is None:
+            git_command = find_git_command()
+        argv = git_command + [service.decode('ascii'), path]
         p = SubprocessWrapper(
             subprocess.Popen(argv, bufsize=0, stdin=subprocess.PIPE,
                              stdout=subprocess.PIPE,
@@ -662,19 +732,44 @@ class LocalGitClient(GitClient):
         # Ignore the thin_packs argument
 
     def send_pack(self, path, determine_wants, generate_pack_contents,
-                  progress=None):
+                  progress=None, write_pack=write_pack_objects):
         """Upload a pack to a remote repository.
 
         :param path: Repository path
         :param generate_pack_contents: Function that can return a sequence of
             the shas of the objects to upload.
         :param progress: Optional progress function
+        :param write_pack: Function called with (file, iterable of objects) to
+            write the objects returned by generate_pack_contents to the server.
 
         :raises SendPackError: if server rejects the pack data
         :raises UpdateRefsError: if the server supports report-status
                                  and rejects ref updates
         """
-        raise NotImplementedError(self.send_pack)
+        from dulwich.repo import Repo
+
+        with closing(Repo(path)) as target:
+            old_refs = target.get_refs()
+            new_refs = determine_wants(dict(old_refs))
+
+            have = [sha1 for sha1 in old_refs.values() if sha1 != ZERO_SHA]
+            want = []
+            all_refs = set(new_refs.keys()).union(set(old_refs.keys()))
+            for refname in all_refs:
+                old_sha1 = old_refs.get(refname, ZERO_SHA)
+                new_sha1 = new_refs.get(refname, ZERO_SHA)
+                if new_sha1 not in have and new_sha1 != ZERO_SHA:
+                    want.append(new_sha1)
+
+            if not want and old_refs == new_refs:
+                return new_refs
+
+            target.object_store.add_objects(generate_pack_contents(have, want))
+
+            for name, sha in new_refs.items():
+                target.refs[name] = sha
+
+        return new_refs
 
     def fetch(self, path, target, determine_wants=None, progress=None):
         """Fetch into a target repository.
@@ -687,9 +782,9 @@ class LocalGitClient(GitClient):
         :return: remote refs as dictionary
         """
         from dulwich.repo import Repo
-        r = Repo(path)
-        return r.fetch(target, determine_wants=determine_wants,
-                       progress=progress)
+        with closing(Repo(path)) as r:
+            return r.fetch(target, determine_wants=determine_wants,
+                           progress=progress)
 
     def fetch_pack(self, path, determine_wants, graph_walker, pack_data,
                    progress=None):
@@ -701,18 +796,25 @@ class LocalGitClient(GitClient):
         :param progress: Callback for progress reports (strings)
         """
         from dulwich.repo import Repo
-        r = Repo(path)
-        objects_iter = r.fetch_objects(determine_wants, graph_walker, progress)
+        with closing(Repo(path)) as r:
+            objects_iter = r.fetch_objects(determine_wants, graph_walker, progress)
 
-        # Did the process short-circuit (e.g. in a stateless RPC call)? Note
-        # that the client still expects a 0-object pack in most cases.
-        if objects_iter is None:
-            return
-        write_pack_objects(ProtocolFile(None, pack_data), objects_iter)
+            # Did the process short-circuit (e.g. in a stateless RPC call)? Note
+            # that the client still expects a 0-object pack in most cases.
+            if objects_iter is None:
+                return
+            write_pack_objects(ProtocolFile(None, pack_data), objects_iter)
+
+    def get_refs(self, path):
+        """Retrieve the current refs from a git smart server."""
+        from dulwich.repo import Repo
+
+        with closing(Repo(path)) as target:
+            return target.get_refs()
 
 
 # What Git client to use for local access
-default_local_git_client_cls = SubprocessGitClient
+default_local_git_client_cls = LocalGitClient
 
 
 class SSHVendor(object):
@@ -732,7 +834,7 @@ class SSHVendor(object):
         with the remote command.
 
         :param host: Host name
-        :param command: Command to run
+        :param command: Command to run (as argv array)
         :param username: Optional ame of user to log in as
         :param port: Optional SSH port to use
         """
@@ -743,6 +845,10 @@ class SubprocessSSHVendor(SSHVendor):
     """SSH vendor that shells out to the local 'ssh' command."""
 
     def run_command(self, host, command, username=None, port=None):
+        if (type(command) is not list or
+            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']
@@ -845,7 +951,9 @@ else:
 
         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
@@ -861,7 +969,7 @@ else:
             channel = client.get_transport().open_session()
 
             # Run commands
-            channel.exec_command(*command)
+            channel.exec_command(subprocess.list2cmdline(command))
 
             return ParamikoWrapper(
                 client, channel, progress_stderr=progress_stderr)
@@ -881,16 +989,26 @@ class SSHGitClient(TraditionalGitClient):
         self.alternative_paths = {}
 
     def _get_cmd_path(self, cmd):
-        return self.alternative_paths.get(cmd, 'git-%s' % cmd)
+        cmd = self.alternative_paths.get(cmd, b'git-' + cmd)
+        assert isinstance(cmd, bytes)
+        if sys.version_info[:2] <= (2, 6):
+            return shlex.split(cmd)
+        else:
+            # TODO(jelmer): Don't decode/encode here
+            return [x.encode('ascii') for x in shlex.split(cmd.decode('ascii'))]
 
     def _connect(self, cmd, path):
-        if path.startswith("/~"):
+        if type(cmd) is not bytes:
+            raise TypeError(path)
+        if type(path) is not bytes:
+            raise TypeError(path)
+        if path.startswith(b"/~"):
             path = path[1:]
+        argv = self._get_cmd_path(cmd) + [path]
         con = get_ssh_vendor().run_command(
-            self.host, ["%s '%s'" % (self._get_cmd_path(cmd), path)],
-            port=self.port, username=self.username)
-        return (Protocol(con.read, con.write, con.close, 
-                         report_activity=self._report_activity), 
+            self.host, argv, port=self.port, username=self.username)
+        return (Protocol(con.read, con.write, con.close,
+                         report_activity=self._report_activity),
                 con.can_read)
 
 
@@ -929,6 +1047,9 @@ class HttpGitClient(GitClient):
             self.opener = opener
         GitClient.__init__(self, *args, **kwargs)
 
+    def __repr__(self):
+        return "%s(%r, dumb=%r)" % (type(self).__name__, self.base_url, self.dumb)
+
     def _get_url(self, path):
         return urlparse.urljoin(self.base_url, path).rstrip("/") + "/"
 
@@ -977,13 +1098,15 @@ class HttpGitClient(GitClient):
         return resp
 
     def send_pack(self, path, determine_wants, generate_pack_contents,
-                  progress=None):
+                  progress=None, write_pack=write_pack_objects):
         """Upload a pack to a remote repository.
 
         :param path: Repository path
         :param generate_pack_contents: Function that can return a sequence of
             the shas of the objects to upload.
         :param progress: Optional progress function
+        :param write_pack: Function called with (file, iterable of objects) to
+            write the objects returned by generate_pack_contents to the server.
 
         :raises SendPackError: if server rejects the pack data
         :raises UpdateRefsError: if the server supports report-status
@@ -991,10 +1114,10 @@ class HttpGitClient(GitClient):
         """
         url = self._get_url(path)
         old_refs, server_capabilities = self._discover_references(
-            "git-receive-pack", url)
+            b"git-receive-pack", url)
         negotiated_capabilities = self._send_capabilities & server_capabilities
 
-        if 'report-status' in negotiated_capabilities:
+        if CAPABILITY_REPORT_STATUS in negotiated_capabilities:
             self._report_status_parser = ReportStatusParser()
 
         new_refs = determine_wants(dict(old_refs))
@@ -1010,8 +1133,8 @@ class HttpGitClient(GitClient):
             return new_refs
         objects = generate_pack_contents(have, want)
         if len(objects) > 0:
-            entries, sha = write_pack_objects(req_proto.write_file(), objects)
-        resp = self._smart_request("git-receive-pack", url,
+            write_pack(req_proto.write_file(), objects)
+        resp = self._smart_request(b"git-receive-pack", url,
                                    data=req_data.getvalue())
         try:
             resp_proto = Protocol(resp.read, None)
@@ -1034,7 +1157,7 @@ class HttpGitClient(GitClient):
         """
         url = self._get_url(path)
         refs, server_capabilities = self._discover_references(
-            "git-upload-pack", url)
+            b"git-upload-pack", url)
         negotiated_capabilities = self._fetch_capabilities & server_capabilities
         wants = determine_wants(refs)
         if wants is not None:
@@ -1049,7 +1172,7 @@ class HttpGitClient(GitClient):
             req_proto, negotiated_capabilities, graph_walker, wants,
             lambda: False)
         resp = self._smart_request(
-            "git-upload-pack", url, data=req_data.getvalue())
+            b"git-upload-pack", url, data=req_data.getvalue())
         try:
             resp_proto = Protocol(resp.read, None)
             self._handle_upload_pack_tail(resp_proto, negotiated_capabilities,
@@ -1058,6 +1181,13 @@ class HttpGitClient(GitClient):
         finally:
             resp.close()
 
+    def get_refs(self, path):
+        """Retrieve the current refs from a git smart server."""
+        url = self._get_url(path)
+        refs, _ = self._discover_references(
+            b"git-upload-pack", url)
+        return refs
+
 
 def get_transport_and_path_from_url(url, config=None, **kwargs):
     """Obtain a git client from a URL.
@@ -1104,6 +1234,11 @@ def get_transport_and_path(location, **kwargs):
     except ValueError:
         pass
 
+    if (sys.platform == 'win32' and
+            location[0].isalpha() and location[1:3] == ':\\'):
+        # Windows local path
+        return default_local_git_client_cls(**kwargs), location
+
     if ':' in location and not '@' in location:
         # SSH with no user@, zero or one leading slash.
         (hostname, path) = location.split(':')