瀏覽代碼

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
 endif
 RUNTEST = PYTHONPATH=.:$(PYTHONPATH) $(PYTHON) -m $(TESTRUNNER)
 RUNTEST = PYTHONPATH=.:$(PYTHONPATH) $(PYTHON) -m $(TESTRUNNER)
 
 
+DESTDIR=/
+
 all: build
 all: build
 
 
 doc:: pydoctor
 doc:: pydoctor
@@ -20,7 +22,7 @@ build::
 	$(SETUP) build_ext -i
 	$(SETUP) build_ext -i
 
 
 install::
 install::
-	$(SETUP) install
+	$(SETUP) install --root="$(DESTDIR)"
 
 
 check:: build
 check:: build
 	$(RUNTEST) dulwich.tests.test_suite
 	$(RUNTEST) dulwich.tests.test_suite
@@ -31,6 +33,9 @@ check-tutorial:: build
 check-nocompat:: build
 check-nocompat:: build
 	$(RUNTEST) dulwich.tests.nocompat_test_suite
 	$(RUNTEST) dulwich.tests.nocompat_test_suite
 
 
+check-compat:: build
+	$(RUNTEST) dulwich.tests.compat_test_suite
+
 check-pypy:: clean
 check-pypy:: clean
 	$(MAKE) check-noextensions PYTHON=pypy
 	$(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
 0.8.5	2012-03-29
 
 
  BUG FIXES
  BUG FIXES
@@ -50,7 +92,7 @@
 
 
   * ``Repo.do_commit`` will now use the user identity from
   * ``Repo.do_commit`` will now use the user identity from
     .git/config or ~/.gitconfig if none was explicitly specified.
     .git/config or ~/.gitconfig if none was explicitly specified.
-   (Jelmer Vernooij)
+    (Jelmer Vernooij)
 
 
  BUG FIXES
  BUG FIXES
 
 

+ 3 - 0
bin/dulwich

@@ -65,6 +65,9 @@ def cmd_fetch(args):
     if "--all" in opts:
     if "--all" in opts:
         determine_wants = r.object_store.determine_wants_all
         determine_wants = r.object_store.determine_wants_all
     refs = client.fetch(path, r, progress=sys.stdout.write)
     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):
 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
 dulwich (0.8.5-2) unstable; urgency=low
 
 
   * Make index tests a bit less strict, fixes FTBFS on some systems.
   * 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."""
 """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;
 	char *text, *start, *end;
 	int len, namelen, strict;
 	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};
 	static char *kwlist[] = {"text", "strict", NULL};
 
 
 	if (!PyArg_ParseTupleAndKeywords(args, kw, "s#|O", kwlist,
 	if (!PyArg_ParseTupleAndKeywords(args, kw, "s#|O", kwlist,
 	                                 &text, &len, &py_strict))
 	                                 &text, &len, &py_strict))
 		return NULL;
 		return NULL;
-
-
 	strict = py_strict ?  PyObject_IsTrue(py_strict) : 0;
 	strict = py_strict ?  PyObject_IsTrue(py_strict) : 0;
-
 	/* TODO: currently this returns a list; if memory usage is a concern,
 	/* TODO: currently this returns a list; if memory usage is a concern,
 	 * consider rewriting as a custom iterator object */
 	 * consider rewriting as a custom iterator object */
 	ret = PyList_New(0);
 	ret = PyList_New(0);
-
 	if (ret == NULL) {
 	if (ret == NULL) {
 		return NULL;
 		return NULL;
 	}
 	}
-
 	start = text;
 	start = text;
 	end = text + len;
 	end = text + len;
-
 	while (text < end) {
 	while (text < end) {
 		long mode;
 		long mode;
 		if (strict && text[0] == '0') {
 		if (strict && text[0] == '0') {
@@ -85,36 +79,35 @@ static PyObject *py_parse_tree(PyObject *self, PyObject *args, PyObject *kw)
 			Py_DECREF(ret);
 			Py_DECREF(ret);
 			return NULL;
 			return NULL;
 		}
 		}
-
 		mode = strtol(text, &text, 8);
 		mode = strtol(text, &text, 8);
-
 		if (*text != ' ') {
 		if (*text != ' ') {
 			PyErr_SetString(PyExc_ValueError, "Expected space");
 			PyErr_SetString(PyExc_ValueError, "Expected space");
 			Py_DECREF(ret);
 			Py_DECREF(ret);
 			return NULL;
 			return NULL;
 		}
 		}
-
 		text++;
 		text++;
-
 		namelen = strnlen(text, len - (text - start));
 		namelen = strnlen(text, len - (text - start));
-
 		name = PyString_FromStringAndSize(text, namelen);
 		name = PyString_FromStringAndSize(text, namelen);
 		if (name == NULL) {
 		if (name == NULL) {
 			Py_DECREF(ret);
 			Py_DECREF(ret);
 			return NULL;
 			return NULL;
 		}
 		}
-
 		if (text + namelen + 20 >= end) {
 		if (text + namelen + 20 >= end) {
 			PyErr_SetString(PyExc_ValueError, "SHA truncated");
 			PyErr_SetString(PyExc_ValueError, "SHA truncated");
 			Py_DECREF(ret);
 			Py_DECREF(ret);
 			Py_DECREF(name);
 			Py_DECREF(name);
 			return NULL;
 			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) {
 		if (item == NULL) {
 			Py_DECREF(ret);
 			Py_DECREF(ret);
+			Py_DECREF(sha);
 			Py_DECREF(name);
 			Py_DECREF(name);
 			return NULL;
 			return NULL;
 		}
 		}
@@ -124,10 +117,8 @@ static PyObject *py_parse_tree(PyObject *self, PyObject *args, PyObject *kw)
 			return NULL;
 			return NULL;
 		}
 		}
 		Py_DECREF(item);
 		Py_DECREF(item);
-
 		text += namelen+21;
 		text += namelen+21;
 	}
 	}
-
 	return ret;
 	return ret;
 }
 }
 
 

+ 90 - 85
dulwich/_pack.c

@@ -22,13 +22,13 @@
 
 
 static int py_is_sha(PyObject *sha)
 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;
 	size_t outindex = 0;
 	int index;
 	int index;
 	uint8_t *out;
 	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))
 	if (!PyArg_ParseTuple(args, "OO", &py_src_buf, &py_delta))
 		return NULL;
 		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 = (uint8_t *)PyString_AS_STRING(py_src_buf);
 	src_buf_len = PyString_GET_SIZE(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, 
 		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_src_buf);
 		Py_DECREF(py_delta);
 		Py_DECREF(py_delta);
 		return NULL;
 		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);
 	ret = PyString_FromStringAndSize(NULL, dest_size);
 	if (ret == NULL) {
 	if (ret == NULL) {
 		PyErr_NoMemory();
 		PyErr_NoMemory();
@@ -118,107 +118,112 @@ static PyObject *py_apply_delta(PyObject *self, PyObject *args)
 		return NULL;
 		return NULL;
 	}
 	}
 	out = (uint8_t *)PyString_AsString(ret);
 	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;
 			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);
 			memcpy(out+outindex, src_buf+cp_off, cp_size);
 			outindex += cp_size;
 			outindex += cp_size;
 		} else if (cmd != 0) {
 		} else if (cmd != 0) {
 			memcpy(out+outindex, delta+index, cmd);
 			memcpy(out+outindex, delta+index, cmd);
 			outindex += cmd;
 			outindex += cmd;
-            index += cmd;
+			index += cmd;
 		} else {
 		} else {
 			PyErr_SetString(PyExc_ValueError, "Invalid opcode 0");
 			PyErr_SetString(PyExc_ValueError, "Invalid opcode 0");
 			Py_DECREF(ret);
 			Py_DECREF(ret);
-            Py_DECREF(py_delta);
+			Py_DECREF(py_delta);
 			Py_DECREF(py_src_buf);
 			Py_DECREF(py_src_buf);
 			return NULL;
 			return NULL;
 		}
 		}
 	}
 	}
 	Py_DECREF(py_src_buf);
 	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");
 		PyErr_SetString(PyExc_ValueError, "delta not empty");
 		Py_DECREF(ret);
 		Py_DECREF(ret);
 		return NULL;
 		return NULL;
 	}
 	}
 
 
 	if (dest_size != outindex) {
 	if (dest_size != outindex) {
-        PyErr_SetString(PyExc_ValueError, "dest size incorrect");
+		PyErr_SetString(PyExc_ValueError, "dest size incorrect");
 		Py_DECREF(ret);
 		Py_DECREF(ret);
 		return NULL;
 		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)
 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;
 	int start, end;
-    if (!PyArg_ParseTuple(args, "iis#O", &start, &end, 
+	if (!PyArg_ParseTuple(args, "iis#O", &start, &end, 
 						  &sha, &sha_len, &unpack_name))
 						  &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);
 			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);
 		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);
 			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
     return len(select.select([fileno], [], [], 0)[0]) > 0
 
 
 COMMON_CAPABILITIES = ['ofs-delta', 'side-band-64k']
 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
 SEND_CAPABILITIES = ['report-status'] + COMMON_CAPABILITIES
 
 
 
 
@@ -155,8 +155,8 @@ class GitClient(object):
         self._report_activity = report_activity
         self._report_activity = report_activity
         self._fetch_capabilities = set(FETCH_CAPABILITIES)
         self._fetch_capabilities = set(FETCH_CAPABILITIES)
         self._send_capabilities = set(SEND_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):
     def _read_refs(self, proto):
         server_capabilities = None
         server_capabilities = None
@@ -440,7 +440,7 @@ class TraditionalGitClient(GitClient):
         old_refs, server_capabilities = self._read_refs(proto)
         old_refs, server_capabilities = self._read_refs(proto)
         negotiated_capabilities = self._send_capabilities & server_capabilities
         negotiated_capabilities = self._send_capabilities & server_capabilities
         try:
         try:
-            new_refs = determine_wants(old_refs)
+            new_refs = determine_wants(dict(old_refs))
         except:
         except:
             proto.write_pkt_line(None)
             proto.write_pkt_line(None)
             raise
             raise
@@ -707,7 +707,7 @@ class HttpGitClient(GitClient):
         old_refs, server_capabilities = self._discover_references(
         old_refs, server_capabilities = self._discover_references(
             "git-receive-pack", url)
             "git-receive-pack", url)
         negotiated_capabilities = self._send_capabilities & server_capabilities
         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:
         if new_refs is None:
             return old_refs
             return old_refs
         if self.dumb:
         if self.dumb:
@@ -779,7 +779,7 @@ def get_transport_and_path(uri, **kwargs):
         return SSHGitClient(parsed.hostname, port=parsed.port,
         return SSHGitClient(parsed.hostname, port=parsed.port,
                             username=parsed.username, **kwargs), parsed.path
                             username=parsed.username, **kwargs), parsed.path
     elif parsed.scheme in ('http', 'https'):
     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:
     if parsed.scheme and not parsed.netloc:
         # SSH with no user@, zero or one leading slash.
         # SSH with no user@, zero or one leading slash.

+ 12 - 1
dulwich/config.py

@@ -28,6 +28,8 @@ import errno
 import os
 import os
 import re
 import re
 
 
+from UserDict import DictMixin
+
 from dulwich.file import GitFile
 from dulwich.file import GitFile
 
 
 
 
@@ -73,7 +75,7 @@ class Config(object):
         raise NotImplementedError(self.set)
         raise NotImplementedError(self.set)
 
 
 
 
-class ConfigDict(Config):
+class ConfigDict(Config, DictMixin):
     """Git configuration stored in a dictionary."""
     """Git configuration stored in a dictionary."""
 
 
     def __init__(self, values=None):
     def __init__(self, values=None):
@@ -90,6 +92,15 @@ class ConfigDict(Config):
             isinstance(other, self.__class__) and
             isinstance(other, self.__class__) and
             other._values == self._values)
             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
     @classmethod
     def _parse_setting(cls, name):
     def _parse_setting(cls, name):
         parts = name.split(".")
         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 tree_id: The SHA of the merge tree.
     :param rename_detector: RenameDetector object for detecting renames.
     :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
         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
         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:
         elif len(sha) == 20:
             return sha_to_hex(sha)
             return sha_to_hex(sha)
         else:
         else:
-            raise ValueError("Invalid sha %r" % sha)
+            raise ValueError("Invalid sha %r" % (sha,))
 
 
     def contains_loose(self, sha):
     def contains_loose(self, sha):
         """Check if a particular object is present by SHA1 and is loose."""
         """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._parents = []
         self._extra = []
         self._extra = []
         self._author = None
         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:
             if field == _TREE_HEADER:
                 self._tree = value
                 self._tree = value
             elif field == _PARENT_HEADER:
             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._crc32_table_offset = self._name_table_offset + 20 * len(self)
         self._pack_offset_table_offset = (self._crc32_table_offset +
         self._pack_offset_table_offset = (self._crc32_table_offset +
                                           4 * len(self))
                                           4 * len(self))
+        self._pack_offset_largetable_offset = (self._pack_offset_table_offset +
+                                          4 * len(self))
 
 
     def _unpack_entry(self, i):
     def _unpack_entry(self, i):
         return (self._unpack_name(i), self._unpack_offset(i),
         return (self._unpack_name(i), self._unpack_offset(i),
@@ -626,7 +628,11 @@ class PackIndex2(FilePackIndex):
 
 
     def _unpack_offset(self, i):
     def _unpack_offset(self, i):
         offset = self._pack_offset_table_offset + i * 4
         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):
     def _unpack_crc32_checksum(self, i):
         return unpack_from('>L', self._contents,
         return unpack_from('>L', self._contents,
@@ -1036,8 +1042,8 @@ class PackData(object):
         if type == OFS_DELTA:
         if type == OFS_DELTA:
             (delta_offset, delta) = obj
             (delta_offset, delta) = obj
             # TODO: clean up asserts and replace with nicer error messages
             # 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
             base_offset = offset-delta_offset
             type, base_obj = self.get_object_at(base_offset)
             type, base_obj = self.get_object_at(base_offset)
             assert isinstance(type, int)
             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]))
         f.write(struct.pack('>L', fan_out_table[i]))
         fan_out_table[i+1] += fan_out_table[i]
         fan_out_table[i+1] += fan_out_table[i]
     for (name, offset, entry_checksum) in entries:
     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))
         f.write(struct.pack('>L20s', offset, name))
     assert len(pack_checksum) == 20
     assert len(pack_checksum) == 20
     f.write(pack_checksum)
     f.write(pack_checksum)
@@ -1707,6 +1715,7 @@ def write_pack_index_v2(f, entries, pack_checksum):
     for (name, offset, entry_checksum) in entries:
     for (name, offset, entry_checksum) in entries:
         fan_out_table[ord(name[0])] += 1
         fan_out_table[ord(name[0])] += 1
     # Fan-out table
     # Fan-out table
+    largetable = []
     for i in range(0x100):
     for i in range(0x100):
         f.write(struct.pack('>L', fan_out_table[i]))
         f.write(struct.pack('>L', fan_out_table[i]))
         fan_out_table[i+1] += 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:
     for (name, offset, entry_checksum) in entries:
         f.write(struct.pack('>L', entry_checksum))
         f.write(struct.pack('>L', entry_checksum))
     for (name, offset, entry_checksum) in entries:
     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
     assert len(pack_checksum) == 20
     f.write(pack_checksum)
     f.write(pack_checksum)
     return f.write_sha()
     return f.write_sha()
@@ -1828,8 +1841,6 @@ class Pack(object):
     def get_raw(self, sha1):
     def get_raw(self, sha1):
         offset = self.index.object_index(sha1)
         offset = self.index.object_index(sha1)
         obj_type, obj = self.data.get_object_at(offset)
         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)
         type_num, chunks = self.data.resolve_object(offset, obj_type, obj)
         return type_num, ''.join(chunks)
         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.
         :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):
     def get_config_stack(self):
         """Return a config stack for this repository.
         """Return a config stack for this repository.
@@ -1250,8 +1241,16 @@ class Repo(BaseRepo):
               os.path.isdir(os.path.join(root, REFSDIR))):
               os.path.isdir(os.path.join(root, REFSDIR))):
             self.bare = True
             self.bare = True
             self._controldir = root
             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:
         else:
-            raise NotGitRepository(root)
+            raise NotGitRepository(
+                "No git repository was found at %(path)s" % dict(path=root)
+            )
         self.path = root
         self.path = root
         object_store = DiskObjectStore(os.path.join(self.controldir(),
         object_store = DiskObjectStore(os.path.join(self.controldir(),
                                                     OBJECTDIR))
                                                     OBJECTDIR))
@@ -1375,17 +1374,34 @@ class Repo(BaseRepo):
 
 
         # Update target head
         # Update target head
         head, head_sha = self.refs._follow('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
         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):
     def __repr__(self):
         return "<Repo at %r>" % self.path
         return "<Repo at %r>" % self.path
 
 
@@ -1470,6 +1486,14 @@ class MemoryRepo(BaseRepo):
         """
         """
         raise NoIndexPresent()
         raise NoIndexPresent()
 
 
+    def get_config(self):
+        """Retrieve the config object.
+
+        :return: `ConfigFile` object.
+        """
+        from dulwich.config import ConfigFile
+        return ConfigFile()
+
     @classmethod
     @classmethod
     def init_bare(cls, objects, refs):
     def init_bare(cls, objects, refs):
         """Create a new bare repository in memory.
         """Create a new bare repository in memory.

+ 5 - 4
dulwich/server.py

@@ -149,8 +149,9 @@ class DictBackend(Backend):
         try:
         try:
             return self.repos[path]
             return self.repos[path]
         except KeyError:
         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):
 class FileSystemBackend(Backend):
@@ -618,8 +619,8 @@ class ReceivePackHandler(Handler):
         status = []
         status = []
         # TODO: more informative error messages than just the exception string
         # TODO: more informative error messages than just the exception string
         try:
         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'))
             status.append(('unpack', 'ok'))
         except all_exceptions, e:
         except all_exceptions, e:
             status.append(('unpack', str(e).replace('\n', '')))
             status.append(('unpack', str(e).replace('\n', '')))

+ 7 - 0
dulwich/tests/__init__.py

@@ -161,6 +161,13 @@ def nocompat_test_suite():
     return result
     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():
 def test_suite():
     result = unittest.TestSuite()
     result = unittest.TestSuite()
     result.addTests(self_test_suite())
     result.addTests(self_test_suite())

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

@@ -19,7 +19,7 @@
 
 
 """Utilities for testing git server compatibility."""
 """Utilities for testing git server compatibility."""
 
 
-
+import errno
 import os
 import os
 import select
 import select
 import shutil
 import shutil
@@ -200,3 +200,10 @@ class NoSideBand64kReceivePackHandler(ReceivePackHandler):
     def capabilities(cls):
     def capabilities(cls):
         return tuple(c for c in ReceivePackHandler.capabilities()
         return tuple(c for c in ReceivePackHandler.capabilities()
                      if c != 'side-band-64k')
                      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 tempfile
 import threading
 import threading
 import urllib
 import urllib
+from socket import gethostname
 
 
 from dulwich import (
 from dulwich import (
     client,
     client,
@@ -425,6 +426,7 @@ class HTTPGitServer(BaseHTTPServer.HTTPServer):
     def __init__(self, server_address, root_path):
     def __init__(self, server_address, root_path):
         BaseHTTPServer.HTTPServer.__init__(self, server_address, GitHTTPRequestHandler)
         BaseHTTPServer.HTTPServer.__init__(self, server_address, GitHTTPRequestHandler)
         self.root_path = root_path
         self.root_path = root_path
+        self.server_name = "localhost"
 
 
     def get_url(self):
     def get_url(self):
         return 'http://%s:%s/' % (self.server_name, self.server_port)
         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,
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 # MA  02110-1301, USA.
 # MA  02110-1301, USA.
 
 
-"""Tests for reading and writing configuraiton files."""
+"""Tests for reading and writing configuration files."""
 
 
 from cStringIO import StringIO
 from cStringIO import StringIO
 from dulwich.config import (
 from dulwich.config import (
@@ -170,6 +170,17 @@ class ConfigDictTests(TestCase):
         cd.set(("core", ), "foo", "invalid")
         cd.set(("core", ), "foo", "invalid")
         self.assertRaises(ValueError, cd.get_boolean, ("core", ), "foo")
         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):
 class StackedConfigTests(TestCase):
 
 

+ 6 - 16
dulwich/tests/test_index.py

@@ -26,7 +26,6 @@ import os
 import shutil
 import shutil
 import stat
 import stat
 import struct
 import struct
-import sys
 import tempfile
 import tempfile
 
 
 from dulwich.index import (
 from dulwich.index import (
@@ -216,15 +215,8 @@ class IndexEntryFromStatTests(TestCase):
 
 
 class BuildIndexTests(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
         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[7], filesize)  # filesize
         self.assertEquals(index_entry[8], sha)  # sha
         self.assertEquals(index_entry[8], sha)  # sha
 
 
@@ -258,7 +250,7 @@ class BuildIndexTests(TestCase):
 
 
     def test_nonempty(self):
     def test_nonempty(self):
         if os.name != 'posix':
         if os.name != 'posix':
-            self.skip("test depends on POSIX shell")
+            self.skipTest("test depends on POSIX shell")
 
 
         repo_dir = tempfile.mkdtemp()
         repo_dir = tempfile.mkdtemp()
         repo = Repo.init(repo_dir)
         repo = Repo.init(repo_dir)
@@ -283,8 +275,6 @@ class BuildIndexTests(TestCase):
                 repo.object_store, tree.id)
                 repo.object_store, tree.id)
 
 
         # Verify index entries
         # Verify index entries
-        import time
-        ctime = time.time()
         index = repo.open_index()
         index = repo.open_index()
         self.assertEquals(len(index), 4)
         self.assertEquals(len(index), 4)
 
 
@@ -292,28 +282,28 @@ class BuildIndexTests(TestCase):
         apath = os.path.join(repo.path, 'a')
         apath = os.path.join(repo.path, 'a')
         self.assertTrue(os.path.exists(apath))
         self.assertTrue(os.path.exists(apath))
         self.assertReasonableIndexEntry(index['a'],
         self.assertReasonableIndexEntry(index['a'],
-            ctime, stat.S_IFREG | 0644, 6, filea.id)
+            stat.S_IFREG | 0644, 6, filea.id)
         self.assertFileContents(apath, 'file a')
         self.assertFileContents(apath, 'file a')
 
 
         # fileb
         # fileb
         bpath = os.path.join(repo.path, 'b')
         bpath = os.path.join(repo.path, 'b')
         self.assertTrue(os.path.exists(bpath))
         self.assertTrue(os.path.exists(bpath))
         self.assertReasonableIndexEntry(index['b'],
         self.assertReasonableIndexEntry(index['b'],
-            ctime, stat.S_IFREG | 0644, 6, fileb.id)
+            stat.S_IFREG | 0644, 6, fileb.id)
         self.assertFileContents(bpath, 'file b')
         self.assertFileContents(bpath, 'file b')
 
 
         # filed
         # filed
         dpath = os.path.join(repo.path, 'c', 'd')
         dpath = os.path.join(repo.path, 'c', 'd')
         self.assertTrue(os.path.exists(dpath))
         self.assertTrue(os.path.exists(dpath))
         self.assertReasonableIndexEntry(index['c/d'], 
         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')
         self.assertFileContents(dpath, 'file d')
 
 
         # symlink to d
         # symlink to d
         epath = os.path.join(repo.path, 'c', 'e')
         epath = os.path.join(repo.path, 'c', 'e')
         self.assertTrue(os.path.exists(epath))
         self.assertTrue(os.path.exists(epath))
         self.assertReasonableIndexEntry(index['c/e'], 
         self.assertReasonableIndexEntry(index['c/e'], 
-            ctime, stat.S_IFLNK, 1, filee.id)
+            stat.S_IFLNK, 1, filee.id)
         self.assertFileContents(epath, 'd', symlink=True)
         self.assertFileContents(epath, 'd', symlink=True)
 
 
         # Verify no extra files
         # 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))
         c = self.make_commit(commit_timezone=(-1 * 3600))
         self.assertTrue(" -0100\n" in c.as_raw_string())
         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'
 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_empty = ''
     test_string_big = 'Z' * 8192
     test_string_big = 'Z' * 8192
+    test_string_huge = 'Z' * 100000
 
 
     def _test_roundtrip(self, base, target):
     def _test_roundtrip(self, base, target):
         self.assertEqual(target,
         self.assertEqual(target,
@@ -179,6 +180,10 @@ class TestPackDeltas(TestCase):
     def test_overflow(self):
     def test_overflow(self):
         self._test_roundtrip(self.test_string_empty, self.test_string_big)
         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):
 class TestPackData(PackTests):
     """Tests getting the data from the packfile."""
     """Tests getting the data from the packfile."""
@@ -471,6 +476,30 @@ class BaseTestPackIndexWriting(object):
         self.assertEqual(idx.get_pack_checksum(), pack_checksum)
         self.assertEqual(idx.get_pack_checksum(), pack_checksum)
         self.assertEqual(0, len(idx))
         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):
     def test_single(self):
         entry_sha = hex_to_sha('6f670c0fb53f9463760b7295fbb814e965fb20c8')
         entry_sha = hex_to_sha('6f670c0fb53f9463760b7295fbb814e965fb20c8')
         my_entries = [(entry_sha, 178, 42)]
         my_entries = [(entry_sha, 178, 42)]
@@ -520,6 +549,7 @@ class TestMemoryIndexWriting(TestCase, BaseTestPackIndexWriting):
     def setUp(self):
     def setUp(self):
         TestCase.setUp(self)
         TestCase.setUp(self)
         self._has_crc32_checksum = True
         self._has_crc32_checksum = True
+        self._supports_large = True
 
 
     def index(self, filename, entries, pack_checksum):
     def index(self, filename, entries, pack_checksum):
         return MemoryPackIndex(entries, pack_checksum)
         return MemoryPackIndex(entries, pack_checksum)
@@ -535,6 +565,7 @@ class TestPackIndexWritingv1(TestCase, BaseTestFilePackIndexWriting):
         BaseTestFilePackIndexWriting.setUp(self)
         BaseTestFilePackIndexWriting.setUp(self)
         self._has_crc32_checksum = False
         self._has_crc32_checksum = False
         self._expected_version = 1
         self._expected_version = 1
+        self._supports_large = False
         self._write_fn = write_pack_index_v1
         self._write_fn = write_pack_index_v1
 
 
     def tearDown(self):
     def tearDown(self):
@@ -548,6 +579,7 @@ class TestPackIndexWritingv2(TestCase, BaseTestFilePackIndexWriting):
         TestCase.setUp(self)
         TestCase.setUp(self)
         BaseTestFilePackIndexWriting.setUp(self)
         BaseTestFilePackIndexWriting.setUp(self)
         self._has_crc32_checksum = True
         self._has_crc32_checksum = True
+        self._supports_large = True
         self._expected_version = 2
         self._expected_version = 2
         self._write_fn = write_pack_index_v2
         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(),
         self.assertEqual(shas, [t.head(),
                          '2a72d929692c41d8554c07f6301757ba18a65d91'])
                          '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):
     def test_merge_history(self):
         r = self._repo = open_repo('simple_merge.git')
         r = self._repo = open_repo('simple_merge.git')
         shas = [e.commit.id for e in r.get_walker()]
         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')
         r = self._repo = open_repo('ooo_merge.git')
         self.assertIsInstance(r.get_config_stack(), Config)
         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):
     def test_common_revisions(self):
         """
         """
         This test demonstrates that ``find_common_revisions()`` actually returns
         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.assertRaises(NotGitRepository,
             self.backend.open_repository, os.path.join(self.path, "foo"))
             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):
 class ServeCommandTests(TestCase):
     """Tests for serve_command."""
     """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
         # Ensure that c1..y4 get excluded even though they're popped from the
         # priority queue long before y5.
         # priority queue long before y5.
         self.assertWalkYields([m6, x2], [m6.id], exclude=[y5.id])
         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._add_handler(self._app)
         self.assertEqual('output', self._app(self._environ, None))
         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):
 class GunzipTestCase(HTTPGitApplicationTestCase):
     """TestCase for testing the GunzipFilter, ensuring the wsgi.input
     """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
                 if self._pq and all(c.id in self._excluded
                                     for _, c in self._pq):
                                     for _, c in self._pq):
                     _, n = self._pq[0]
                     _, 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
                         # If the next commit is newer than the last one, we need
                         # to keep walking in case its parents (which we may not
                         # to keep walking in case its parents (which we may not
                         # have seen yet) are excluded. This gives the excluded
                         # have seen yet) are excluded. This gives the excluded
@@ -255,6 +255,9 @@ class Walker(object):
         return False
         return False
 
 
     def _change_matches(self, change):
     def _change_matches(self, change):
+        if not change:
+            return False
+
         old_path = change.old.path
         old_path = change.old.path
         new_path = change.new.path
         new_path = change.new.path
         if self._path_matches(new_path):
         if self._path_matches(new_path):
@@ -338,8 +341,8 @@ def _topo_reorder(entries):
     order, e.g. in commit time order.
     order, e.g. in commit time order.
 
 
     :param entries: An iterable of WalkEntry objects.
     :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()
     todo = collections.deque()
     pending = {}
     pending = {}

+ 10 - 4
dulwich/web.py

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

+ 2 - 2
setup.py

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