瀏覽代碼

Import upstream version 0.6.2+bzr701

Jelmer Vernooij 14 年之前
父節點
當前提交
bcf0ed6a53

+ 5 - 5
Makefile

@@ -19,21 +19,21 @@ install::
 	$(SETUP) install
 
 check:: build
-	PYTHONPATH=. $(PYTHON) $(TESTRUNNER) dulwich
-	which git > /dev/null && PYTHONPATH=. $(PYTHON) $(TESTRUNNER) $(TESTFLAGS) -i compat
+	PYTHONPATH=.:$(PYTHONPATH) $(PYTHON) $(TESTRUNNER) dulwich
+	which git > /dev/null && PYTHONPATH=.:$(PYTHONPATH) $(PYTHON) $(TESTRUNNER) $(TESTFLAGS) -i compat
 
 check-noextensions:: clean
-	PYTHONPATH=. $(PYTHON) $(TESTRUNNER) $(TESTFLAGS) dulwich
+	PYTHONPATH=.:$(PYTHONPATH) $(PYTHON) $(TESTRUNNER) $(TESTFLAGS) dulwich
 
 check-compat:: build
-	PYTHONPATH=. $(PYTHON) $(TESTRUNNER) $(TESTFLAGS) -i compat
+	PYTHONPATH=.:$(PYTHONPATH) $(PYTHON) $(TESTRUNNER) $(TESTFLAGS) -i compat
 
 clean::
 	$(SETUP) clean --all
 	rm -f dulwich/*.so
 
 coverage:: build
-	PYTHONPATH=. $(PYTHON) $(TESTRUNNER) --cover-package=dulwich --with-coverage --cover-erase --cover-inclusive dulwich
+	PYTHONPATH=.:$(PYTHONPATH) $(PYTHON) $(TESTRUNNER) --cover-package=dulwich --with-coverage --cover-erase --cover-inclusive dulwich
 
 coverage-annotate: coverage
 	python-coverage -a -o /usr

+ 13 - 0
NEWS

@@ -1,3 +1,16 @@
+0.7.0	UNRELEASED
+
+ FEATURES
+
+  * Add Tree.items(). (Jelmer Vernooij)
+
+  * Add eof() and unread_pkt_line() methods to Protocol. (Dave Borowitz)
+
+ BUG FIXES
+
+  * Correct short-circuiting operation for no-op fetches in the server.
+    (Dave Borowitz)
+
 0.6.2	2010-10-16
 
  BUG FIXES

+ 1 - 1
dulwich/__init__.py

@@ -27,4 +27,4 @@ import protocol
 import repo
 import server
 
-__version__ = (0, 6, 2)
+__version__ = (0, 7, 0)

+ 51 - 46
dulwich/_objects.c

@@ -35,6 +35,8 @@ size_t strnlen(char *text, size_t maxlen)
 
 #define bytehex(x) (((x)<0xa)?('0'+(x)):('a'-0xa+(x)))
 
+static PyObject *tree_entry_cls;
+
 static PyObject *sha_to_pyhex(const unsigned char *sha)
 {
 	char hexsha[41];
@@ -144,88 +146,79 @@ int cmp_tree_item(const void *_a, const void *_b)
 	return strcmp(remain_a, remain_b);
 }
 
-static void free_tree_items(struct tree_item *items, int num) {
-	int i;
-	for (i = 0; i < num; i++) {
-		Py_DECREF(items[i].tuple);
-	}
-	free(items);
-}
-
 static PyObject *py_sorted_tree_items(PyObject *self, PyObject *entries)
 {
-	struct tree_item *qsort_entries;
-	int num, i;
-	PyObject *ret;
+	struct tree_item *qsort_entries = NULL;
+	int num_entries, n = 0, i;
+	PyObject *ret, *key, *value, *py_mode, *py_sha;
 	Py_ssize_t pos = 0;
-	PyObject *key, *value;
 
 	if (!PyDict_Check(entries)) {
 		PyErr_SetString(PyExc_TypeError, "Argument not a dictionary");
-		return NULL;
+		goto error;
 	}
 
-	num = PyDict_Size(entries);
-	qsort_entries = malloc(num * sizeof(struct tree_item));
-	if (qsort_entries == NULL) {
+	num_entries = PyDict_Size(entries);
+	if (PyErr_Occurred())
+		goto error;
+	qsort_entries = PyMem_New(struct tree_item, num_entries);
+	if (!qsort_entries) {
 		PyErr_NoMemory();
-		return NULL;
+		goto error;
 	}
 
-	i = 0;
 	while (PyDict_Next(entries, &pos, &key, &value)) {
-		PyObject *py_mode, *py_int_mode, *py_sha;
-
 		if (!PyString_Check(key)) {
 			PyErr_SetString(PyExc_TypeError, "Name is not a string");
-			free_tree_items(qsort_entries, i);
-			return NULL;
+			goto error;
 		}
 
 		if (PyTuple_Size(value) != 2) {
 			PyErr_SetString(PyExc_ValueError, "Tuple has invalid size");
-			free_tree_items(qsort_entries, i);
-			return NULL;
+			goto error;
 		}
 
 		py_mode = PyTuple_GET_ITEM(value, 0);
-		py_int_mode = PyNumber_Int(py_mode);
-		if (!py_int_mode) {
+		if (!PyInt_Check(py_mode)) {
 			PyErr_SetString(PyExc_TypeError, "Mode is not an integral type");
-			free_tree_items(qsort_entries, i);
-			return NULL;
+			goto error;
 		}
 
 		py_sha = PyTuple_GET_ITEM(value, 1);
 		if (!PyString_Check(py_sha)) {
 			PyErr_SetString(PyExc_TypeError, "SHA is not a string");
-			Py_DECREF(py_int_mode);
-			free_tree_items(qsort_entries, i);
-			return NULL;
+			goto error;
 		}
-		qsort_entries[i].name = PyString_AS_STRING(key);
-		qsort_entries[i].mode = PyInt_AS_LONG(py_mode);
-		qsort_entries[i].tuple = PyTuple_Pack(3, key, py_int_mode, py_sha);
-		Py_DECREF(py_int_mode);
-		i++;
+		qsort_entries[n].name = PyString_AS_STRING(key);
+		qsort_entries[n].mode = PyInt_AS_LONG(py_mode);
+
+		qsort_entries[n].tuple = PyObject_CallFunctionObjArgs(
+				tree_entry_cls, key, py_mode, py_sha, NULL);
+		if (qsort_entries[n].tuple == NULL)
+			goto error;
+		n++;
 	}
 
-	qsort(qsort_entries, num, sizeof(struct tree_item), cmp_tree_item);
+	qsort(qsort_entries, num_entries, sizeof(struct tree_item), cmp_tree_item);
 
-	ret = PyList_New(num);
+	ret = PyList_New(num_entries);
 	if (ret == NULL) {
-		free_tree_items(qsort_entries, i);
 		PyErr_NoMemory();
-		return NULL;
+		goto error;
 	}
 
-	for (i = 0; i < num; i++) {
+	for (i = 0; i < num_entries; i++) {
 		PyList_SET_ITEM(ret, i, qsort_entries[i].tuple);
 	}
-
-	free(qsort_entries);
-
+	PyMem_Free(qsort_entries);
 	return ret;
+
+error:
+	for (i = 0; i < n; i++) {
+		Py_XDECREF(qsort_entries[i].tuple);
+	}
+	PyMem_Free(qsort_entries);
+	return NULL;
 }
 
 static PyMethodDef py_objects_methods[] = {
@@ -234,11 +227,23 @@ static PyMethodDef py_objects_methods[] = {
 	{ NULL, NULL, 0, NULL }
 };
 
-void init_objects(void)
+PyMODINIT_FUNC
+init_objects(void)
 {
-	PyObject *m;
+	PyObject *m, *objects_mod;
 
 	m = Py_InitModule3("_objects", py_objects_methods, NULL);
 	if (m == NULL)
 		return;
+
+	/* This is a circular import but should be safe since this module is
+	 * imported at at the very bottom of objects.py. */
+	objects_mod = PyImport_ImportModule("dulwich.objects");
+	if (objects_mod == NULL)
+		return;
+
+	tree_entry_cls = PyObject_GetAttrString(objects_mod, "TreeEntry");
+	Py_DECREF(objects_mod);
+	if (tree_entry_cls == NULL)
+		return;
 }

+ 21 - 4
dulwich/fastexport.py

@@ -27,7 +27,6 @@ from dulwich.objects import (
     Blob,
     Commit,
     Tag,
-    parse_timezone,
     )
 from fastimport import (
     commands,
@@ -111,19 +110,25 @@ class GitImportProcessor(processor.ImportProcessor):
     """An import processor that imports into a Git repository using Dulwich.
 
     """
+    # FIXME: Batch creation of objects?
 
     def __init__(self, repo, params=None, verbose=False, outf=None):
         processor.ImportProcessor.__init__(self, params, verbose)
         self.repo = repo
         self.last_commit = None
+        self.markers = {}
 
     def import_stream(self, stream):
         p = parser.ImportParser(stream)
         self.process(p.iter_commands)
+        return self.markers
 
     def blob_handler(self, cmd):
         """Process a BlobCommand."""
-        self.repo.object_store.add_object(Blob.from_string(cmd.data))
+        blob = Blob.from_string(cmd.data)
+        self.repo.object_store.add_object(blob)
+        if cmd.mark:
+            self.markers[cmd.mark] = blob.id
 
     def checkpoint_handler(self, cmd):
         """Process a CheckpointCommand."""
@@ -132,8 +137,18 @@ class GitImportProcessor(processor.ImportProcessor):
     def commit_handler(self, cmd):
         """Process a CommitCommand."""
         commit = Commit()
-        commit.author = cmd.author
-        commit.committer = cmd.committer
+        if cmd.author is not None:
+            author = cmd.author
+        else:
+            author = cmd.committer
+        (author_name, author_email, author_timestamp, author_timezone) = author
+        (committer_name, committer_email, commit_timestamp, commit_timezone) = cmd.committer
+        commit.author = "%s <%s>" % (author_name, author_email)
+        commit.author_timezone = author_timezone
+        commit.author_time = int(author_timestamp)
+        commit.committer = "%s <%s>" % (committer_name, committer_email)
+        commit.commit_timezone = commit_timezone
+        commit.commit_time = int(commit_timestamp)
         commit.message = cmd.message
         commit.parents = []
         contents = {}
@@ -146,6 +161,8 @@ class GitImportProcessor(processor.ImportProcessor):
         self.repo.object_store.add_object(commit)
         self.repo[cmd.ref] = commit.id
         self.last_commit = commit.id
+        if cmd.mark:
+            self.markers[cmd.mark] = commit.id
 
     def progress_handler(self, cmd):
         """Process a ProgressCommand."""

+ 1 - 0
dulwich/file.py

@@ -30,6 +30,7 @@ def ensure_dir_exists(dirname):
         if e.errno != errno.EEXIST:
             raise
 
+
 def fancy_rename(oldname, newname):
     """Rename file with temporary backup file to rollback if rename fails"""
     if not os.path.exists(newname):

+ 54 - 0
dulwich/misc.py

@@ -99,3 +99,57 @@ def unpack_from(fmt, buf, offset=0):
     except AttributeError:
         b = buf[offset:offset+struct.calcsize(fmt)]
         return struct.unpack(fmt, b)
+
+
+try:
+    from collections import namedtuple
+
+    TreeEntryTuple = namedtuple('TreeEntryTuple', ['path', 'mode', 'sha'])
+except ImportError:
+    # Provide manual implementations of namedtuples for Python <2.5.
+    # If the class definitions change, be sure to keep these in sync by running
+    # namedtuple(..., verbose=True) in a recent Python and pasting the output.
+
+    # Necessary globals go here.
+    _tuple = tuple
+    _property = property
+    from operator import itemgetter as _itemgetter
+
+    class TreeEntryTuple(tuple):
+            'TreeEntryTuple(path, mode, sha)'
+
+            __slots__ = ()
+
+            _fields = ('path', 'mode', 'sha')
+
+            def __new__(_cls, path, mode, sha):
+                return _tuple.__new__(_cls, (path, mode, sha))
+
+            @classmethod
+            def _make(cls, iterable, new=tuple.__new__, len=len):
+                'Make a new TreeEntryTuple object from a sequence or iterable'
+                result = new(cls, iterable)
+                if len(result) != 3:
+                    raise TypeError('Expected 3 arguments, got %d' % len(result))
+                return result
+
+            def __repr__(self):
+                return 'TreeEntryTuple(path=%r, mode=%r, sha=%r)' % self
+
+            def _asdict(t):
+                'Return a new dict which maps field names to their values'
+                return {'path': t[0], 'mode': t[1], 'sha': t[2]}
+
+            def _replace(_self, **kwds):
+                'Return a new TreeEntryTuple object replacing specified fields with new values'
+                result = _self._make(map(kwds.pop, ('path', 'mode', 'sha'), _self))
+                if kwds:
+                    raise ValueError('Got unexpected field names: %r' % kwds.keys())
+                return result
+
+            def __getnewargs__(self):
+                return tuple(self)
+
+            path = _property(_itemgetter(0))
+            mode = _property(_itemgetter(1))
+            sha = _property(_itemgetter(2))

+ 1 - 1
dulwich/object_store.py

@@ -183,7 +183,7 @@ class BaseObjectStore(object):
 
         :param tree_id: SHA1 of the tree.
         :param include_trees: If True, include tree objects in the iteration.
-        :yield: Tuples of (path, mode, hexhsa) for objects in a tree.
+        :return: Yields tuples of (path, mode, hexhsa) for objects in a tree.
         """
         todo = [('', stat.S_IFDIR, tree_id)]
         while todo:

+ 24 - 2
dulwich/objects.py

@@ -25,6 +25,7 @@ from cStringIO import (
     StringIO,
     )
 import os
+import posixpath
 import stat
 import zlib
 
@@ -39,6 +40,7 @@ from dulwich.errors import (
 from dulwich.file import GitFile
 from dulwich.misc import (
     make_sha,
+    TreeEntryTuple,
     )
 
 
@@ -684,6 +686,14 @@ class Tag(ShaFile):
     message = serializable_property("message", "The message attached to this tag")
 
 
+class TreeEntry(TreeEntryTuple):
+    """Namedtuple encapsulating a single tree entry."""
+
+    def in_path(self, path):
+        """Return a copy of this entry with the given path prepended."""
+        return TreeEntry(posixpath.join(path, self.path), self.mode, self.sha)
+
+
 def parse_tree(text):
     """Parse a tree text.
 
@@ -733,7 +743,7 @@ def sorted_tree_items(entries):
         mode = int(mode)
         if not isinstance(hexsha, str):
             raise TypeError('Expected a string for SHA, got %r' % hexsha)
-        yield name, mode, hexsha
+        yield TreeEntry(name, mode, hexsha)
 
 
 def cmp_entry((name1, value1), (name2, value2)):
@@ -811,7 +821,12 @@ class Tree(ShaFile):
         self._needs_serialization = True
 
     def entries(self):
-        """Return a list of tuples describing the tree entries"""
+        """Return a list of tuples describing the tree entries.
+        
+        :note: The order of the tuples that are returned is different from that 
+            returned by the items and iteritems methods. This function will be 
+            deprecated in the future.
+        """
         self._ensure_parsed()
         # The order of this is different from iteritems() for historical
         # reasons
@@ -826,6 +841,13 @@ class Tree(ShaFile):
         self._ensure_parsed()
         return sorted_tree_items(self._entries)
 
+    def items(self):
+        """Return the sorted entries in this tree.
+
+        :return: List with (name, mode, sha) tuples
+        """
+        return list(self.iteritems())
+
     def _deserialize(self, chunks):
         """Grab the entries in the tree"""
         try:

+ 12 - 8
dulwich/patch.py

@@ -18,7 +18,7 @@
 
 """Classes for dealing with git am-style patches.
 
-These patches are basically unified diffs with some extra metadata tacked 
+These patches are basically unified diffs with some extra metadata tacked
 on.
 """
 
@@ -46,7 +46,7 @@ def write_commit_patch(f, commit, contents, progress, version=None):
     f.write("\n")
     f.write("---\n")
     try:
-        p = subprocess.Popen(["diffstat"], stdout=subprocess.PIPE, 
+        p = subprocess.Popen(["diffstat"], stdout=subprocess.PIPE,
                              stdin=subprocess.PIPE)
     except OSError, e:
         pass # diffstat not available?
@@ -65,7 +65,7 @@ def write_commit_patch(f, commit, contents, progress, version=None):
 
 def get_summary(commit):
     """Determine the summary line for use in a filename.
-    
+
     :param commit: Commit
     :return: Summary string
     """
@@ -102,7 +102,7 @@ def unified_diff(a, b, fromfile='', tofile='', n=3):
                     yield '+' + line
 
 
-def write_blob_diff(f, (old_path, old_mode, old_blob), 
+def write_blob_diff(f, (old_path, old_mode, old_blob),
                        (new_path, new_mode, new_blob)):
     """Write diff file header.
 
@@ -133,14 +133,14 @@ def write_blob_diff(f, (old_path, old_mode, old_blob),
         if new_mode is not None:
             if old_mode is not None:
                 f.write("old mode %o\n" % old_mode)
-            f.write("new mode %o\n" % new_mode) 
+            f.write("new mode %o\n" % new_mode)
         else:
             f.write("deleted mode %o\n" % old_mode)
     f.write("index %s..%s %o\n" % (
         blob_id(old_blob), blob_id(new_blob), new_mode))
     old_contents = lines(old_blob)
     new_contents = lines(new_blob)
-    f.writelines(unified_diff(old_contents, new_contents, 
+    f.writelines(unified_diff(old_contents, new_contents,
         old_path, new_path))
 
 
@@ -167,9 +167,13 @@ def git_am_patch_split(f):
         if l == "---\n":
             break
         if first:
-            c.message += "\n"
+            if l.startswith("From: "):
+                c.author = l[len("From: "):].rstrip()
+            else:
+                c.message += "\n" + l
             first = False
-        c.message += l
+        else:
+            c.message += l
     diff = ""
     for l in f:
         if l == "-- \n":

+ 39 - 3
dulwich/protocol.py

@@ -82,15 +82,24 @@ class Protocol(object):
         self.read = read
         self.write = write
         self.report_activity = report_activity
+        self._readahead = None
 
     def read_pkt_line(self):
         """Reads a pkt-line from the remote git process.
 
+        This method may read from the readahead buffer; see unread_pkt_line.
+
         :return: The next string from the stream, without the length prefix, or
             None for a flush-pkt ('0000').
         """
+        if self._readahead is None:
+            read = self.read
+        else:
+            read = self._readahead.read
+            self._readahead = None
+
         try:
-            sizestr = self.read(4)
+            sizestr = read(4)
             if not sizestr:
                 raise HangupException()
             size = int(sizestr, 16)
@@ -100,14 +109,41 @@ class Protocol(object):
                 return None
             if self.report_activity:
                 self.report_activity(size, 'read')
-            return self.read(size-4)
+            return read(size-4)
         except socket.error, e:
             raise GitProtocolError(e)
 
+    def eof(self):
+        """Test whether the protocol stream has reached EOF.
+
+        Note that this refers to the actual stream EOF and not just a flush-pkt.
+
+        :return: True if the stream is at EOF, False otherwise.
+        """
+        try:
+            next_line = self.read_pkt_line()
+        except HangupException:
+            return True
+        self.unread_pkt_line(next_line)
+        return False
+
+    def unread_pkt_line(self, data):
+        """Unread a single line of data into the readahead buffer.
+
+        This method can be used to unread a single pkt-line into a fixed
+        readahead buffer.
+
+        :param data: The data to unread, without the length prefix.
+        :raise ValueError: If more than one pkt-line is unread.
+        """
+        if self._readahead is not None:
+            raise ValueError('Attempted to unread multiple pkt-lines.')
+        self._readahead = StringIO(pkt_line(data))
+
     def read_pkt_seq(self):
         """Read a sequence of pkt-lines from the remote git process.
 
-        :yield: Each line of data up to but not including the next flush-pkt.
+        :return: Yields each line of data up to but not including the next flush-pkt.
         """
         pkt = self.read_pkt_line()
         while pkt:

+ 4 - 3
dulwich/repo.py

@@ -51,7 +51,6 @@ from dulwich.objects import (
     Tag,
     Tree,
     hex_to_sha,
-    object_class,
     )
 import warnings
 
@@ -833,8 +832,10 @@ class BaseRepo(object):
         :return: iterator over objects, with __len__ implemented
         """
         wants = determine_wants(self.get_refs())
-        if not wants:
-            return []
+        if wants is None:
+            # TODO(dborowitz): find a way to short-circuit that doesn't change
+            # this interface.
+            return None
         haves = self.object_store.find_common_revisions(graph_walker)
         return self.object_store.iter_shas(
           self.object_store.find_missing_objects(haves, wants, progress,

+ 11 - 3
dulwich/server.py

@@ -264,8 +264,9 @@ class UploadPackHandler(Handler):
           graph_walker.determine_wants, graph_walker, self.progress,
           get_tagged=self.get_tagged)
 
-        # Do they want any objects?
-        if len(objects_iter) == 0:
+        # Did the process short-circuit (e.g. in a stateless RPC call)? Note
+        # that the client still expects a 0-object pack in most cases.
+        if objects_iter is None:
             return
 
         self.progress("dul-daemon says what\n")
@@ -367,7 +368,7 @@ class ProtocolGraphWalker(object):
             self.proto.write_pkt_line(None)
 
             if self.advertise_refs:
-                return []
+                return None
 
         # Now client will sending want want want commands
         want = self.proto.read_pkt_line()
@@ -388,6 +389,13 @@ class ProtocolGraphWalker(object):
             command, sha = self.read_proto_line(allowed)
 
         self.set_wants(want_revs)
+
+        if self.stateless_rpc and self.proto.eof():
+            # The client may close the socket at this point, expecting a
+            # flush-pkt from the server. We might be ready to send a packfile at
+            # this point, so we need to explicitly short-circuit in this case.
+            return None
+
         return want_revs
 
     def ack(self, have_ref):

+ 12 - 0
dulwich/tests/compat/server_utils.py

@@ -88,6 +88,18 @@ class ServerTests(object):
         self._old_repo.object_store._pack_cache = None
         self.assertReposEqual(self._old_repo, self._new_repo)
 
+    def test_fetch_from_dulwich_no_op(self):
+        self._old_repo = import_repo('server_old.export')
+        self._new_repo = import_repo('server_old.export')
+        self.assertReposEqual(self._old_repo, self._new_repo)
+        port = self._start_server(self._new_repo)
+
+        run_git_or_fail(['fetch', self.url(port)] + self.branch_args(),
+                        cwd=self._old_repo.path)
+        # flush the pack cache so any new packs are picked up
+        self._old_repo.object_store._pack_cache = None
+        self.assertReposEqual(self._old_repo, self._new_repo)
+
 
 class ShutdownServerMixIn:
     """Mixin that allows serve_forever to be shut down.

+ 53 - 0
dulwich/tests/test_fastexport.py

@@ -28,6 +28,9 @@ from dulwich.objects import (
     Commit,
     Tree,
     )
+from dulwich.repo import (
+    MemoryRepo,
+    )
 from dulwich.tests import (
     TestCase,
     TestSkipped,
@@ -35,6 +38,7 @@ from dulwich.tests import (
 
 
 class GitFastExporterTests(TestCase):
+    """Tests for the GitFastExporter tests."""
 
     def setUp(self):
         super(GitFastExporterTests, self).setUp()
@@ -78,3 +82,52 @@ data 3
 msg
 M 644 1 foo
 """, self.stream.getvalue())
+
+
+class GitImportProcessorTests(TestCase):
+    """Tests for the GitImportProcessor tests."""
+
+    def setUp(self):
+        super(GitImportProcessorTests, self).setUp()
+        self.repo = MemoryRepo()
+        try:
+            from dulwich.fastexport import GitImportProcessor
+        except ImportError:
+            raise TestSkipped("python-fastimport not available")
+        self.processor = GitImportProcessor(self.repo)
+
+    def test_commit_handler(self):
+        from fastimport import commands
+        cmd = commands.CommitCommand("refs/heads/foo", "mrkr",
+            ("Jelmer", "jelmer@samba.org", 432432432.0, 3600),
+            ("Jelmer", "jelmer@samba.org", 432432432.0, 3600),
+            "FOO", None, [], [])
+        self.processor.commit_handler(cmd)
+        commit = self.repo[self.processor.last_commit]
+        self.assertEquals("Jelmer <jelmer@samba.org>", commit.author)
+        self.assertEquals("Jelmer <jelmer@samba.org>", commit.committer)
+        self.assertEquals("FOO", commit.message)
+        self.assertEquals([], commit.parents)
+        self.assertEquals(432432432.0, commit.commit_time)
+        self.assertEquals(432432432.0, commit.author_time)
+        self.assertEquals(3600, commit.commit_timezone)
+        self.assertEquals(3600, commit.author_timezone)
+        self.assertEquals(commit, self.repo["refs/heads/foo"])
+
+    def test_import_stream(self):
+        markers = self.processor.import_stream(StringIO("""blob
+mark :1
+data 11
+text for a
+
+commit refs/heads/master
+mark :2
+committer Joe Foo <joe@foo.com> 1288287382 +0000
+data 20
+<The commit message>
+M 100644 :1 a
+
+"""))
+        self.assertEquals(2, len(markers))
+        self.assertIsInstance(self.repo[markers["1"]], Blob)
+        self.assertIsInstance(self.repo[markers["2"]], Commit)

+ 14 - 5
dulwich/tests/test_objects.py

@@ -42,6 +42,7 @@ from dulwich.objects import (
     check_hexsha,
     check_identity,
     parse_timezone,
+    TreeEntry,
     parse_tree,
     _parse_tree_py,
     sorted_tree_items,
@@ -425,9 +426,9 @@ _TREE_ITEMS = {
   }
 
 _SORTED_TREE_ITEMS = [
-  ('a.c', 0100755, 'd80c186a03f423a81b39df39dc87fd269736ca86'),
-  ('a', stat.S_IFDIR, 'd80c186a03f423a81b39df39dc87fd269736ca86'),
-  ('a/c', stat.S_IFDIR, 'd80c186a03f423a81b39df39dc87fd269736ca86'),
+  TreeEntry('a.c', 0100755, 'd80c186a03f423a81b39df39dc87fd269736ca86'),
+  TreeEntry('a', stat.S_IFDIR, 'd80c186a03f423a81b39df39dc87fd269736ca86'),
+  TreeEntry('a/c', stat.S_IFDIR, 'd80c186a03f423a81b39df39dc87fd269736ca86'),
   ]
 
 
@@ -447,12 +448,18 @@ class TreeTests(ShaFileCheckTests):
         x["a.b"] = (stat.S_IFDIR, "d80c186a03f423a81b39df39dc87fd269736ca86")
         self.assertEquals("07bfcb5f3ada15bbebdfa3bbb8fd858a363925c8", x.id)
 
-    def test_tree_dir_sort(self):
+    def test_tree_iteritems_dir_sort(self):
         x = Tree()
         for name, item in _TREE_ITEMS.iteritems():
             x[name] = item
         self.assertEquals(_SORTED_TREE_ITEMS, list(x.iteritems()))
 
+    def test_tree_items_dir_sort(self):
+        x = Tree()
+        for name, item in _TREE_ITEMS.iteritems():
+            x[name] = item
+        self.assertEquals(_SORTED_TREE_ITEMS, x.items())
+
     def _do_test_parse_tree(self, parse_tree):
         dir = os.path.join(os.path.dirname(__file__), 'data', 'trees')
         o = Tree.from_path(hex_to_filename(dir, tree_sha))
@@ -471,7 +478,9 @@ class TreeTests(ShaFileCheckTests):
         def do_sort(entries):
             return list(sorted_tree_items(entries))
 
-        self.assertEqual(_SORTED_TREE_ITEMS, do_sort(_TREE_ITEMS))
+        actual = do_sort(_TREE_ITEMS)
+        self.assertEqual(_SORTED_TREE_ITEMS, actual)
+        self.assertTrue(isinstance(actual[0], TreeEntry))
 
         # C/Python implementations may differ in specific error types, but
         # should all error on invalid inputs.

+ 24 - 0
dulwich/tests/test_patch.py

@@ -111,3 +111,27 @@ Subject:  [Dulwich-users] [PATCH] Added unit tests for
 """
         c, diff, version = git_am_patch_split(StringIO(text))
         self.assertEquals('Added unit tests for dulwich.object_store.tree_lookup_path.\n\n* dulwich/tests/test_object_store.py\n  (TreeLookupPathTests): This test case contains a few tests that ensure the\n   tree_lookup_path function works as expected.\n', c.message)
+
+    def test_extract_pseudo_from_header(self):
+        text = """From ff643aae102d8870cac88e8f007e70f58f3a7363 Mon Sep 17 00:00:00 2001
+From: Jelmer Vernooij <jelmer@samba.org>
+Date: Thu, 15 Apr 2010 15:40:28 +0200
+Subject:  [Dulwich-users] [PATCH] Added unit tests for
+ dulwich.object_store.tree_lookup_path.
+
+From: Jelmer Vernooy <jelmer@debian.org>
+
+* dulwich/tests/test_object_store.py
+  (TreeLookupPathTests): This test case contains a few tests that ensure the
+   tree_lookup_path function works as expected.
+---
+ pixmaps/prey.ico |  Bin 9662 -> 9662 bytes
+ 1 files changed, 0 insertions(+), 0 deletions(-)
+ mode change 100755 => 100644 pixmaps/prey.ico
+
+-- 
+1.7.0.4
+"""
+        c, diff, version = git_am_patch_split(StringIO(text))
+        self.assertEquals("Jelmer Vernooy <jelmer@debian.org>", c.author)
+        self.assertEquals('Added unit tests for dulwich.object_store.tree_lookup_path.\n\n* dulwich/tests/test_object_store.py\n  (TreeLookupPathTests): This test case contains a few tests that ensure the\n   tree_lookup_path function works as expected.\n', c.message)

+ 33 - 1
dulwich/tests/test_protocol.py

@@ -21,6 +21,9 @@
 
 from StringIO import StringIO
 
+from dulwich.errors import (
+    HangupException,
+    )
 from dulwich.protocol import (
     Protocol,
     ReceivableProtocol,
@@ -50,6 +53,24 @@ class BaseProtocolTests(object):
         self.rin.seek(0)
         self.assertEquals('cmd ', self.proto.read_pkt_line())
 
+    def test_eof(self):
+        self.rin.write('0000')
+        self.rin.seek(0)
+        self.assertFalse(self.proto.eof())
+        self.assertEquals(None, self.proto.read_pkt_line())
+        self.assertTrue(self.proto.eof())
+        self.assertRaises(HangupException, self.proto.read_pkt_line)
+
+    def test_unread_pkt_line(self):
+        self.rin.write('0007foo0000')
+        self.rin.seek(0)
+        self.assertEquals('foo', self.proto.read_pkt_line())
+        self.proto.unread_pkt_line('bar')
+        self.assertEquals('bar', self.proto.read_pkt_line())
+        self.assertEquals(None, self.proto.read_pkt_line())
+        self.proto.unread_pkt_line('baz1')
+        self.assertRaises(ValueError, self.proto.unread_pkt_line, 'baz2')
+
     def test_read_pkt_seq(self):
         self.rin.write('0008cmd 0005l0000')
         self.rin.seek(0)
@@ -91,10 +112,14 @@ class ProtocolTests(BaseProtocolTests, TestCase):
 class ReceivableStringIO(StringIO):
     """StringIO with socket-like recv semantics for testing."""
 
+    def __init__(self):
+        StringIO.__init__(self)
+        self.allow_read_past_eof = False
+
     def recv(self, size):
         # fail fast if no bytes are available; in a real socket, this would
         # block forever
-        if self.tell() == len(self.getvalue()):
+        if self.tell() == len(self.getvalue()) and not self.allow_read_past_eof:
             raise AssertionError('Blocking read past end of socket')
         if size == 1:
             return self.read(1)
@@ -111,6 +136,13 @@ class ReceivableProtocolTests(BaseProtocolTests, TestCase):
         self.proto = ReceivableProtocol(self.rin.recv, self.rout.write)
         self.proto._rbufsize = 8
 
+    def test_eof(self):
+        # Allow blocking reads past EOF just for this test. The only parts of
+        # the protocol that might check for EOF do not depend on the recv()
+        # semantics anyway.
+        self.rin.allow_read_past_eof = True
+        BaseProtocolTests.test_eof(self)
+
     def test_recv(self):
         all_data = '1234567' * 10  # not a multiple of bufsize
         self.rin.write(all_data)

+ 1 - 1
setup.py

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