Jelmer Vernooij 13 år sedan
förälder
incheckning
631cb5917f

+ 1 - 0
AUTHORS

@@ -2,6 +2,7 @@ Jelmer Vernooij <jelmer@samba.org>
 James Westby <jw+debian@jameswestby.net>
 John Carr <john.carr@unrouted.co.uk>
 Dave Borowitz <dborowitz@google.com>
+Chris Eberle <eberle1080@gmail.com>
 
 Hervé Cauwelier <herve@itaapy.com> wrote the original tutorial.
 

+ 32 - 1
NEWS

@@ -1,4 +1,32 @@
-0.8.1	UNRELEASED
+0.8.2	UNRELEASED
+
+ BUG FIXES
+
+  * Cope with different zlib buffer sizes in sha1 file parser.
+    (Jelmer Vernooij)
+
+  * Fix get_transport_and_path for HTTP/HTTPS URLs.
+    (Bruno Renié)
+
+  * Avoid calling free_objects() on NULL in error cases. (Chris Eberle)
+
+  * Fix use --bare argument to 'dulwich init'. (Chris Eberle)
+
+  * Properly abort connections when the determine_wants function
+    raises an exception. (Jelmer Vernooij, #856769)
+
+  * Tweak xcodebuild hack to deal with more error output.
+    (Jelmer Vernooij, #903840)
+
+ FEATURES
+
+  * Add support for retrieving tarballs from remote servers.
+    (Jelmer Vernooij, #379087)
+
+  * New method ``update_server_info`` which generates data
+    for dumb server access. (Jelmer Vernooij, #731235)
+
+0.8.1	2011-10-31
 
  FEATURES
 
@@ -35,6 +63,9 @@
  * Don't compile C extensions when running in pypy.
    (Ronny Pfannschmidt, #881546)
 
+ * Use different name for strnlen replacement function to avoid clashing
+   with system strnlen. (Jelmer Vernooij, #880362)
+
  API CHANGES
 
   * ``Repo.revision_history`` is now deprecated in favor of ``Repo.get_walker``.

+ 8 - 0
README

@@ -11,3 +11,11 @@ maintained by Jelmer Vernooij et al.
 Please file bugs in the Dulwich project on Launchpad: 
 
 https://bugs.launchpad.net/dulwich/+filebug
+
+The dulwich documentation can be found in doc/ and on the web:
+
+http://www.samba.org/~jelmer/dulwich/docs/
+
+The API reference can be generated using pydoctor, by running "make pydoctor", or on the web:
+
+http://www.samba.org/~jelmer/dulwich/apidocs

+ 20 - 3
bin/dulwich

@@ -1,6 +1,8 @@
-#!/usr/bin/python
+#!/usr/bin/python -u
+#
 # dulwich - Simple command-line interface to Dulwich
-# Copyright (C) 2008 Jelmer Vernooij <jelmer@samba.org>
+# Copyright (C) 2008-2011 Jelmer Vernooij <jelmer@samba.org>
+# vim: expandtab
 #
 # This program is free software; you can redistribute it and/or
 # modify it under the terms of the GNU General Public License
@@ -33,6 +35,14 @@ from dulwich.errors import ApplyDeltaError
 from dulwich.index import Index
 from dulwich.pack import Pack, sha_to_hex
 from dulwich.repo import Repo
+from dulwich.server import update_server_info
+
+
+def cmd_archive(args):
+    opts, args = getopt(args, "", [])
+    client, path = get_transport_and_path(args.pop(0))
+    committish = args.pop(0)
+    client.archive(path, committish, sys.stdout.write, sys.stderr.write)
 
 
 def cmd_fetch_pack(args):
@@ -114,7 +124,7 @@ def cmd_dump_index(args):
 
 
 def cmd_init(args):
-    opts, args = getopt(args, "", ["--bare"])
+    opts, args = getopt(args, "", ["bare"])
     opts = dict(opts)
 
     if args == []:
@@ -165,6 +175,11 @@ def cmd_commit(args):
     r.do_commit(committer=committer, author=author, message=opts["--message"])
 
 
+def cmd_update_server_info(args):
+    r = Repo(".")
+    update_server_info(r)
+
+
 commands = {
     "commit": cmd_commit,
     "fetch-pack": cmd_fetch_pack,
@@ -173,6 +188,8 @@ commands = {
     "init": cmd_init,
     "log": cmd_log,
     "clone": cmd_clone,
+    "archive": cmd_archive,
+    "update-server-info": cmd_update_server_info,
     }
 
 if len(sys.argv) < 2:

+ 1 - 1
dulwich/__init__.py

@@ -23,4 +23,4 @@
 
 from dulwich import (client, protocol, repo, server)
 
-__version__ = (0, 8, 1)
+__version__ = (0, 8, 2)

+ 6 - 3
dulwich/_diff_tree.c

@@ -135,7 +135,8 @@ static PyObject **tree_entries(char *path, Py_ssize_t path_len, PyObject *tree,
 	return result;
 
 error:
-	free_objects(result, i);
+	if (result)
+		free_objects(result, i);
 	Py_DECREF(items);
 	return NULL;
 }
@@ -243,8 +244,10 @@ error:
 	result = NULL;
 
 done:
-	free_objects(entries1, n1);
-	free_objects(entries2, n2);
+	if (entries1)
+		free_objects(entries1, n1);
+	if (entries2)
+		free_objects(entries2, n2);
 	return result;
 }
 

+ 4 - 6
dulwich/_objects.c

@@ -21,20 +21,18 @@
 #include <stdlib.h>
 #include <sys/stat.h>
 
-#if defined(__APPLE__)
-#include <Availability.h>
-#endif
-
 #if (PY_VERSION_HEX < 0x02050000)
 typedef int Py_ssize_t;
 #endif
 
-#if defined(__MINGW32_VERSION) || (defined(__APPLE__) && __MAC_OS_X_VERSION_MIN_REQUIRED < 1070) || (defined(_MSC_VER) && _MSC_VER < 1400)
-size_t strnlen(const char *text, size_t maxlen)
+#if defined(__MINGW32_VERSION) || defined(__APPLE__)
+size_t rep_strnlen(char *text, size_t maxlen);
+size_t rep_strnlen(char *text, size_t maxlen)
 {
 	const char *last = memchr(text, '\0', maxlen);
 	return last ? (size_t) (last - text) : maxlen;
 }
+#define strnlen rep_strnlen
 #endif
 
 #define bytehex(x) (((x)<0xa)?('0'+(x)):('a'-0xa+(x)))

+ 43 - 9
dulwich/client.py

@@ -35,6 +35,7 @@ from dulwich.errors import (
     UpdateRefsError,
     )
 from dulwich.protocol import (
+    _RBUFSIZE,
     PktLineParser,
     Protocol,
     TCP_GIT_PORT,
@@ -350,7 +351,7 @@ class GitClient(object):
         proto.write_pkt_line('done\n')
 
     def _handle_upload_pack_tail(self, proto, capabilities, graph_walker,
-                                 pack_data, progress):
+                                 pack_data, progress, rbufsize=_RBUFSIZE):
         """Handle the tail of a 'git-upload-pack' request.
 
         :param proto: Protocol object to read from
@@ -358,6 +359,7 @@ class GitClient(object):
         :param graph_walker: GraphWalker instance to call .ack() on
         :param pack_data: Function to call with pack data
         :param progress: Optional progress reporting function
+        :param rbufsize: Read buffer size
         """
         pkt = proto.read_pkt_line()
         while pkt:
@@ -375,9 +377,11 @@ class GitClient(object):
             if data:
                 raise Exception('Unexpected response %r' % data)
         else:
-            # FIXME: Buffering?
-            pack_data(self.read())
-
+            while True:
+                data = self.read(rbufsize)
+                if data == "":
+                    break
+                pack_data(data)
 
 
 class TraditionalGitClient(GitClient):
@@ -415,7 +419,11 @@ class TraditionalGitClient(GitClient):
         negotiated_capabilities = list(self._send_capabilities)
         if 'report-status' not in server_capabilities:
             negotiated_capabilities.remove('report-status')
-        new_refs = determine_wants(old_refs)
+        try:
+            new_refs = determine_wants(old_refs)
+        except:
+            proto.write_pkt_line(None)
+            raise
         if new_refs is None:
             proto.write_pkt_line(None)
             return old_refs
@@ -442,7 +450,11 @@ class TraditionalGitClient(GitClient):
         proto, can_read = self._connect('upload-pack', path)
         (refs, server_capabilities) = self._read_refs(proto)
         negotiated_capabilities = list(self._fetch_capabilities)
-        wants = determine_wants(refs)
+        try:
+            wants = determine_wants(refs)
+        except:
+            proto.write_pkt_line(None)
+            raise
         if not wants:
             proto.write_pkt_line(None)
             return refs
@@ -452,6 +464,24 @@ class TraditionalGitClient(GitClient):
             graph_walker, pack_data, progress)
         return refs
 
+    def archive(self, path, committish, write_data, progress=None):
+        proto, can_read = self._connect('upload-archive', path)
+        proto.write_pkt_line("argument %s" % committish)
+        proto.write_pkt_line(None)
+        pkt = proto.read_pkt_line()
+        if pkt == "NACK\n":
+            return
+        elif pkt == "ACK\n":
+            pass
+        elif pkt.startswith("ERR "):
+            raise GitProtocolError(pkt[4:].rstrip("\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})
+
 
 class TCPGitClient(TraditionalGitClient):
     """A Git Client that works over TCP directly (i.e. git://)."""
@@ -520,6 +550,10 @@ class SubprocessGitClient(TraditionalGitClient):
 
     def __init__(self, *args, **kwargs):
         self._connection = None
+        self._stderr = None
+        self._stderr = kwargs.get('stderr')
+        if 'stderr' in kwargs:
+            del kwargs['stderr']
         GitClient.__init__(self, *args, **kwargs)
 
     def _connect(self, service, path):
@@ -527,7 +561,8 @@ class SubprocessGitClient(TraditionalGitClient):
         argv = ['git', service, path]
         p = SubprocessWrapper(
             subprocess.Popen(argv, bufsize=0, stdin=subprocess.PIPE,
-                             stdout=subprocess.PIPE))
+                             stdout=subprocess.PIPE,
+                             stderr=self._stderr))
         return Protocol(p.read, p.write,
                         report_activity=self._report_activity), p.can_read
 
@@ -715,8 +750,7 @@ def get_transport_and_path(uri):
         return SSHGitClient(parsed.hostname, port=parsed.port,
                             username=parsed.username), parsed.path
     elif parsed.scheme in ('http', 'https'):
-        return HttpGitClient(urlparse.urlunparse(
-            parsed.scheme, parsed.netloc, path='/'))
+        return HttpGitClient(urlparse.urlunparse(parsed)), parsed.path
 
     if parsed.scheme and not parsed.netloc:
         # SSH with no user@, zero or one leading slash.

+ 50 - 13
dulwich/index.py

@@ -275,19 +275,13 @@ class Index(object):
         :param want_unchanged: Whether unchanged files should be reported
         :return: Iterator over tuples with (oldpath, newpath), (oldmode, newmode), (oldsha, newsha)
         """
-        mine = set(self._byname.keys())
-        for (name, mode, sha) in object_store.iter_tree_contents(tree):
-            if name in mine:
-                if (want_unchanged or self.get_sha1(name) != sha or 
-                    self.get_mode(name) != mode):
-                    yield ((name, name), (mode, self.get_mode(name)), (sha, self.get_sha1(name)))
-                mine.remove(name)
-            else:
-                # Was removed
-                yield ((name, None), (mode, None), (sha, None))
-        # Mention added files
-        for name in mine:
-            yield ((None, name), (None, self.get_mode(name)), (None, self.get_sha1(name)))
+        def lookup_entry(path):
+            entry = self[path]
+            return entry[-2], entry[-6]
+        for (name, mode, sha) in changes_from_tree(self._byname.keys(),
+                lookup_entry, object_store, tree,
+                want_unchanged=want_unchanged):
+            yield (name, mode, sha)
 
     def commit(self, object_store):
         """Create a new tree from an index.
@@ -345,3 +339,46 @@ def commit_index(object_store, index):
     :return: Root tree sha.
     """
     return commit_tree(object_store, index.iterblobs())
+
+
+def changes_from_tree(names, lookup_entry, object_store, tree,
+        want_unchanged=False):
+    """Find the differences between the contents of a tree and
+    a working copy.
+
+    :param names: Iterable of names in the working copy
+    :param lookup_entry: Function to lookup an entry in the working copy
+    :param object_store: Object store to use for retrieving tree contents
+    :param tree: SHA1 of the root tree
+    :param want_unchanged: Whether unchanged files should be reported
+    :return: Iterator over tuples with (oldpath, newpath), (oldmode, newmode),
+        (oldsha, newsha)
+    """
+    other_names = set(names)
+    for (name, mode, sha) in object_store.iter_tree_contents(tree):
+        try:
+            (other_sha, other_mode) = lookup_entry(name)
+        except KeyError:
+            # Was removed
+            yield ((name, None), (mode, None), (sha, None))
+        else:
+            other_names.remove(name)
+            if (want_unchanged or other_sha != sha or other_mode != mode):
+                yield ((name, name), (mode, other_mode), (sha, other_sha))
+
+    # Mention added files
+    for name in other_names:
+        (other_sha, other_mode) = lookup_entry(name)
+        yield ((None, name), (None, other_mode), (None, other_sha))
+
+
+def index_entry_from_stat(stat_val, hex_sha, flags):
+    """Create a new index entry from a stat value.
+
+    :param stat_val: POSIX stat_result instance
+    :param hex_sha: Hex sha of the object
+    :param flags: Index flags
+    """
+    return (stat_val.st_ctime, stat_val.st_mtime, stat_val.st_dev,
+            stat_val.st_ino, stat_val.st_mode, stat_val.st_uid,
+            stat_val.st_gid, stat_val.st_size, hex_sha, flags)

+ 13 - 4
dulwich/object_store.py

@@ -476,6 +476,7 @@ class DiskObjectStore(PackBasedObjectStore):
 
         # Complete the pack.
         for ext_sha in indexer.ext_refs():
+            assert len(ext_sha) == 20
             type_num, data = self.get_raw(ext_sha)
             offset = f.tell()
             crc32 = write_pack_object(f, type_num, data, sha=new_sha)
@@ -610,9 +611,17 @@ class MemoryObjectStore(BaseObjectStore):
         super(MemoryObjectStore, self).__init__()
         self._data = {}
 
+    def _to_hexsha(self, sha):
+        if len(sha) == 40:
+            return sha
+        elif len(sha) == 20:
+            return sha_to_hex(sha)
+        else:
+            raise ValueError("Invalid sha %r" % sha)
+
     def contains_loose(self, sha):
         """Check if a particular object is present by SHA1 and is loose."""
-        return sha in self._data
+        return self._to_hexsha(sha) in self._data
 
     def contains_packed(self, sha):
         """Check if a particular object is present by SHA1 and is packed."""
@@ -633,15 +642,15 @@ class MemoryObjectStore(BaseObjectStore):
         :param name: sha for the object.
         :return: tuple with numeric type and object contents.
         """
-        obj = self[name]
+        obj = self[self._to_hexsha(name)]
         return obj.type_num, obj.as_raw_string()
 
     def __getitem__(self, name):
-        return self._data[name]
+        return self._data[self._to_hexsha(name)]
 
     def __delitem__(self, name):
         """Delete an object from this store, for testing only."""
-        del self._data[name]
+        del self._data[self._to_hexsha(name)]
 
     def add_object(self, obj):
         """Add a single object to this object store.

+ 1 - 1
dulwich/objects.py

@@ -294,7 +294,7 @@ class ShaFile(object):
     def _is_legacy_object(cls, magic):
         b0, b1 = map(ord, magic)
         word = (b0 << 8) + b1
-        return b0 == 0x78 and (word % 31) == 0
+        return (b0 & 0x8F) == 0x08 and (word % 31) == 0
 
     @classmethod
     def _parse_file_header(cls, f):

+ 10 - 5
dulwich/pack.py

@@ -605,9 +605,11 @@ class PackIndex2(FilePackIndex):
 
     def __init__(self, filename, file=None, contents=None, size=None):
         super(PackIndex2, self).__init__(filename, file, contents, size)
-        assert self._contents[:4] == '\377tOc', 'Not a v2 pack index file'
+        if self._contents[:4] != '\377tOc':
+            raise AssertionError('Not a v2 pack index file')
         (self.version, ) = unpack_from('>L', self._contents, 4)
-        assert self.version == 2, 'Version was %d' % self.version
+        if self.version != 2:
+            raise AssertionError('Version was %d' % self.version)
         self._fan_out_table = self._read_fan_out_table(8)
         self._name_table_offset = 8 + 0x100 * 4
         self._crc32_table_offset = self._name_table_offset + 20 * len(self)
@@ -641,9 +643,11 @@ def read_pack_header(read):
     header = read(12)
     if not header:
         return None, None
-    assert header[:4] == 'PACK'
+    if header[:4] != 'PACK':
+        raise AssertionError('Invalid pack header %r' % header)
     (version,) = unpack_from('>L', header, 4)
-    assert version in (2, 3), 'Version was %d' % version
+    if version not in (2, 3):
+        raise AssertionError('Version was %d' % version)
     (num_objects,) = unpack_from('>L', header, 8)
     return (version, num_objects)
 
@@ -693,7 +697,8 @@ def unpack_object(read_all, read_some=None, compute_crc32=False,
     if type_num == OFS_DELTA:
         bytes, crc32 = take_msb_bytes(read_all, crc32=crc32)
         raw_base += len(bytes)
-        assert not (bytes[-1] & 0x80)
+        if bytes[-1] & 0x80:
+            raise AssertionError
         delta_base_offset = bytes[0] & 0x7f
         for byte in bytes[1:]:
             delta_base_offset += 1

+ 34 - 0
dulwich/repo.py

@@ -388,6 +388,40 @@ class DictRefsContainer(RefsContainer):
         self._peeled.update(peeled)
 
 
+class InfoRefsContainer(RefsContainer):
+    """Refs container that reads refs from a info/refs file."""
+
+    def __init__(self, f):
+        self._refs = {}
+        self._peeled = {}
+        for l in f.readlines():
+            sha, name = l.rstrip("\n").split("\t")
+            if name.endswith("^{}"):
+                name = name[:-3]
+                if not check_ref_format(name):
+                    raise ValueError("invalid ref name '%s'" % name)
+                self._peeled[name] = sha
+            else:
+                if not check_ref_format(name):
+                    raise ValueError("invalid ref name '%s'" % name)
+                self._refs[name] = sha
+
+    def allkeys(self):
+        return self._refs.keys()
+
+    def read_loose_ref(self, name):
+        return self._refs.get(name, None)
+
+    def get_packed_refs(self):
+        return {}
+
+    def get_peeled(self, name):
+        try:
+            return self._peeled[name]
+        except KeyError:
+            return self._refs[name]
+
+
 class DiskRefsContainer(RefsContainer):
     """Refs container that reads refs from disk."""
 

+ 52 - 5
dulwich/server.py

@@ -27,6 +27,7 @@ Documentation/technical directory in the cgit distribution, and in particular:
 
 
 import collections
+import os
 import socket
 import SocketServer
 import sys
@@ -36,6 +37,7 @@ from dulwich.errors import (
     ApplyDeltaError,
     ChecksumMismatch,
     GitProtocolError,
+    NotGitRepository,
     UnexpectedCommandError,
     ObjectFormatException,
     )
@@ -72,14 +74,19 @@ class Backend(object):
     """A backend for the Git smart server implementation."""
 
     def open_repository(self, path):
-        """Open the repository at a path."""
+        """Open the repository at a path.
+
+        :param path: Path to the repository
+        :raise NotGitRepository: no git repository was found at path
+        :return: Instance of BackendRepo
+        """
         raise NotImplementedError(self.open_repository)
 
 
 class BackendRepo(object):
     """Repository abstraction used by the Git server.
-    
-    Please note that the methods required here are a 
+
+    Please note that the methods required here are a
     subset of those provided by dulwich.repo.Repo.
     """
 
@@ -125,8 +132,11 @@ class DictBackend(Backend):
 
     def open_repository(self, path):
         logger.debug('Opening repository at %s', path)
-        # FIXME: What to do in case there is no repo ?
-        return self.repos[path]
+        try:
+            return self.repos[path]
+        except KeyError:
+            raise NotGitRepository("No git repository was found at %(path)s",
+                path=path)
 
 
 class FileSystemBackend(Backend):
@@ -778,3 +788,40 @@ def serve_command(handler_cls, argv=sys.argv, backend=None, inf=sys.stdin,
     # FIXME: Catch exceptions and write a single-line summary to outf.
     handler.handle()
     return 0
+
+
+def generate_info_refs(repo):
+    """Generate an info refs file."""
+    refs = repo.get_refs()
+    for name in sorted(refs.iterkeys()):
+        # get_refs() includes HEAD as a special case, but we don't want to
+        # advertise it
+        if name == 'HEAD':
+            continue
+        sha = refs[name]
+        o = repo.object_store[sha]
+        if not o:
+            continue
+        yield '%s\t%s\n' % (sha, name)
+        peeled_sha = repo.get_peeled(name)
+        if peeled_sha != sha:
+            yield '%s\t%s^{}\n' % (peeled_sha, name)
+
+
+def generate_objects_info_packs(repo):
+    """Generate an index for for packs."""
+    for pack in repo.object_store.packs:
+        yield 'P pack-%s.pack\n' % pack.name()
+
+
+def update_server_info(repo):
+    """Generate server info for dumb file access.
+
+    This generates info/refs and objects/info/packs,
+    similar to "git update-server-info".
+    """
+    repo._put_named_file(os.path.join('info', 'refs'),
+        "".join(generate_info_refs(repo)))
+
+    repo._put_named_file(os.path.join('objects', 'info', 'packs'),
+        "".join(generate_objects_info_packs(repo)))

+ 2 - 0
dulwich/tests/__init__.py

@@ -105,10 +105,12 @@ def tutorial_test_suite():
         ]
     tutorial_files = ["../../docs/tutorial/%s.txt" % name for name in tutorial]
     def setup(test):
+        test.__old_cwd = os.getcwd()
         test.__dulwich_tempdir = tempfile.mkdtemp()
         os.chdir(test.__dulwich_tempdir)
     def teardown(test):
         shutil.rmtree(test.__dulwich_tempdir)
+        os.chdir(test.__old_cwd)
     return doctest.DocFileSuite(setUp=setup, tearDown=teardown,
         *tutorial_files)
 

+ 15 - 2
dulwich/tests/compat/test_client.py

@@ -19,6 +19,7 @@
 
 """Compatibilty tests between the Dulwich client and the cgit server."""
 
+from cStringIO import StringIO
 import BaseHTTPServer
 import SimpleHTTPServer
 import copy
@@ -27,6 +28,7 @@ import select
 import shutil
 import signal
 import subprocess
+import tarfile
 import tempfile
 import threading
 import urllib
@@ -164,6 +166,14 @@ class DulwichClientTestBase(object):
                               'refs/heads/master': 'non-fast-forward'},
                              e.ref_status)
 
+    def test_archive(self):
+        c = self._client()
+        f = StringIO()
+        c.archive(self._build_path('/server_new.export'), 'HEAD', f.write)
+        f.seek(0)
+        tf = tarfile.open(fileobj=f)
+        self.assertEquals(['baz', 'foo'], tf.getnames())
+
     def test_fetch_pack(self):
         c = self._client()
         dest = repo.Repo(os.path.join(self.gitroot, 'dest'))
@@ -211,7 +221,7 @@ class DulwichTCPClientTest(CompatTestCase, DulwichClientTestBase):
             ['daemon', '--verbose', '--export-all',
              '--pid-file=%s' % self.pidfile, '--base-path=%s' % self.gitroot,
              '--detach', '--reuseaddr', '--enable=receive-pack',
-             '--listen=localhost', self.gitroot], cwd=self.gitroot)
+             '--enable=upload-archive', '--listen=localhost', self.gitroot], cwd=self.gitroot)
         if not check_for_daemon():
             raise SkipTest('git-daemon failed to start')
 
@@ -272,7 +282,7 @@ class DulwichSubprocessClientTest(CompatTestCase, DulwichClientTestBase):
         CompatTestCase.tearDown(self)
 
     def _client(self):
-        return client.SubprocessGitClient()
+        return client.SubprocessGitClient(stderr=subprocess.PIPE)
 
     def _build_path(self, path):
         return self.gitroot + path
@@ -446,3 +456,6 @@ class DulwichHttpClientTest(CompatTestCase, DulwichClientTestBase):
 
     def _build_path(self, path):
         return path
+
+    def test_archive(self):
+        raise SkipTest("exporting archives not supported over http")

+ 2 - 5
dulwich/tests/compat/test_pack.py

@@ -43,12 +43,9 @@ class TestPack(PackTests):
 
     def setUp(self):
         require_git_version((1, 5, 0))
-        PackTests.setUp(self)
+        super(TestPack, self).setUp()
         self._tempdir = tempfile.mkdtemp()
-
-    def tearDown(self):
-        shutil.rmtree(self._tempdir)
-        PackTests.tearDown(self)
+        self.addCleanup(shutil.rmtree, self._tempdir)
 
     def test_copy(self):
         origpack = self.get_pack(pack1_sha)

+ 2 - 5
dulwich/tests/compat/test_repository.py

@@ -45,12 +45,9 @@ class ObjectStoreTestCase(CompatTestCase):
     """Tests for git repository compatibility."""
 
     def setUp(self):
-        CompatTestCase.setUp(self)
+        super(ObjectStoreTestCase, self).setUp()
         self._repo = import_repo('server_new.export')
-
-    def tearDown(self):
-        CompatTestCase.tearDown(self)
-        tear_down_repo(self._repo)
+        self.addCleanup(tear_down_repo, self._repo)
 
     def _run_git(self, args):
         return run_git_or_fail(args, cwd=self._repo.path)

+ 3 - 2
dulwich/tests/compat/utils.py

@@ -133,8 +133,9 @@ def run_git_or_fail(args, git_path=_DEFAULT_GIT, input=None, **popen_kwargs):
     popen_kwargs['stderr'] = subprocess.STDOUT
     returncode, stdout = run_git(args, git_path=git_path, input=input,
                                  capture_stdout=True, **popen_kwargs)
-    assert returncode == 0, "git with args %r failed with %d" % (
-        args, returncode)
+    if returncode != 0:
+        raise AssertionError("git with args %r failed with %d" % (
+            args, returncode))
     return stdout
 
 

+ 15 - 0
dulwich/tests/test_client.py

@@ -23,6 +23,7 @@ from dulwich.client import (
     TCPGitClient,
     SubprocessGitClient,
     SSHGitClient,
+    HttpGitClient,
     ReportStatusParser,
     SendPackError,
     UpdateRefsError,
@@ -66,6 +67,14 @@ class GitClientTests(TestCase):
         self.assertEquals(set(['ofs-delta', 'report-status', 'side-band-64k']),
                           set(self.client._send_capabilities))
 
+    def test_archive_ack(self):
+        self.rin.write(
+            '0009NACK\n'
+            '0000')
+        self.rin.seek(0)
+        self.client.archive('bla', 'HEAD', None, None)
+        self.assertEquals(self.rout.getvalue(), '0011argument HEAD0000')
+
     def test_fetch_pack_none(self):
         self.rin.write(
             '008855dcc6bf963f922e1ed5c4bbaaefcfacef57b1d7 HEAD.multi_ack '
@@ -137,6 +146,12 @@ class GitClientTests(TestCase):
         self.assertRaises(ValueError, get_transport_and_path,
         'prospero://bar/baz')
 
+    def test_get_transport_and_path_http(self):
+        url = 'https://github.com/jelmer/dulwich'
+        client, path = get_transport_and_path(url)
+        self.assertTrue(isinstance(client, HttpGitClient))
+        self.assertEquals('/jelmer/dulwich', path)
+
 
 class SSHGitClientTests(TestCase):
 

+ 22 - 0
dulwich/tests/test_index.py

@@ -23,6 +23,7 @@ from cStringIO import (
     StringIO,
     )
 import os
+import posix
 import shutil
 import stat
 import struct
@@ -32,6 +33,7 @@ from dulwich.index import (
     Index,
     cleanup_mode,
     commit_tree,
+    index_entry_from_stat,
     read_index,
     write_cache_time,
     write_index,
@@ -169,3 +171,23 @@ class WriteCacheTimeTests(TestCase):
         f = StringIO()
         write_cache_time(f, 434343.000000021)
         self.assertEquals(struct.pack(">LL", 434343, 21), f.getvalue())
+
+
+class IndexEntryFromStatTests(TestCase):
+
+    def test_simple(self):
+        st = posix.stat_result((16877, 131078, 64769L,
+                154, 1000, 1000, 12288,
+                1323629595, 1324180496, 1324180496))
+        entry = index_entry_from_stat(st, "22" * 20, 0)
+        self.assertEquals(entry, (
+            1324180496,
+            1324180496,
+            64769L,
+            131078,
+            16877,
+            1000,
+            1000,
+            12288,
+            '2222222222222222222222222222222222222222',
+            0))

+ 27 - 0
dulwich/tests/test_objects.py

@@ -38,6 +38,7 @@ from dulwich.objects import (
     Blob,
     Tree,
     Commit,
+    ShaFile,
     Tag,
     format_timezone,
     hex_to_sha,
@@ -229,6 +230,31 @@ class ShaFileCheckTests(TestCase):
         self.assertEqual(None, obj.check())
 
 
+small_buffer_zlib_object = (
+ "\x48\x89\x15\xcc\x31\x0e\xc2\x30\x0c\x40\x51\xe6"
+ "\x9c\xc2\x3b\xaa\x64\x37\xc4\xc1\x12\x42\x5c\xc5"
+ "\x49\xac\x52\xd4\x92\xaa\x78\xe1\xf6\x94\xed\xeb"
+ "\x0d\xdf\x75\x02\xa2\x7c\xea\xe5\x65\xd5\x81\x8b"
+ "\x9a\x61\xba\xa0\xa9\x08\x36\xc9\x4c\x1a\xad\x88"
+ "\x16\xba\x46\xc4\xa8\x99\x6a\x64\xe1\xe0\xdf\xcd"
+ "\xa0\xf6\x75\x9d\x3d\xf8\xf1\xd0\x77\xdb\xfb\xdc"
+ "\x86\xa3\x87\xf1\x2f\x93\xed\x00\xb7\xc7\xd2\xab"
+ "\x2e\xcf\xfe\xf1\x3b\x50\xa4\x91\x53\x12\x24\x38"
+ "\x23\x21\x86\xf0\x03\x2f\x91\x24\x52"
+ )
+
+
+class ShaFileTests(TestCase):
+
+    def test_deflated_smaller_window_buffer(self):
+        # zlib on some systems uses smaller buffers,
+        # resulting in a different header.
+        # See https://github.com/libgit2/libgit2/pull/464
+        sf = ShaFile.from_file(StringIO(small_buffer_zlib_object))
+        self.assertEquals(sf.type_name, "tag")
+        self.assertEquals(sf.tagger, " <@localhost>")
+
+
 class CommitSerializationTests(TestCase):
 
     def make_commit(self, **kwargs):
@@ -582,6 +608,7 @@ OK2XeQOiEeXtT76rV4t2WR4=
 
 
 class TagParseTests(ShaFileCheckTests):
+
     def make_tag_lines(self,
                        object_sha="a38d6181ff27824c79fc7df825164a212eff6a3f",
                        object_type_name="commit",

+ 54 - 0
dulwich/tests/test_repository.py

@@ -37,6 +37,7 @@ from dulwich.config import ConfigFile
 from dulwich.repo import (
     check_ref_format,
     DictRefsContainer,
+    InfoRefsContainer,
     Repo,
     MemoryRepo,
     read_packed_refs,
@@ -892,3 +893,56 @@ class DiskRefsContainerTests(RefsContainerTests, TestCase):
             self._refs.read_ref("refs/heads/packed"))
         self.assertEqual(None,
             self._refs.read_ref("nonexistant"))
+
+
+_TEST_REFS_SERIALIZED = (
+'42d06bd4b77fed026b154d16493e5deab78f02ec\trefs/heads/master\n'
+'42d06bd4b77fed026b154d16493e5deab78f02ec\trefs/heads/packed\n'
+'df6800012397fb85c56e7418dd4eb9405dee075c\trefs/tags/refs-0.1\n'
+'3ec9c43c84ff242e3ef4a9fc5bc111fd780a76a8\trefs/tags/refs-0.2\n')
+
+
+class InfoRefsContainerTests(TestCase):
+
+    def test_invalid_refname(self):
+        text = _TEST_REFS_SERIALIZED + '00' * 20 + '\trefs/stash\n'
+        refs = InfoRefsContainer(StringIO(text))
+        expected_refs = dict(_TEST_REFS)
+        del expected_refs['HEAD']
+        expected_refs["refs/stash"] = "00" * 20
+        self.assertEquals(expected_refs, refs.as_dict())
+
+    def test_keys(self):
+        refs = InfoRefsContainer(StringIO(_TEST_REFS_SERIALIZED))
+        actual_keys = set(refs.keys())
+        self.assertEqual(set(refs.allkeys()), actual_keys)
+        # ignore the symref loop if it exists
+        actual_keys.discard('refs/heads/loop')
+        expected_refs = dict(_TEST_REFS)
+        del expected_refs['HEAD']
+        self.assertEqual(set(expected_refs.iterkeys()), actual_keys)
+
+        actual_keys = refs.keys('refs/heads')
+        actual_keys.discard('loop')
+        self.assertEqual(['master', 'packed'], sorted(actual_keys))
+        self.assertEqual(['refs-0.1', 'refs-0.2'],
+                         sorted(refs.keys('refs/tags')))
+
+    def test_as_dict(self):
+        refs = InfoRefsContainer(StringIO(_TEST_REFS_SERIALIZED))
+        # refs/heads/loop does not show up even if it exists
+        expected_refs = dict(_TEST_REFS)
+        del expected_refs['HEAD']
+        self.assertEqual(expected_refs, refs.as_dict())
+
+    def test_contains(self):
+        refs = InfoRefsContainer(StringIO(_TEST_REFS_SERIALIZED))
+        self.assertTrue('refs/heads/master' in refs)
+        self.assertFalse('refs/heads/bar' in refs)
+
+    def test_get_peeled(self):
+        refs = InfoRefsContainer(StringIO(_TEST_REFS_SERIALIZED))
+        # refs/heads/loop does not show up even if it exists
+        self.assertEqual(
+            _TEST_REFS['refs/heads/master'],
+            refs.get_peeled('refs/heads/master'))

+ 28 - 0
dulwich/tests/test_server.py

@@ -44,6 +44,7 @@ from dulwich.server import (
     ReceivePackHandler,
     SingleAckGraphWalkerImpl,
     UploadPackHandler,
+    update_server_info,
     )
 from dulwich.tests import TestCase
 from dulwich.tests.utils import (
@@ -689,3 +690,30 @@ class ServeCommandTests(TestCase):
             outlines[0][4:].split("\x00")[0])
         self.assertEquals("0000", outlines[-1])
         self.assertEquals(0, exitcode)
+
+
+class UpdateServerInfoTests(TestCase):
+    """Tests for update_server_info."""
+
+    def setUp(self):
+        super(UpdateServerInfoTests, self).setUp()
+        self.path = tempfile.mkdtemp()
+        self.repo = Repo.init(self.path)
+
+    def test_empty(self):
+        update_server_info(self.repo)
+        self.assertEquals("",
+            open(os.path.join(self.path, ".git", "info", "refs"), 'r').read())
+        self.assertEquals("",
+            open(os.path.join(self.path, ".git", "objects", "info", "packs"), 'r').read())
+
+    def test_simple(self):
+        commit_id = self.repo.do_commit(
+            message="foo",
+            committer="Joe Example <joe@example.com>",
+            ref="refs/heads/foo")
+        update_server_info(self.repo)
+        ref_text = open(os.path.join(self.path, ".git", "info", "refs"), 'r').read()
+        self.assertEquals(ref_text, "%s\trefs/heads/foo\n" % commit_id)
+        packs_text = open(os.path.join(self.path, ".git", "objects", "info", "packs"), 'r').read()
+        self.assertEquals(packs_text, "")

+ 1 - 1
dulwich/tests/test_walk.py

@@ -123,7 +123,7 @@ class WalkerTest(TestCase):
         del self.store[cs[-1].id]
         for i in xrange(1, 11):
             self.assertWalkYields(cs[:i], [cs[0].id], max_entries=i)
-        self.assertRaises(MissingCommitError, Walker, self.store, cs[0].id)
+        self.assertRaises(MissingCommitError, Walker, self.store, [cs[-1].id])
 
     def test_branch(self):
         c1, x2, x3, y4 = self.make_commits([[1], [2, 1], [3, 2], [4, 1]])

+ 1 - 0
dulwich/tests/utils.py

@@ -230,6 +230,7 @@ def build_pack(f, objects_spec, store=None):
     expected = []
     for i in xrange(num_objects):
         type_num, data, sha = full_objects[i]
+        assert len(sha) == 20
         expected.append((offsets[i], type_num, data, sha, crc32s[i]))
 
     sf.write_sha()

+ 6 - 16
dulwich/web.py

@@ -29,6 +29,7 @@ try:
 except ImportError:
     from dulwich._compat import parse_qs
 from dulwich import log_utils
+from dulwich.gzip import GzipConsumer
 from dulwich.protocol import (
     ReceivableProtocol,
     )
@@ -38,6 +39,8 @@ from dulwich.repo import (
 from dulwich.server import (
     DictBackend,
     DEFAULT_HANDLERS,
+    generate_info_refs,
+    generate_objects_info_packs,
     )
 
 
@@ -180,28 +183,15 @@ def get_info_refs(req, backend, mat):
         req.respond(HTTP_OK, 'text/plain')
         logger.info('Emulating dumb info/refs')
         repo = get_repo(backend, mat)
-        refs = repo.get_refs()
-        for name in sorted(refs.iterkeys()):
-            # get_refs() includes HEAD as a special case, but we don't want to
-            # advertise it
-            if name == 'HEAD':
-                continue
-            sha = refs[name]
-            o = repo[sha]
-            if not o:
-                continue
-            yield '%s\t%s\n' % (sha, name)
-            peeled_sha = repo.get_peeled(name)
-            if peeled_sha != sha:
-                yield '%s\t%s^{}\n' % (peeled_sha, name)
+        for text in generate_info_refs(repo):
+            yield text
 
 
 def get_info_packs(req, backend, mat):
     req.nocache()
     req.respond(HTTP_OK, 'text/plain')
     logger.info('Emulating dumb info/packs')
-    for pack in get_repo(backend, mat).object_store.packs:
-        yield 'P pack-%s.pack\n' % pack.name()
+    return generate_objects_info_packs(get_repo(backend, mat))
 
 
 class _LengthLimitedFile(object):

+ 11 - 19
setup.py

@@ -10,7 +10,7 @@ except ImportError:
     has_setuptools = False
 from distutils.core import Distribution
 
-dulwich_version_string = '0.8.1'
+dulwich_version_string = '0.8.2'
 
 include_dirs = []
 # Windows MSVC support
@@ -35,27 +35,19 @@ class DulwichDistribution(Distribution):
 
     pure = False
 
-def runcmd(cmd, env):
-    import subprocess
-    p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
-                         stderr=subprocess.PIPE, env=env)
-    out, err = p.communicate()
-    err = [e for e in err.splitlines()
-           if not e.startswith('Not trusting file') \
-              and not e.startswith('warning: Not importing')]
-    if err:
-        return ''
-    return out
-
-
 if sys.platform == 'darwin' and os.path.exists('/usr/bin/xcodebuild'):
     # XCode 4.0 dropped support for ppc architecture, which is hardcoded in
     # distutils.sysconfig
-    version = runcmd(['/usr/bin/xcodebuild', '-version'], {}).splitlines()[0]
-    # Also parse only first digit, because 3.2.1 can't be parsed nicely
-    if (version.startswith('Xcode') and
-        int(version.split()[1].split('.')[0]) >= 4):
-        os.environ['ARCHFLAGS'] = ''
+    import subprocess
+    p = subprocess.Popen(
+        ['/usr/bin/xcodebuild', '-version'], stdout=subprocess.PIPE,
+        stderr=subprocess.PIPE, env={})
+    out, err = p.communicate()
+    for l in out.splitlines():
+        # Also parse only first digit, because 3.2.1 can't be parsed nicely
+        if (l.startswith('Xcode') and
+            int(l.split()[1].split('.')[0]) >= 4):
+            os.environ['ARCHFLAGS'] = ''
 
 setup_kwargs = {}