浏览代码

Merge tag 'dulwich-0.8.6' into unstable.

Conflicts:
	.gitignore
Jelmer Vernooij 12 年之前
父节点
当前提交
c81a2de98d

+ 12 - 0
.gitignore

@@ -0,0 +1,12 @@
+_trial_temp
+build
+MANIFEST
+dist
+apidocs
+*,cover
+.testrepository
+*.pyc
+*.so
+*~
+*.swp
+docs/tutorial/index.html

+ 6 - 1
Makefile

@@ -8,6 +8,8 @@ TESTRUNNER ?= unittest2.__main__
 endif
 RUNTEST = PYTHONPATH=.:$(PYTHONPATH) $(PYTHON) -m $(TESTRUNNER)
 
+DESTDIR=/
+
 all: build
 
 doc:: pydoctor
@@ -20,7 +22,7 @@ build::
 	$(SETUP) build_ext -i
 
 install::
-	$(SETUP) install
+	$(SETUP) install --root="$(DESTDIR)"
 
 check:: build
 	$(RUNTEST) dulwich.tests.test_suite
@@ -31,6 +33,9 @@ check-tutorial:: build
 check-nocompat:: build
 	$(RUNTEST) dulwich.tests.nocompat_test_suite
 
+check-compat:: build
+	$(RUNTEST) dulwich.tests.compat_test_suite
+
 check-pypy:: clean
 	$(MAKE) check-noextensions PYTHON=pypy
 

+ 43 - 1
NEWS

@@ -1,3 +1,45 @@
+0.8.6	2012-11-09
+
+ API CHANGES
+
+  * dulwich.__init__ no longer imports client, protocol, repo and
+    server modules. (Jelmer Vernooij)
+
+ FEATURES
+
+  * ConfigDict now behaves more like a dictionary.
+    (Adam 'Cezar' Jenkins, issue #58)
+
+  * HTTPGitApplication now takes an optional
+    `fallback_app` argument. (Jonas Haag, issue #67)
+
+  * Support for large pack index files. (Jameson Nash)
+
+ TESTING
+
+  * Make index entry tests a little bit less strict, to cope with
+    slightly different behaviour on various platforms.
+    (Jelmer Vernooij)
+
+  * ``setup.py test`` (available when setuptools is installed) now
+    runs all tests, not just the basic unit tests.
+    (Jelmer Vernooij)
+
+ BUG FIXES
+
+  * Commit._deserialize now actually deserializes the current state rather than
+    the previous one. (Yifan Zhang, issue #59)
+
+  * Handle None elements in lists of TreeChange objects. (Alex Holmes)
+
+  * Support cloning repositories without HEAD set.
+    (D-Key, Jelmer Vernooij, issue #69)
+
+  * Support ``MemoryRepo.get_config``. (Jelmer Vernooij)
+
+  * In ``get_transport_and_path``, pass extra keyword arguments on to
+    HttpGitClient. (Jelmer Vernooij)
+
 0.8.5	2012-03-29
 
  BUG FIXES
@@ -50,7 +92,7 @@
 
   * ``Repo.do_commit`` will now use the user identity from
     .git/config or ~/.gitconfig if none was explicitly specified.
-   (Jelmer Vernooij)
+    (Jelmer Vernooij)
 
  BUG FIXES
 

+ 3 - 0
bin/dulwich

@@ -65,6 +65,9 @@ def cmd_fetch(args):
     if "--all" in opts:
         determine_wants = r.object_store.determine_wants_all
     refs = client.fetch(path, r, progress=sys.stdout.write)
+    print "Remote refs:"
+    for item in refs.iteritems():
+        print "%s -> %s" % item
 
 
 def cmd_log(args):

+ 6 - 0
debian/changelog

@@ -1,3 +1,9 @@
+dulwich (0.8.6-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+
+ -- Jelmer Vernooij <jelmer@debian.org>  Fri, 09 Nov 2012 23:44:30 +0100
+
 dulwich (0.8.5-2) unstable; urgency=low
 
   * Make index tests a bit less strict, fixes FTBFS on some systems.

+ 1 - 3
dulwich/__init__.py

@@ -21,6 +21,4 @@
 
 """Python implementation of the Git file formats and protocols."""
 
-from dulwich import (client, protocol, repo, server)
-
-__version__ = (0, 8, 5)
+__version__ = (0, 8, 6)

+ 9 - 18
dulwich/_objects.c

@@ -56,27 +56,21 @@ static PyObject *py_parse_tree(PyObject *self, PyObject *args, PyObject *kw)
 {
 	char *text, *start, *end;
 	int len, namelen, strict;
-	PyObject *ret, *item, *name, *py_strict = NULL;
+	PyObject *ret, *item, *name, *sha, *py_strict = NULL;
 	static char *kwlist[] = {"text", "strict", NULL};
 
 	if (!PyArg_ParseTupleAndKeywords(args, kw, "s#|O", kwlist,
 	                                 &text, &len, &py_strict))
 		return NULL;
-
-
 	strict = py_strict ?  PyObject_IsTrue(py_strict) : 0;
-
 	/* TODO: currently this returns a list; if memory usage is a concern,
 	 * consider rewriting as a custom iterator object */
 	ret = PyList_New(0);
-
 	if (ret == NULL) {
 		return NULL;
 	}
-
 	start = text;
 	end = text + len;
-
 	while (text < end) {
 		long mode;
 		if (strict && text[0] == '0') {
@@ -85,36 +79,35 @@ static PyObject *py_parse_tree(PyObject *self, PyObject *args, PyObject *kw)
 			Py_DECREF(ret);
 			return NULL;
 		}
-
 		mode = strtol(text, &text, 8);
-
 		if (*text != ' ') {
 			PyErr_SetString(PyExc_ValueError, "Expected space");
 			Py_DECREF(ret);
 			return NULL;
 		}
-
 		text++;
-
 		namelen = strnlen(text, len - (text - start));
-
 		name = PyString_FromStringAndSize(text, namelen);
 		if (name == NULL) {
 			Py_DECREF(ret);
 			return NULL;
 		}
-
 		if (text + namelen + 20 >= end) {
 			PyErr_SetString(PyExc_ValueError, "SHA truncated");
 			Py_DECREF(ret);
 			Py_DECREF(name);
 			return NULL;
 		}
-
-		item = Py_BuildValue("(NlN)", name, mode,
-		                     sha_to_pyhex((unsigned char *)text+namelen+1));
+		sha = sha_to_pyhex((unsigned char *)text+namelen+1);
+		if (sha == NULL) {
+			Py_DECREF(ret);
+			Py_DECREF(name);
+			return NULL;
+		}
+		item = Py_BuildValue("(NlN)", name, mode, sha); 
 		if (item == NULL) {
 			Py_DECREF(ret);
+			Py_DECREF(sha);
 			Py_DECREF(name);
 			return NULL;
 		}
@@ -124,10 +117,8 @@ static PyObject *py_parse_tree(PyObject *self, PyObject *args, PyObject *kw)
 			return NULL;
 		}
 		Py_DECREF(item);
-
 		text += namelen+21;
 	}
-
 	return ret;
 }
 

+ 90 - 85
dulwich/_pack.c

@@ -22,13 +22,13 @@
 
 static int py_is_sha(PyObject *sha)
 {
-    if (!PyString_CheckExact(sha))
-        return 0;
+	if (!PyString_CheckExact(sha))
+		return 0;
 
-    if (PyString_Size(sha) != 20)
-        return 0;
+	if (PyString_Size(sha) != 20)
+		return 0;
 
-    return 1;
+	return 1;
 }
 
 
@@ -79,37 +79,37 @@ static PyObject *py_apply_delta(PyObject *self, PyObject *args)
 	size_t outindex = 0;
 	int index;
 	uint8_t *out;
-	PyObject *ret, *py_src_buf, *py_delta;
+	PyObject *ret, *py_src_buf, *py_delta, *ret_list;
 
 	if (!PyArg_ParseTuple(args, "OO", &py_src_buf, &py_delta))
 		return NULL;
 
-    py_src_buf = py_chunked_as_string(py_src_buf);
-    if (py_src_buf == NULL)
-        return NULL;
+	py_src_buf = py_chunked_as_string(py_src_buf);
+	if (py_src_buf == NULL)
+		return NULL;
 
-    py_delta = py_chunked_as_string(py_delta);
-    if (py_delta == NULL) {
-        Py_DECREF(py_src_buf);
-        return NULL;
-    }
+	py_delta = py_chunked_as_string(py_delta);
+	if (py_delta == NULL) {
+		Py_DECREF(py_src_buf);
+		return NULL;
+	}
 
 	src_buf = (uint8_t *)PyString_AS_STRING(py_src_buf);
 	src_buf_len = PyString_GET_SIZE(py_src_buf);
 
-    delta = (uint8_t *)PyString_AS_STRING(py_delta);
-    delta_len = PyString_GET_SIZE(py_delta);
+	delta = (uint8_t *)PyString_AS_STRING(py_delta);
+	delta_len = PyString_GET_SIZE(py_delta);
 
-    index = 0;
-    src_size = get_delta_header_size(delta, &index, delta_len);
-    if (src_size != src_buf_len) {
+	index = 0;
+	src_size = get_delta_header_size(delta, &index, delta_len);
+	if (src_size != src_buf_len) {
 		PyErr_Format(PyExc_ValueError, 
-			"Unexpected source buffer size: %lu vs %d", src_size, src_buf_len);
+					 "Unexpected source buffer size: %lu vs %d", src_size, src_buf_len);
 		Py_DECREF(py_src_buf);
 		Py_DECREF(py_delta);
 		return NULL;
 	}
-    dest_size = get_delta_header_size(delta, &index, delta_len);
+	dest_size = get_delta_header_size(delta, &index, delta_len);
 	ret = PyString_FromStringAndSize(NULL, dest_size);
 	if (ret == NULL) {
 		PyErr_NoMemory();
@@ -118,107 +118,112 @@ static PyObject *py_apply_delta(PyObject *self, PyObject *args)
 		return NULL;
 	}
 	out = (uint8_t *)PyString_AsString(ret);
-    while (index < delta_len) {
-        char cmd = delta[index];
-        index++;
-        if (cmd & 0x80) {
-            size_t cp_off = 0, cp_size = 0;
+	while (index < delta_len) {
+		char cmd = delta[index];
+		index++;
+		if (cmd & 0x80) {
+			size_t cp_off = 0, cp_size = 0;
 			int i;
-            for (i = 0; i < 4; i++) {
-                if (cmd & (1 << i)) {
-                    uint8_t x = delta[index];
-                    index++;
-                    cp_off |= x << (i * 8);
+			for (i = 0; i < 4; i++) {
+				if (cmd & (1 << i)) {
+					uint8_t x = delta[index];
+					index++;
+					cp_off |= x << (i * 8);
 				}
 			}
-            for (i = 0; i < 3; i++) {
-                if (cmd & (1 << (4+i))) {
-                    uint8_t x = delta[index];
-                    index++;
-                    cp_size |= x << (i * 8);
+			for (i = 0; i < 3; i++) {
+				if (cmd & (1 << (4+i))) {
+					uint8_t x = delta[index];
+					index++;
+					cp_size |= x << (i * 8);
 				}
 			}
-            if (cp_size == 0)
-                cp_size = 0x10000;
-            if (cp_off + cp_size < cp_size ||
-                cp_off + cp_size > src_size ||
-                cp_size > dest_size)
-                break;
+			if (cp_size == 0)
+				cp_size = 0x10000;
+			if (cp_off + cp_size < cp_size ||
+				cp_off + cp_size > src_size ||
+				cp_size > dest_size)
+				break;
 			memcpy(out+outindex, src_buf+cp_off, cp_size);
 			outindex += cp_size;
 		} else if (cmd != 0) {
 			memcpy(out+outindex, delta+index, cmd);
 			outindex += cmd;
-            index += cmd;
+			index += cmd;
 		} else {
 			PyErr_SetString(PyExc_ValueError, "Invalid opcode 0");
 			Py_DECREF(ret);
-            Py_DECREF(py_delta);
+			Py_DECREF(py_delta);
 			Py_DECREF(py_src_buf);
 			return NULL;
 		}
 	}
 	Py_DECREF(py_src_buf);
-    Py_DECREF(py_delta);
-    
-    if (index != delta_len) {
+	Py_DECREF(py_delta);
+
+	if (index != delta_len) {
 		PyErr_SetString(PyExc_ValueError, "delta not empty");
 		Py_DECREF(ret);
 		return NULL;
 	}
 
 	if (dest_size != outindex) {
-        PyErr_SetString(PyExc_ValueError, "dest size incorrect");
+		PyErr_SetString(PyExc_ValueError, "dest size incorrect");
 		Py_DECREF(ret);
 		return NULL;
 	}
 
-    return Py_BuildValue("[N]", ret);
+	ret_list = Py_BuildValue("[N]", ret);
+	if (ret_list == NULL) {
+		Py_DECREF(ret);
+		return NULL;
+	}
+	return ret_list;
 }
 
 static PyObject *py_bisect_find_sha(PyObject *self, PyObject *args)
 {
-    PyObject *unpack_name;
-    char *sha;
-    int sha_len;
+	PyObject *unpack_name;
+	char *sha;
+	int sha_len;
 	int start, end;
-    if (!PyArg_ParseTuple(args, "iis#O", &start, &end, 
+	if (!PyArg_ParseTuple(args, "iis#O", &start, &end, 
 						  &sha, &sha_len, &unpack_name))
-        return NULL;
-
-    if (sha_len != 20) {
-        PyErr_SetString(PyExc_ValueError, "Sha is not 20 bytes long");
-        return NULL;
-    }
-    if (start > end) {
-        PyErr_SetString(PyExc_AssertionError, "start > end");
-        return NULL;
-    }
-
-    while (start <= end) {
-        PyObject *file_sha;
-        int i = (start + end)/2;
-        int cmp;
-        file_sha = PyObject_CallFunction(unpack_name, "i", i);
-        if (file_sha == NULL) {
-            return NULL;
-        }
-        if (!py_is_sha(file_sha)) {
-            PyErr_SetString(PyExc_TypeError, "unpack_name returned non-sha object");
+		return NULL;
+
+	if (sha_len != 20) {
+		PyErr_SetString(PyExc_ValueError, "Sha is not 20 bytes long");
+		return NULL;
+	}
+	if (start > end) {
+		PyErr_SetString(PyExc_AssertionError, "start > end");
+		return NULL;
+	}
+
+	while (start <= end) {
+		PyObject *file_sha;
+		int i = (start + end)/2;
+		int cmp;
+		file_sha = PyObject_CallFunction(unpack_name, "i", i);
+		if (file_sha == NULL) {
+			return NULL;
+		}
+		if (!py_is_sha(file_sha)) {
+			PyErr_SetString(PyExc_TypeError, "unpack_name returned non-sha object");
 			Py_DECREF(file_sha);
-            return NULL;
-        }
-        cmp = memcmp(PyString_AsString(file_sha), sha, 20);
+			return NULL;
+		}
+		cmp = memcmp(PyString_AsString(file_sha), sha, 20);
 		Py_DECREF(file_sha);
-        if (cmp < 0)
-            start = i + 1;
-        else if (cmp > 0)
-            end = i - 1;
-        else {
+		if (cmp < 0)
+			start = i + 1;
+		else if (cmp > 0)
+			end = i - 1;
+		else {
 			return PyInt_FromLong(i);
-        }
-    }
-    Py_RETURN_NONE;
+		}
+	}
+	Py_RETURN_NONE;
 }
 
 

+ 6 - 6
dulwich/client.py

@@ -75,7 +75,7 @@ def _fileno_can_read(fileno):
     return len(select.select([fileno], [], [], 0)[0]) > 0
 
 COMMON_CAPABILITIES = ['ofs-delta', 'side-band-64k']
-FETCH_CAPABILITIES = ['multi_ack', 'multi_ack_detailed'] + COMMON_CAPABILITIES
+FETCH_CAPABILITIES = ['thin-pack', 'multi_ack', 'multi_ack_detailed'] + COMMON_CAPABILITIES
 SEND_CAPABILITIES = ['report-status'] + COMMON_CAPABILITIES
 
 
@@ -155,8 +155,8 @@ class GitClient(object):
         self._report_activity = report_activity
         self._fetch_capabilities = set(FETCH_CAPABILITIES)
         self._send_capabilities = set(SEND_CAPABILITIES)
-        if thin_packs:
-            self._fetch_capabilities.add('thin-pack')
+        if not thin_packs:
+            self._fetch_capabilities.remove('thin-pack')
 
     def _read_refs(self, proto):
         server_capabilities = None
@@ -440,7 +440,7 @@ class TraditionalGitClient(GitClient):
         old_refs, server_capabilities = self._read_refs(proto)
         negotiated_capabilities = self._send_capabilities & server_capabilities
         try:
-            new_refs = determine_wants(old_refs)
+            new_refs = determine_wants(dict(old_refs))
         except:
             proto.write_pkt_line(None)
             raise
@@ -707,7 +707,7 @@ class HttpGitClient(GitClient):
         old_refs, server_capabilities = self._discover_references(
             "git-receive-pack", url)
         negotiated_capabilities = self._send_capabilities & server_capabilities
-        new_refs = determine_wants(old_refs)
+        new_refs = determine_wants(dict(old_refs))
         if new_refs is None:
             return old_refs
         if self.dumb:
@@ -779,7 +779,7 @@ def get_transport_and_path(uri, **kwargs):
         return SSHGitClient(parsed.hostname, port=parsed.port,
                             username=parsed.username, **kwargs), parsed.path
     elif parsed.scheme in ('http', 'https'):
-        return HttpGitClient(urlparse.urlunparse(parsed)), parsed.path
+        return HttpGitClient(urlparse.urlunparse(parsed), **kwargs), parsed.path
 
     if parsed.scheme and not parsed.netloc:
         # SSH with no user@, zero or one leading slash.

+ 12 - 1
dulwich/config.py

@@ -28,6 +28,8 @@ import errno
 import os
 import re
 
+from UserDict import DictMixin
+
 from dulwich.file import GitFile
 
 
@@ -73,7 +75,7 @@ class Config(object):
         raise NotImplementedError(self.set)
 
 
-class ConfigDict(Config):
+class ConfigDict(Config, DictMixin):
     """Git configuration stored in a dictionary."""
 
     def __init__(self, values=None):
@@ -90,6 +92,15 @@ class ConfigDict(Config):
             isinstance(other, self.__class__) and
             other._values == self._values)
 
+    def __getitem__(self, key):
+        return self._values[key]
+      
+    def __setitem__(self, key, value):
+        self._values[key] = value
+        
+    def keys(self):
+        return self._values.keys()
+
     @classmethod
     def _parse_setting(cls, name):
         parts = name.split(".")

+ 2 - 1
dulwich/diff_tree.py

@@ -228,7 +228,8 @@ def tree_changes_for_merge(store, parent_tree_ids, tree_id,
     :param tree_id: The SHA of the merge tree.
     :param rename_detector: RenameDetector object for detecting renames.
 
-    :yield: Lists of TreeChange objects, one per conflicted path in the merge.
+    :return: Iterator over lists of TreeChange objects, one per conflicted path
+        in the merge.
 
         Each list contains one element per parent, with the TreeChange for that
         path relative to that parent. An element may be None if it never existed

+ 1 - 1
dulwich/object_store.py

@@ -623,7 +623,7 @@ class MemoryObjectStore(BaseObjectStore):
         elif len(sha) == 20:
             return sha_to_hex(sha)
         else:
-            raise ValueError("Invalid sha %r" % sha)
+            raise ValueError("Invalid sha %r" % (sha,))
 
     def contains_loose(self, sha):
         """Check if a particular object is present by SHA1 and is loose."""

+ 1 - 1
dulwich/objects.py

@@ -1059,7 +1059,7 @@ class Commit(ShaFile):
         self._parents = []
         self._extra = []
         self._author = None
-        for field, value in parse_commit(''.join(self._chunked_text)):
+        for field, value in parse_commit(''.join(chunks)):
             if field == _TREE_HEADER:
                 self._tree = value
             elif field == _PARENT_HEADER:

+ 19 - 8
dulwich/pack.py

@@ -615,6 +615,8 @@ class PackIndex2(FilePackIndex):
         self._crc32_table_offset = self._name_table_offset + 20 * len(self)
         self._pack_offset_table_offset = (self._crc32_table_offset +
                                           4 * len(self))
+        self._pack_offset_largetable_offset = (self._pack_offset_table_offset +
+                                          4 * len(self))
 
     def _unpack_entry(self, i):
         return (self._unpack_name(i), self._unpack_offset(i),
@@ -626,7 +628,11 @@ class PackIndex2(FilePackIndex):
 
     def _unpack_offset(self, i):
         offset = self._pack_offset_table_offset + i * 4
-        return unpack_from('>L', self._contents, offset)[0]
+        offset = unpack_from('>L', self._contents, offset)[0]
+        if offset & (2**31):
+            offset = self._pack_offset_largetable_offset + (offset&(2**31-1)) * 8L
+            offset = unpack_from('>Q', self._contents, offset)[0]
+        return offset
 
     def _unpack_crc32_checksum(self, i):
         return unpack_from('>L', self._contents,
@@ -1036,8 +1042,8 @@ class PackData(object):
         if type == OFS_DELTA:
             (delta_offset, delta) = obj
             # TODO: clean up asserts and replace with nicer error messages
-            assert isinstance(offset, int)
-            assert isinstance(delta_offset, int)
+            assert isinstance(offset, int) or isinstance(offset, long)
+            assert isinstance(delta_offset, int) or isinstance(offset, long)
             base_offset = offset-delta_offset
             type, base_obj = self.get_object_at(base_offset)
             assert isinstance(type, int)
@@ -1560,6 +1566,8 @@ def write_pack_index_v1(f, entries, pack_checksum):
         f.write(struct.pack('>L', fan_out_table[i]))
         fan_out_table[i+1] += fan_out_table[i]
     for (name, offset, entry_checksum) in entries:
+        if not (offset <= 0xffffffff):
+            raise TypeError("pack format 1 only supports offsets < 2Gb")
         f.write(struct.pack('>L20s', offset, name))
     assert len(pack_checksum) == 20
     f.write(pack_checksum)
@@ -1707,6 +1715,7 @@ def write_pack_index_v2(f, entries, pack_checksum):
     for (name, offset, entry_checksum) in entries:
         fan_out_table[ord(name[0])] += 1
     # Fan-out table
+    largetable = []
     for i in range(0x100):
         f.write(struct.pack('>L', fan_out_table[i]))
         fan_out_table[i+1] += fan_out_table[i]
@@ -1715,9 +1724,13 @@ def write_pack_index_v2(f, entries, pack_checksum):
     for (name, offset, entry_checksum) in entries:
         f.write(struct.pack('>L', entry_checksum))
     for (name, offset, entry_checksum) in entries:
-        # FIXME: handle if MSBit is set in offset
-        f.write(struct.pack('>L', offset))
-    # FIXME: handle table for pack files > 8 Gb
+        if offset < 2**31:
+            f.write(struct.pack('>L', offset))
+        else:
+            f.write(struct.pack('>L', 2**31 + len(largetable)))
+            largetable.append(offset)
+    for offset in largetable:
+        f.write(struct.pack('>Q', offset))
     assert len(pack_checksum) == 20
     f.write(pack_checksum)
     return f.write_sha()
@@ -1828,8 +1841,6 @@ class Pack(object):
     def get_raw(self, sha1):
         offset = self.index.object_index(sha1)
         obj_type, obj = self.data.get_object_at(offset)
-        if type(offset) is long:
-          offset = int(offset)
         type_num, chunks = self.data.resolve_object(offset, obj_type, obj)
         return type_num, ''.join(chunks)
 

+ 42 - 18
dulwich/repo.py

@@ -967,16 +967,7 @@ class BaseRepo(object):
 
         :return: `ConfigFile` object for the ``.git/config`` file.
         """
-        from dulwich.config import ConfigFile
-        path = os.path.join(self._controldir, 'config')
-        try:
-            return ConfigFile.from_path(path)
-        except (IOError, OSError), e:
-            if e.errno != errno.ENOENT:
-                raise
-            ret = ConfigFile()
-            ret.path = path
-            return ret
+        raise NotImplementedError(self.get_config)
 
     def get_config_stack(self):
         """Return a config stack for this repository.
@@ -1250,8 +1241,16 @@ class Repo(BaseRepo):
               os.path.isdir(os.path.join(root, REFSDIR))):
             self.bare = True
             self._controldir = root
+        elif (os.path.isfile(os.path.join(root, ".git"))):
+            import re
+            with open(os.path.join(root, ".git"), 'r') as f:
+                _, path = re.match('(gitdir: )(.+$)', f.read()).groups()
+            self.bare = False
+            self._controldir = os.path.join(root, path)
         else:
-            raise NotGitRepository(root)
+            raise NotGitRepository(
+                "No git repository was found at %(path)s" % dict(path=root)
+            )
         self.path = root
         object_store = DiskObjectStore(os.path.join(self.controldir(),
                                                     OBJECTDIR))
@@ -1375,17 +1374,34 @@ class Repo(BaseRepo):
 
         # Update target head
         head, head_sha = self.refs._follow('HEAD')
-        target.refs.set_symbolic_ref('HEAD', head)
-        target['HEAD'] = head_sha
+        if head is not None and head_sha is not None:
+            target.refs.set_symbolic_ref('HEAD', head)
+            target['HEAD'] = head_sha
 
-        if not bare:
-            # Checkout HEAD to target dir
-            from dulwich.index import build_index_from_tree
-            build_index_from_tree(target.path, target.index_path(),
-                    target.object_store, target['HEAD'].tree)
+            if not bare:
+                # Checkout HEAD to target dir
+                from dulwich.index import build_index_from_tree
+                build_index_from_tree(target.path, target.index_path(),
+                        target.object_store, target['HEAD'].tree)
 
         return target
 
+    def get_config(self):
+        """Retrieve the config object.
+
+        :return: `ConfigFile` object for the ``.git/config`` file.
+        """
+        from dulwich.config import ConfigFile
+        path = os.path.join(self._controldir, 'config')
+        try:
+            return ConfigFile.from_path(path)
+        except (IOError, OSError), e:
+            if e.errno != errno.ENOENT:
+                raise
+            ret = ConfigFile()
+            ret.path = path
+            return ret
+
     def __repr__(self):
         return "<Repo at %r>" % self.path
 
@@ -1470,6 +1486,14 @@ class MemoryRepo(BaseRepo):
         """
         raise NoIndexPresent()
 
+    def get_config(self):
+        """Retrieve the config object.
+
+        :return: `ConfigFile` object.
+        """
+        from dulwich.config import ConfigFile
+        return ConfigFile()
+
     @classmethod
     def init_bare(cls, objects, refs):
         """Create a new bare repository in memory.

+ 5 - 4
dulwich/server.py

@@ -149,8 +149,9 @@ class DictBackend(Backend):
         try:
             return self.repos[path]
         except KeyError:
-            raise NotGitRepository("No git repository was found at %(path)s",
-                path=path)
+            raise NotGitRepository(
+                "No git repository was found at %(path)s" % dict(path=path)
+            )
 
 
 class FileSystemBackend(Backend):
@@ -618,8 +619,8 @@ class ReceivePackHandler(Handler):
         status = []
         # TODO: more informative error messages than just the exception string
         try:
-            p = self.repo.object_store.add_thin_pack(self.proto.read,
-                                                     self.proto.recv)
+            recv = getattr(self.proto, "recv", None)
+            p = self.repo.object_store.add_thin_pack(self.proto.read, recv)
             status.append(('unpack', 'ok'))
         except all_exceptions, e:
             status.append(('unpack', str(e).replace('\n', '')))

+ 7 - 0
dulwich/tests/__init__.py

@@ -161,6 +161,13 @@ def nocompat_test_suite():
     return result
 
 
+def compat_test_suite():
+    result = unittest.TestSuite()
+    from dulwich.tests.compat import test_suite as compat_test_suite
+    result.addTests(compat_test_suite())
+    return result
+
+
 def test_suite():
     result = unittest.TestSuite()
     result.addTests(self_test_suite())

+ 8 - 1
dulwich/tests/compat/server_utils.py

@@ -19,7 +19,7 @@
 
 """Utilities for testing git server compatibility."""
 
-
+import errno
 import os
 import select
 import shutil
@@ -200,3 +200,10 @@ class NoSideBand64kReceivePackHandler(ReceivePackHandler):
     def capabilities(cls):
         return tuple(c for c in ReceivePackHandler.capabilities()
                      if c != 'side-band-64k')
+
+
+def ignore_error((e_type, e_value, e_tb)):
+    """Check whether this error is safe to ignore."""
+    return (issubclass(e_type, socket.error) and
+            e_value[0] in (errno.ECONNRESET, errno.EPIPE))
+

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

@@ -32,6 +32,7 @@ import tarfile
 import tempfile
 import threading
 import urllib
+from socket import gethostname
 
 from dulwich import (
     client,
@@ -425,6 +426,7 @@ class HTTPGitServer(BaseHTTPServer.HTTPServer):
     def __init__(self, server_address, root_path):
         BaseHTTPServer.HTTPServer.__init__(self, server_address, GitHTTPRequestHandler)
         self.root_path = root_path
+        self.server_name = "localhost"
 
     def get_url(self):
         return 'http://%s:%s/' % (self.server_name, self.server_port)

+ 1 - 0
dulwich/tests/data/repos/empty.git/HEAD

@@ -0,0 +1 @@
+ref: refs/heads/master

+ 7 - 0
dulwich/tests/data/repos/empty.git/config

@@ -0,0 +1,7 @@
+[core]
+	repositoryformatversion = 0
+	filemode = false
+	bare = true
+	symlinks = false
+	ignorecase = true
+	hideDotFiles = dotGitOnly

+ 2 - 0
dulwich/tests/data/repos/empty.git/objects/info/.gitignore

@@ -0,0 +1,2 @@
+*
+!.gitignore

+ 2 - 0
dulwich/tests/data/repos/empty.git/objects/pack/.gitignore

@@ -0,0 +1,2 @@
+*
+!.gitignore

+ 2 - 0
dulwich/tests/data/repos/empty.git/refs/heads/.gitignore

@@ -0,0 +1,2 @@
+*
+!.gitignore

+ 2 - 0
dulwich/tests/data/repos/empty.git/refs/tags/.gitignore

@@ -0,0 +1,2 @@
+*
+!.gitignore

+ 1 - 0
dulwich/tests/data/repos/submodule/dotgit

@@ -0,0 +1 @@
+gitdir: ./a.git

+ 12 - 1
dulwich/tests/test_config.py

@@ -16,7 +16,7 @@
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 # MA  02110-1301, USA.
 
-"""Tests for reading and writing configuraiton files."""
+"""Tests for reading and writing configuration files."""
 
 from cStringIO import StringIO
 from dulwich.config import (
@@ -170,6 +170,17 @@ class ConfigDictTests(TestCase):
         cd.set(("core", ), "foo", "invalid")
         self.assertRaises(ValueError, cd.get_boolean, ("core", ), "foo")
 
+    def test_dict(self):
+        cd = ConfigDict()
+        cd.set(("core", ), "foo", "bla")
+        cd.set(("core2", ), "foo", "bloe")
+
+        self.assertEqual([("core2", ), ("core", )], cd.keys())
+        self.assertEqual(cd[("core", )], {'foo': 'bla'})
+
+        cd['a'] = 'b'
+        self.assertEqual(cd['a'], 'b')
+
 
 class StackedConfigTests(TestCase):
 

+ 6 - 16
dulwich/tests/test_index.py

@@ -26,7 +26,6 @@ import os
 import shutil
 import stat
 import struct
-import sys
 import tempfile
 
 from dulwich.index import (
@@ -216,15 +215,8 @@ class IndexEntryFromStatTests(TestCase):
 
 class BuildIndexTests(TestCase):
 
-    def assertReasonableIndexEntry(self, index_entry, 
-            time, mode, filesize, sha):
-        delta = 1000000
-        self.assertEquals(index_entry[0], index_entry[1])  # ctime and atime
-        self.assertTrue(index_entry[0] > time - delta)
+    def assertReasonableIndexEntry(self, index_entry, mode, filesize, sha):
         self.assertEquals(index_entry[4], mode)  # mode
-        if sys.platform != 'nt':
-            self.assertEquals(index_entry[5], os.getuid())
-            self.assertEquals(index_entry[6], os.getgid())
         self.assertEquals(index_entry[7], filesize)  # filesize
         self.assertEquals(index_entry[8], sha)  # sha
 
@@ -258,7 +250,7 @@ class BuildIndexTests(TestCase):
 
     def test_nonempty(self):
         if os.name != 'posix':
-            self.skip("test depends on POSIX shell")
+            self.skipTest("test depends on POSIX shell")
 
         repo_dir = tempfile.mkdtemp()
         repo = Repo.init(repo_dir)
@@ -283,8 +275,6 @@ class BuildIndexTests(TestCase):
                 repo.object_store, tree.id)
 
         # Verify index entries
-        import time
-        ctime = time.time()
         index = repo.open_index()
         self.assertEquals(len(index), 4)
 
@@ -292,28 +282,28 @@ class BuildIndexTests(TestCase):
         apath = os.path.join(repo.path, 'a')
         self.assertTrue(os.path.exists(apath))
         self.assertReasonableIndexEntry(index['a'],
-            ctime, stat.S_IFREG | 0644, 6, filea.id)
+            stat.S_IFREG | 0644, 6, filea.id)
         self.assertFileContents(apath, 'file a')
 
         # fileb
         bpath = os.path.join(repo.path, 'b')
         self.assertTrue(os.path.exists(bpath))
         self.assertReasonableIndexEntry(index['b'],
-            ctime, stat.S_IFREG | 0644, 6, fileb.id)
+            stat.S_IFREG | 0644, 6, fileb.id)
         self.assertFileContents(bpath, 'file b')
 
         # filed
         dpath = os.path.join(repo.path, 'c', 'd')
         self.assertTrue(os.path.exists(dpath))
         self.assertReasonableIndexEntry(index['c/d'], 
-            ctime, stat.S_IFREG | 0644, 6, filed.id)
+            stat.S_IFREG | 0644, 6, filed.id)
         self.assertFileContents(dpath, 'file d')
 
         # symlink to d
         epath = os.path.join(repo.path, 'c', 'e')
         self.assertTrue(os.path.exists(epath))
         self.assertReasonableIndexEntry(index['c/e'], 
-            ctime, stat.S_IFLNK, 1, filee.id)
+            stat.S_IFLNK, 1, filee.id)
         self.assertFileContents(epath, 'd', symlink=True)
 
         # Verify no extra files

+ 6 - 0
dulwich/tests/test_objects.py

@@ -307,6 +307,12 @@ class CommitSerializationTests(TestCase):
         c = self.make_commit(commit_timezone=(-1 * 3600))
         self.assertTrue(" -0100\n" in c.as_raw_string())
 
+    def test_deserialize(self):
+        c = self.make_commit()
+        d = Commit()
+        d._deserialize(c.as_raw_chunks())
+        self.assertEqual(c, d)
+
 
 default_committer = 'James Westby <jw+debian@jameswestby.net> 1174773719 +0000'
 

+ 32 - 0
dulwich/tests/test_pack.py

@@ -162,6 +162,7 @@ class TestPackDeltas(TestCase):
 
     test_string_empty = ''
     test_string_big = 'Z' * 8192
+    test_string_huge = 'Z' * 100000
 
     def _test_roundtrip(self, base, target):
         self.assertEqual(target,
@@ -179,6 +180,10 @@ class TestPackDeltas(TestCase):
     def test_overflow(self):
         self._test_roundtrip(self.test_string_empty, self.test_string_big)
 
+    def test_overflow_64k(self):
+        self.skipTest("big strings don't work yet")
+        self._test_roundtrip(self.test_string_huge, self.test_string_huge)
+
 
 class TestPackData(PackTests):
     """Tests getting the data from the packfile."""
@@ -471,6 +476,30 @@ class BaseTestPackIndexWriting(object):
         self.assertEqual(idx.get_pack_checksum(), pack_checksum)
         self.assertEqual(0, len(idx))
 
+    def test_large(self):
+        entry1_sha = hex_to_sha('4e6388232ec39792661e2e75db8fb117fc869ce6')
+        entry2_sha = hex_to_sha('e98f071751bd77f59967bfa671cd2caebdccc9a2')
+        entries = [(entry1_sha, 0xf2972d0830529b87, 24),
+                   (entry2_sha, (~0xf2972d0830529b87)&(2**64-1), 92)]
+        if not self._supports_large:
+            self.assertRaises(TypeError, self.index, 'single.idx',
+                entries, pack_checksum)
+            return
+        idx = self.index('single.idx', entries, pack_checksum)
+        self.assertEqual(idx.get_pack_checksum(), pack_checksum)
+        self.assertEqual(2, len(idx))
+        actual_entries = list(idx.iterentries())
+        self.assertEqual(len(entries), len(actual_entries))
+        for mine, actual in zip(entries, actual_entries):
+            my_sha, my_offset, my_crc = mine
+            actual_sha, actual_offset, actual_crc = actual
+            self.assertEqual(my_sha, actual_sha)
+            self.assertEqual(my_offset, actual_offset)
+            if self._has_crc32_checksum:
+                self.assertEqual(my_crc, actual_crc)
+            else:
+                self.assertTrue(actual_crc is None)
+
     def test_single(self):
         entry_sha = hex_to_sha('6f670c0fb53f9463760b7295fbb814e965fb20c8')
         my_entries = [(entry_sha, 178, 42)]
@@ -520,6 +549,7 @@ class TestMemoryIndexWriting(TestCase, BaseTestPackIndexWriting):
     def setUp(self):
         TestCase.setUp(self)
         self._has_crc32_checksum = True
+        self._supports_large = True
 
     def index(self, filename, entries, pack_checksum):
         return MemoryPackIndex(entries, pack_checksum)
@@ -535,6 +565,7 @@ class TestPackIndexWritingv1(TestCase, BaseTestFilePackIndexWriting):
         BaseTestFilePackIndexWriting.setUp(self)
         self._has_crc32_checksum = False
         self._expected_version = 1
+        self._supports_large = False
         self._write_fn = write_pack_index_v1
 
     def tearDown(self):
@@ -548,6 +579,7 @@ class TestPackIndexWritingv2(TestCase, BaseTestFilePackIndexWriting):
         TestCase.setUp(self)
         BaseTestFilePackIndexWriting.setUp(self)
         self._has_crc32_checksum = True
+        self._supports_large = True
         self._expected_version = 2
         self._write_fn = write_pack_index_v2
 

+ 40 - 0
dulwich/tests/test_repository.py

@@ -286,6 +286,36 @@ class RepositoryTests(TestCase):
         self.assertEqual(shas, [t.head(),
                          '2a72d929692c41d8554c07f6301757ba18a65d91'])
 
+    def test_clone_no_head(self):
+        temp_dir = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, temp_dir)
+        repo_dir = os.path.join(os.path.dirname(__file__), 'data', 'repos')
+        dest_dir = os.path.join(temp_dir, 'a.git')
+        shutil.copytree(os.path.join(repo_dir, 'a.git'),
+                        dest_dir, symlinks=True)
+        r = Repo(dest_dir)
+        del r.refs["refs/heads/master"]
+        del r.refs["HEAD"]
+        t = r.clone(os.path.join(temp_dir, 'b.git'), mkdir=True)
+        self.assertEqual({
+            'refs/tags/mytag': '28237f4dc30d0d462658d6b937b08a0f0b6ef55a',
+            'refs/tags/mytag-packed':
+                'b0931cadc54336e78a1d980420e3268903b57a50',
+            }, t.refs.as_dict())
+
+    def test_clone_empty(self):
+        """Test clone() doesn't crash if HEAD points to a non-existing ref.
+
+        This simulates cloning server-side bare repository either when it is
+        still empty or if user renames master branch and pushes private repo
+        to the server.
+        Non-bare repo HEAD always points to an existing ref.
+        """
+        r = self._repo = open_repo('empty.git')
+        tmp_dir = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, tmp_dir)
+        r.clone(tmp_dir, mkdir=False, bare=True)
+
     def test_merge_history(self):
         r = self._repo = open_repo('simple_merge.git')
         shas = [e.commit.id for e in r.get_walker()]
@@ -323,6 +353,16 @@ class RepositoryTests(TestCase):
         r = self._repo = open_repo('ooo_merge.git')
         self.assertIsInstance(r.get_config_stack(), Config)
 
+    def test_submodule(self):
+        temp_dir = tempfile.mkdtemp()
+        repo_dir = os.path.join(os.path.dirname(__file__), 'data', 'repos')
+        shutil.copytree(os.path.join(repo_dir, 'a.git'),
+                        os.path.join(temp_dir, 'a.git'), symlinks=True)
+        rel = os.path.relpath(os.path.join(repo_dir, 'submodule'), temp_dir)
+        os.symlink(os.path.join(rel, 'dotgit'), os.path.join(temp_dir, '.git'))
+        r = Repo(temp_dir)
+        self.assertEqual(r.head(), 'a90fa2d900a17e99b433217e988c4eb4a2e9a097')
+
     def test_common_revisions(self):
         """
         This test demonstrates that ``find_common_revisions()`` actually returns

+ 7 - 0
dulwich/tests/test_server.py

@@ -666,6 +666,13 @@ class FileSystemBackendTests(TestCase):
         self.assertRaises(NotGitRepository,
             self.backend.open_repository, os.path.join(self.path, "foo"))
 
+    def test_bad_repo_path(self):
+        repo = MemoryRepo.init_bare([], {})
+        backend = DictBackend({'/': repo})
+
+        self.assertRaises(NotGitRepository,
+                          lambda: backend.open_repository('/ups'))
+
 
 class ServeCommandTests(TestCase):
     """Tests for serve_command."""

+ 4 - 0
dulwich/tests/test_walk.py

@@ -410,3 +410,7 @@ class WalkerTest(TestCase):
         # Ensure that c1..y4 get excluded even though they're popped from the
         # priority queue long before y5.
         self.assertWalkYields([m6, x2], [m6.id], exclude=[y5.id])
+
+    def test_empty_walk(self):
+        c1, c2, c3 = self.make_linear_commits(3)
+        self.assertWalkYields([], [c3.id], exclude=[c3.id])

+ 7 - 0
dulwich/tests/test_web.py

@@ -443,6 +443,13 @@ class HTTPGitApplicationTestCase(TestCase):
         self._add_handler(self._app)
         self.assertEqual('output', self._app(self._environ, None))
 
+    def test_fallback_app(self):
+        def test_app(environ, start_response):
+            return 'output'
+
+        app = HTTPGitApplication('backend', fallback_app=test_app)
+        self.assertEqual('output', app(self._environ, None))
+
 
 class GunzipTestCase(HTTPGitApplicationTestCase):
     """TestCase for testing the GunzipFilter, ensuring the wsgi.input

+ 6 - 3
dulwich/walk.py

@@ -153,7 +153,7 @@ class _CommitTimeQueue(object):
                 if self._pq and all(c.id in self._excluded
                                     for _, c in self._pq):
                     _, n = self._pq[0]
-                    if n.commit_time >= self._last.commit_time:
+                    if self._last and n.commit_time >= self._last.commit_time:
                         # If the next commit is newer than the last one, we need
                         # to keep walking in case its parents (which we may not
                         # have seen yet) are excluded. This gives the excluded
@@ -255,6 +255,9 @@ class Walker(object):
         return False
 
     def _change_matches(self, change):
+        if not change:
+            return False
+
         old_path = change.old.path
         new_path = change.new.path
         if self._path_matches(new_path):
@@ -338,8 +341,8 @@ def _topo_reorder(entries):
     order, e.g. in commit time order.
 
     :param entries: An iterable of WalkEntry objects.
-    :yield: WalkEntry objects from entries in FIFO order, except where a parent
-        would be yielded before any of its children.
+    :return: iterator over WalkEntry objects from entries in FIFO order, except
+        where a parent would be yielded before any of its children.
     """
     todo = collections.deque()
     pending = {}

+ 10 - 4
dulwich/web.py

@@ -318,10 +318,11 @@ class HTTPGitApplication(object):
       ('POST', re.compile('/git-receive-pack$')): handle_service_request,
     }
 
-    def __init__(self, backend, dumb=False, handlers=None):
+    def __init__(self, backend, dumb=False, handlers=None, fallback_app=None):
         self.backend = backend
         self.dumb = dumb
         self.handlers = dict(DEFAULT_HANDLERS)
+        self.fallback_app = fallback_app
         if handlers is not None:
             self.handlers.update(handlers)
 
@@ -339,8 +340,13 @@ class HTTPGitApplication(object):
             if mat:
                 handler = self.services[smethod, spath]
                 break
+
         if handler is None:
-            return req.not_found('Sorry, that method is not supported')
+            if self.fallback_app is not None:
+                return self.fallback_app(environ, start_response)
+            else:
+                return req.not_found('Sorry, that method is not supported')
+
         return handler(req, self.backend, mat)
 
 
@@ -382,11 +388,11 @@ class LimitedInputFilter(object):
         return self.app(environ, start_response)
 
 
-def make_wsgi_chain(backend, dumb=False, handlers=None):
+def make_wsgi_chain(*args, **kwargs):
     """Factory function to create an instance of HTTPGitApplication,
     correctly wrapped with needed middleware.
     """
-    app = HTTPGitApplication(backend, dumb, handlers)
+    app = HTTPGitApplication(*args, **kwargs)
     wrapped_app = GunzipFilter(LimitedInputFilter(app))
     return wrapped_app
 

+ 2 - 2
setup.py

@@ -10,7 +10,7 @@ except ImportError:
     has_setuptools = False
 from distutils.core import Distribution
 
-dulwich_version_string = '0.8.5'
+dulwich_version_string = '0.8.6'
 
 include_dirs = []
 # Windows MSVC support
@@ -52,7 +52,7 @@ if sys.platform == 'darwin' and os.path.exists('/usr/bin/xcodebuild'):
 setup_kwargs = {}
 
 if has_setuptools:
-    setup_kwargs['test_suite'] = 'dulwich.tests'
+    setup_kwargs['test_suite'] = 'dulwich.tests.test_suite'
 
 setup(name='dulwich',
       description='Python Git Library',