Browse Source

Imported Upstream version 0.13.0

Jelmer Vernooij 9 years ago
parent
commit
dd410283bf

+ 28 - 0
.travis.yml

@@ -0,0 +1,28 @@
+language: python
+sudo: false
+env:
+  global: TEST_RUNNER=unittest PYTHONHASHSEED=random
+matrix:
+  include:
+    - python: "2.7"
+      env: TEST_REQUIRE="gevent geventhttpclient fastimport"
+    - python: "pypy"
+      env: TEST_REQUIRE="fastimport"
+    - python: "3.4"
+      env: TEST_REQUIRE="fastimport"
+    - python: "3.5"
+      env: TEST_REQUIRE="fastimport"
+cache:
+  directories:
+    - $HOME/.cache/pip
+script:
+  - pip install pip --upgrade
+  - pip install $TEST_REQUIRE
+
+  # Test without c extensions
+  - python -m $TEST_RUNNER dulwich.tests.test_suite
+
+  # Test with c extensions
+  - python setup.py build_ext -i
+  - python -m $TEST_RUNNER dulwich.tests.test_suite
+

+ 1 - 0
MANIFEST.in

@@ -15,3 +15,4 @@ include tox.ini
 include dulwich.cfg
 include appveyor.yml
 include .testr.conf
+include .travis.yml

+ 0 - 4
Makefile

@@ -3,11 +3,7 @@ PYFLAKES = pyflakes
 PEP8 = pep8
 SETUP = $(PYTHON) setup.py
 PYDOCTOR ?= pydoctor
-ifeq ($(shell $(PYTHON) -c "import sys; print(sys.version_info >= (2, 7))"),True)
 TESTRUNNER ?= unittest
-else
-TESTRUNNER ?= unittest2.__main__
-endif
 RUNTEST = PYTHONHASHSEED=random PYTHONPATH=.:$(PYTHONPATH) $(PYTHON) -m $(TESTRUNNER) $(TEST_OPTIONS)
 
 DESTDIR=/

+ 35 - 0
NEWS

@@ -1,3 +1,38 @@
+0.13.0	2016-04-24
+
+ IMPROVEMENTS
+
+  * Support `ssh://` URLs in get_transport_and_path_from_url().
+    (Jelmer Vernooij, #402)
+
+  * Support missing empty line after headers in Git commits and tags.
+    (Nicolas Dandrimont, #413)
+
+  * Fix `dulwich.porcelain.status` when used in empty trees.
+    (Jelmer Vernooij, #415)
+
+  * Return copies of objects in MemoryObjectStore rather than
+    references, making the behaviour more consistent with that of
+    DiskObjectStore. (Félix Mattrat, Jelmer Vernooij)
+
+  * Fix ``dulwich.web`` on Python3. (#295, Jonas Haag)
+
+ CHANGES
+
+  * Drop support for Python 2.6.
+
+  * Fix python3 client web support. (Jelmer Vernooij)
+
+ BUG FIXES
+
+  * Fix hang on Gzip decompression. (Jonas Haag)
+
+  * Don't rely on working tell() and seek() methods
+    on wsgi.input. (Jonas Haag)
+
+  * Support fastexport/fastimport functionality on python3 with newer
+    versions of fastimport (>= 0.9.5). (Jelmer Vernooij, Félix Mattrat)
+
 0.12.0	2015-12-13
 
  IMPROVEMENTS

+ 1 - 1
PKG-INFO

@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: dulwich
-Version: 0.12.0
+Version: 0.13.0
 Summary: Python Git Library
 Home-page: https://www.dulwich.io/
 Author: Jelmer Vernooij

+ 0 - 1
README.md

@@ -47,4 +47,3 @@ Supported versions of Python
 ----------------------------
 
 At the moment, Dulwich supports (and is tested on) CPython 2.6, 2.7, 3.4, 3.5 and Pypy.
-The ``dulwich.web`` module is currently broken on Python 3 (issue #295).

+ 1 - 1
docs/protocol.txt

@@ -51,7 +51,7 @@ The server tells the client what refs it has. The client states which of those
 SHA1's it would like. It then starts to report which SHA1's it has. The server
 ACKs these allowing the client to work out when to stop sending SHA1's. This
 saves a lot of transfer because the client can make decisions like "well if it
-has this SHA, then it has all its parents so i dont need to care about those".
+has this SHA, then it has all its parents so I don't need to care about those".
 When the client stops sending shas, the server can work out an optimal pack and
 then send it to the client.
 

+ 2 - 2
docs/tutorial/file-format.txt

@@ -36,7 +36,7 @@ A commit file looks like this::
  
   <commit message>
 
-But where are the changes you commited? The commit contains a reference to a
+But where are the changes you committed? The commit contains a reference to a
 tree.
 
 The Tree
@@ -63,7 +63,7 @@ content yet. That's where the reference to a blob comes in.
 The Blob
 --------
 
-A blob is simply the content of files you are versionning.
+A blob is simply the content of files you are versioning.
 
 A blob file looks like this::
 

+ 1 - 1
docs/tutorial/object-store.txt

@@ -22,7 +22,7 @@ empty for now, we'll start by adding a new file::
 Of course you could create a blob from an existing file using ``from_file``
 instead.
 
-As said in the introduction, file content is separed from file name. Let's
+As said in the introduction, file content is separated from file name. Let's
 give this content a name::
 
   >>> from dulwich.objects import Tree

+ 1 - 1
docs/tutorial/repo.txt

@@ -30,7 +30,7 @@ Let's create a folder and turn it into a repository, like ``git init`` would::
   >>> repo
   <Repo at 'myrepo'>
 
-You can already look a the structure of the "myrepo/.git" folder, though it
+You can already look at the structure of the "myrepo/.git" folder, though it
 is mostly empty for now.
 
 Opening an existing repository

+ 1 - 1
dulwich.egg-info/PKG-INFO

@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: dulwich
-Version: 0.12.0
+Version: 0.13.0
 Summary: Python Git Library
 Home-page: https://www.dulwich.io/
 Author: Jelmer Vernooij

+ 1 - 1
dulwich.egg-info/SOURCES.txt

@@ -1,4 +1,5 @@
 .testr.conf
+.travis.yml
 AUTHORS
 CONTRIBUTING
 COPYING
@@ -32,7 +33,6 @@ docs/tutorial/remote.txt
 docs/tutorial/repo.txt
 docs/tutorial/tag.txt
 dulwich/__init__.py
-dulwich/_compat.py
 dulwich/_diff_tree.c
 dulwich/_objects.c
 dulwich/_pack.c

+ 1 - 1
dulwich/__init__.py

@@ -21,4 +21,4 @@
 
 """Python implementation of the Git file formats and protocols."""
 
-__version__ = (0, 12, 0)
+__version__ = (0, 13, 0)

+ 0 - 963
dulwich/_compat.py

@@ -1,963 +0,0 @@
-# _compat.py -- For dealing with python2.6 oddness
-# Copyright (C) 2012-2014 Jelmer Vernooij and others.
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; version 2
-# of the License or (at your option) a later version of the License.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
-# MA  02110-1301, USA.
-
-"""Misc utilities to work with python <2.7.
-
-These utilities can all be deleted when dulwich decides it wants to stop
-support for python <2.7.
-"""
-
-# Backport of OrderedDict() class that runs on Python 2.4, 2.5, 2.6, 2.7 and
-# pypy. Passes Python2.7's test suite and incorporates all the latest updates.
-# Copyright (C) Raymond Hettinger, MIT license
-
-try:
-    from thread import get_ident as _get_ident
-except ImportError:
-    from dummy_thread import get_ident as _get_ident
-
-try:
-    from _abcoll import KeysView, ValuesView, ItemsView
-except ImportError:
-    pass
-
-class OrderedDict(dict):
-    'Dictionary that remembers insertion order'
-    # An inherited dict maps keys to values.
-    # The inherited dict provides __getitem__, __len__, __contains__, and get.
-    # The remaining methods are order-aware.
-    # Big-O running times for all methods are the same as for regular
-    # dictionaries.
-
-    # The internal self.__map dictionary maps keys to links in a doubly linked
-    # list. The circular doubly linked list starts and ends with a sentinel
-    # element. The sentinel element never gets deleted (this simplifies the
-    # algorithm). Each link is stored as a list of length three:  [PREV, NEXT,
-    # KEY].
-
-    def __init__(self, *args, **kwds):
-        '''Initialize an ordered dictionary.  Signature is the same as for
-        regular dictionaries, but keyword arguments are not recommended
-        because their insertion order is arbitrary.
-
-        '''
-        if len(args) > 1:
-            raise TypeError('expected at most 1 arguments, got %d' % len(args))
-        try:
-            self.__root
-        except AttributeError:
-            self.__root = root = []                     # sentinel node
-            root[:] = [root, root, None]
-            self.__map = {}
-        self.__update(*args, **kwds)
-
-    def __setitem__(self, key, value, dict_setitem=dict.__setitem__):
-        'od.__setitem__(i, y) <==> od[i]=y'
-        # Setting a new item creates a new link which goes at the end of the
-        # linked list, and the inherited dictionary is updated with the new
-        # key/value pair.
-        if key not in self:
-            root = self.__root
-            last = root[0]
-            last[1] = root[0] = self.__map[key] = [last, root, key]
-        dict_setitem(self, key, value)
-
-    def __delitem__(self, key, dict_delitem=dict.__delitem__):
-        'od.__delitem__(y) <==> del od[y]'
-        # Deleting an existing item uses self.__map to find the link which is
-        # then removed by updating the links in the predecessor and successor
-        # nodes.
-        dict_delitem(self, key)
-        link_prev, link_next, key = self.__map.pop(key)
-        link_prev[1] = link_next
-        link_next[0] = link_prev
-
-    def __iter__(self):
-        'od.__iter__() <==> iter(od)'
-        root = self.__root
-        curr = root[1]
-        while curr is not root:
-            yield curr[2]
-            curr = curr[1]
-
-    def __reversed__(self):
-        'od.__reversed__() <==> reversed(od)'
-        root = self.__root
-        curr = root[0]
-        while curr is not root:
-            yield curr[2]
-            curr = curr[0]
-
-    def clear(self):
-        'od.clear() -> None.  Remove all items from od.'
-        try:
-            for node in self.__map.itervalues():
-                del node[:]
-            root = self.__root
-            root[:] = [root, root, None]
-            self.__map.clear()
-        except AttributeError:
-            pass
-        dict.clear(self)
-
-    def popitem(self, last=True):
-        """od.popitem() -> (k, v), return and remove a (key, value) pair.
-        Pairs are returned in LIFO order if last is true or FIFO order if false.
-
-        """
-        if not self:
-            raise KeyError('dictionary is empty')
-        root = self.__root
-        if last:
-            link = root[0]
-            link_prev = link[0]
-            link_prev[1] = root
-            root[0] = link_prev
-        else:
-            link = root[1]
-            link_next = link[1]
-            root[1] = link_next
-            link_next[0] = root
-        key = link[2]
-        del self.__map[key]
-        value = dict.pop(self, key)
-        return key, value
-
-    # -- the following methods do not depend on the internal structure --
-
-    def keys(self):
-        """'od.keys() -> list of keys in od"""
-        return list(self)
-
-    def values(self):
-        """od.values() -> list of values in od"""
-        return [self[key] for key in self]
-
-    def items(self):
-        """od.items() -> list of (key, value) pairs in od"""
-        return [(key, self[key]) for key in self]
-
-    def iterkeys(self):
-        """od.iterkeys() -> an iterator over the keys in od"""
-        return iter(self)
-
-    def itervalues(self):
-        """od.itervalues -> an iterator over the values in od"""
-        for k in self:
-            yield self[k]
-
-    def iteritems(self):
-        """od.iteritems -> an iterator over the (key, value) items in od"""
-        for k in self:
-            yield (k, self[k])
-
-    def update(*args, **kwds):
-        """od.update(E, F) -> None.  Update od from dict/iterable E and F.
-
-        If E is a dict instance, does:           for k in E: od[k] = E[k]
-        If E has a .keys() method, does:         for k in E.keys(): od[k] = E[k]
-        Or if E is an iterable of items, does:   for k, v in E: od[k] = v
-        In either case, this is followed by:     for k, v in F.items(): od[k] = v
-        """
-        if len(args) > 2:
-            raise TypeError('update() takes at most 2 positional '
-                            'arguments (%d given)' % (len(args),))
-        elif not args:
-            raise TypeError('update() takes at least 1 argument (0 given)')
-        self = args[0]
-        # Make progressively weaker assumptions about "other"
-        other = ()
-        if len(args) == 2:
-            other = args[1]
-        if isinstance(other, dict):
-            for key in other:
-                self[key] = other[key]
-        elif hasattr(other, 'keys'):
-            for key in other.keys():
-                self[key] = other[key]
-        else:
-            for key, value in other:
-                self[key] = value
-        for key, value in kwds.items():
-            self[key] = value
-
-    __update = update  # let subclasses override update without breaking
-                       # __init__
-
-    __marker = object()
-
-    def pop(self, key, default=__marker):
-        """od.pop(k[,d]) -> v, remove specified key and return the corresponding value.
-        If key is not found, d is returned if given, otherwise KeyError is raised.
-
-        """
-        if key in self:
-            result = self[key]
-            del self[key]
-            return result
-        if default is self.__marker:
-            raise KeyError(key)
-        return default
-
-    def setdefault(self, key, default=None):
-        'od.setdefault(k[,d]) -> od.get(k,d), also set od[k]=d if k not in od'
-        if key in self:
-            return self[key]
-        self[key] = default
-        return default
-
-    def __repr__(self, _repr_running={}):
-        'od.__repr__() <==> repr(od)'
-        call_key = id(self), _get_ident()
-        if call_key in _repr_running:
-            return '...'
-        _repr_running[call_key] = 1
-        try:
-            if not self:
-                return '%s()' % (self.__class__.__name__,)
-            return '%s(%r)' % (self.__class__.__name__, self.items())
-        finally:
-            del _repr_running[call_key]
-
-    def __reduce__(self):
-        'Return state information for pickling'
-        items = [[k, self[k]] for k in self]
-        inst_dict = vars(self).copy()
-        for k in vars(OrderedDict()):
-            inst_dict.pop(k, None)
-        if inst_dict:
-            return (self.__class__, (items,), inst_dict)
-        return self.__class__, (items,)
-
-    def copy(self):
-        'od.copy() -> a shallow copy of od'
-        return self.__class__(self)
-
-    @classmethod
-    def fromkeys(cls, iterable, value=None):
-        '''OD.fromkeys(S[, v]) -> New ordered dictionary with keys from S
-        and values equal to v (which defaults to None).
-
-        '''
-        d = cls()
-        for key in iterable:
-            d[key] = value
-        return d
-
-    def __eq__(self, other):
-        '''od.__eq__(y) <==> od==y.  Comparison to another OD is order-sensitive
-        while comparison to a regular mapping is order-insensitive.
-
-        '''
-        if isinstance(other, OrderedDict):
-            return len(self)==len(other) and self.items() == other.items()
-        return dict.__eq__(self, other)
-
-    def __ne__(self, other):
-        return not self == other
-
-    # -- the following methods are only used in Python 2.7 --
-
-    def viewkeys(self):
-        "od.viewkeys() -> a set-like object providing a view on od's keys"
-        return KeysView(self)
-
-    def viewvalues(self):
-        "od.viewvalues() -> an object providing a view on od's values"
-        return ValuesView(self)
-
-    def viewitems(self):
-        "od.viewitems() -> a set-like object providing a view on od's items"
-        return ItemsView(self)
-
-
-# Copyright 2007 Google, Inc. All Rights Reserved.
-# Licensed to PSF under a Contributor Agreement.
-
-from abc import ABCMeta, abstractmethod
-import sys
-
-### ONE-TRICK PONIES ###
-
-def _hasattr(C, attr):
-    try:
-        return any(attr in B.__dict__ for B in C.__mro__)
-    except AttributeError:
-        # Old-style class
-        return hasattr(C, attr)
-
-
-class Hashable:
-    __metaclass__ = ABCMeta
-
-    @abstractmethod
-    def __hash__(self):
-        return 0
-
-    @classmethod
-    def __subclasshook__(cls, C):
-        if cls is Hashable:
-            try:
-                for B in C.__mro__:
-                    if "__hash__" in B.__dict__:
-                        if B.__dict__["__hash__"]:
-                            return True
-                        break
-            except AttributeError:
-                # Old-style class
-                if getattr(C, "__hash__", None):
-                    return True
-        return NotImplemented
-
-
-class Iterable:
-    __metaclass__ = ABCMeta
-
-    @abstractmethod
-    def __iter__(self):
-        while False:
-            yield None
-
-    @classmethod
-    def __subclasshook__(cls, C):
-        if cls is Iterable:
-            if _hasattr(C, "__iter__"):
-                return True
-        return NotImplemented
-
-Iterable.register(str)
-
-
-class Iterator(Iterable):
-
-    @abstractmethod
-    def next(self):
-        'Return the next item from the iterator. When exhausted, raise StopIteration'
-        raise StopIteration
-
-    def __iter__(self):
-        return self
-
-    @classmethod
-    def __subclasshook__(cls, C):
-        if cls is Iterator:
-            if _hasattr(C, "next") and _hasattr(C, "__iter__"):
-                return True
-        return NotImplemented
-
-
-class Sized:
-    __metaclass__ = ABCMeta
-
-    @abstractmethod
-    def __len__(self):
-        return 0
-
-    @classmethod
-    def __subclasshook__(cls, C):
-        if cls is Sized:
-            if _hasattr(C, "__len__"):
-                return True
-        return NotImplemented
-
-
-class Container:
-    __metaclass__ = ABCMeta
-
-    @abstractmethod
-    def __contains__(self, x):
-        return False
-
-    @classmethod
-    def __subclasshook__(cls, C):
-        if cls is Container:
-            if _hasattr(C, "__contains__"):
-                return True
-        return NotImplemented
-
-
-class Callable:
-    __metaclass__ = ABCMeta
-
-    @abstractmethod
-    def __call__(self, *args, **kwds):
-        return False
-
-    @classmethod
-    def __subclasshook__(cls, C):
-        if cls is Callable:
-            if _hasattr(C, "__call__"):
-                return True
-        return NotImplemented
-
-
-### SETS ###
-
-
-class Set(Sized, Iterable, Container):
-    """A set is a finite, iterable container.
-
-    This class provides concrete generic implementations of all
-    methods except for __contains__, __iter__ and __len__.
-
-    To override the comparisons (presumably for speed, as the
-    semantics are fixed), all you have to do is redefine __le__ and
-    then the other operations will automatically follow suit.
-    """
-
-    def __le__(self, other):
-        if not isinstance(other, Set):
-            return NotImplemented
-        if len(self) > len(other):
-            return False
-        for elem in self:
-            if elem not in other:
-                return False
-        return True
-
-    def __lt__(self, other):
-        if not isinstance(other, Set):
-            return NotImplemented
-        return len(self) < len(other) and self.__le__(other)
-
-    def __gt__(self, other):
-        if not isinstance(other, Set):
-            return NotImplemented
-        return len(self) > len(other) and self.__ge__(other)
-
-    def __ge__(self, other):
-        if not isinstance(other, Set):
-            return NotImplemented
-        if len(self) < len(other):
-            return False
-        for elem in other:
-            if elem not in self:
-                return False
-        return True
-
-    def __eq__(self, other):
-        if not isinstance(other, Set):
-            return NotImplemented
-        return len(self) == len(other) and self.__le__(other)
-
-    def __ne__(self, other):
-        return not (self == other)
-
-    @classmethod
-    def _from_iterable(cls, it):
-        '''Construct an instance of the class from any iterable input.
-
-        Must override this method if the class constructor signature
-        does not accept an iterable for an input.
-        '''
-        return cls(it)
-
-    def __and__(self, other):
-        if not isinstance(other, Iterable):
-            return NotImplemented
-        return self._from_iterable(value for value in other if value in self)
-
-    __rand__ = __and__
-
-    def isdisjoint(self, other):
-        'Return True if two sets have a null intersection.'
-        for value in other:
-            if value in self:
-                return False
-        return True
-
-    def __or__(self, other):
-        if not isinstance(other, Iterable):
-            return NotImplemented
-        chain = (e for s in (self, other) for e in s)
-        return self._from_iterable(chain)
-
-    __ror__ = __or__
-
-    def __sub__(self, other):
-        if not isinstance(other, Set):
-            if not isinstance(other, Iterable):
-                return NotImplemented
-            other = self._from_iterable(other)
-        return self._from_iterable(value for value in self
-                                   if value not in other)
-
-    def __rsub__(self, other):
-        if not isinstance(other, Set):
-            if not isinstance(other, Iterable):
-                return NotImplemented
-            other = self._from_iterable(other)
-        return self._from_iterable(value for value in other
-                                   if value not in self)
-
-    def __xor__(self, other):
-        if not isinstance(other, Set):
-            if not isinstance(other, Iterable):
-                return NotImplemented
-            other = self._from_iterable(other)
-        return (self - other) | (other - self)
-
-    __rxor__ = __xor__
-
-    # Sets are not hashable by default, but subclasses can change this
-    __hash__ = None
-
-    def _hash(self):
-        """Compute the hash value of a set.
-
-        Note that we don't define __hash__: not all sets are hashable.
-        But if you define a hashable set type, its __hash__ should
-        call this function.
-
-        This must be compatible __eq__.
-
-        All sets ought to compare equal if they contain the same
-        elements, regardless of how they are implemented, and
-        regardless of the order of the elements; so there's not much
-        freedom for __eq__ or __hash__.  We match the algorithm used
-        by the built-in frozenset type.
-        """
-        MAX = sys.maxint
-        MASK = 2 * MAX + 1
-        n = len(self)
-        h = 1927868237 * (n + 1)
-        h &= MASK
-        for x in self:
-            hx = hash(x)
-            h ^= (hx ^ (hx << 16) ^ 89869747)  * 3644798167
-            h &= MASK
-        h = h * 69069 + 907133923
-        h &= MASK
-        if h > MAX:
-            h -= MASK + 1
-        if h == -1:
-            h = 590923713
-        return h
-
-Set.register(frozenset)
-
-
-class MutableSet(Set):
-    """A mutable set is a finite, iterable container.
-
-    This class provides concrete generic implementations of all
-    methods except for __contains__, __iter__, __len__,
-    add(), and discard().
-
-    To override the comparisons (presumably for speed, as the
-    semantics are fixed), all you have to do is redefine __le__ and
-    then the other operations will automatically follow suit.
-    """
-
-    @abstractmethod
-    def add(self, value):
-        """Add an element."""
-        raise NotImplementedError
-
-    @abstractmethod
-    def discard(self, value):
-        """Remove an element.  Do not raise an exception if absent."""
-        raise NotImplementedError
-
-    def remove(self, value):
-        """Remove an element. If not a member, raise a KeyError."""
-        if value not in self:
-            raise KeyError(value)
-        self.discard(value)
-
-    def pop(self):
-        """Return the popped value.  Raise KeyError if empty."""
-        it = iter(self)
-        try:
-            value = next(it)
-        except StopIteration:
-            raise KeyError
-        self.discard(value)
-        return value
-
-    def clear(self):
-        """This is slow (creates N new iterators!) but effective."""
-        try:
-            while True:
-                self.pop()
-        except KeyError:
-            pass
-
-    def __ior__(self, it):
-        for value in it:
-            self.add(value)
-        return self
-
-    def __iand__(self, it):
-        for value in (self - it):
-            self.discard(value)
-        return self
-
-    def __ixor__(self, it):
-        if it is self:
-            self.clear()
-        else:
-            if not isinstance(it, Set):
-                it = self._from_iterable(it)
-            for value in it:
-                if value in self:
-                    self.discard(value)
-                else:
-                    self.add(value)
-        return self
-
-    def __isub__(self, it):
-        if it is self:
-            self.clear()
-        else:
-            for value in it:
-                self.discard(value)
-        return self
-
-MutableSet.register(set)
-
-
-### MAPPINGS ###
-
-
-class Mapping(Sized, Iterable, Container):
-
-    """A Mapping is a generic container for associating key/value
-    pairs.
-
-    This class provides concrete generic implementations of all
-    methods except for __getitem__, __iter__, and __len__.
-
-    """
-
-    @abstractmethod
-    def __getitem__(self, key):
-        raise KeyError
-
-    def get(self, key, default=None):
-        'D.get(k[,d]) -> D[k] if k in D, else d.  d defaults to None.'
-        try:
-            return self[key]
-        except KeyError:
-            return default
-
-    def __contains__(self, key):
-        try:
-            self[key]
-        except KeyError:
-            return False
-        else:
-            return True
-
-    def iterkeys(self):
-        'D.iterkeys() -> an iterator over the keys of D'
-        return iter(self)
-
-    def itervalues(self):
-        'D.itervalues() -> an iterator over the values of D'
-        for key in self:
-            yield self[key]
-
-    def iteritems(self):
-        'D.iteritems() -> an iterator over the (key, value) items of D'
-        for key in self:
-            yield (key, self[key])
-
-    def keys(self):
-        "D.keys() -> list of D's keys"
-        return list(self)
-
-    def items(self):
-        "D.items() -> list of D's (key, value) pairs, as 2-tuples"
-        return [(key, self[key]) for key in self]
-
-    def values(self):
-        "D.values() -> list of D's values"
-        return [self[key] for key in self]
-
-    # Mappings are not hashable by default, but subclasses can change this
-    __hash__ = None
-
-    def __eq__(self, other):
-        if not isinstance(other, Mapping):
-            return NotImplemented
-        return dict(self.items()) == dict(other.items())
-
-    def __ne__(self, other):
-        return not (self == other)
-
-class MappingView(Sized):
-
-    def __init__(self, mapping):
-        self._mapping = mapping
-
-    def __len__(self):
-        return len(self._mapping)
-
-    def __repr__(self):
-        return '{0.__class__.__name__}({0._mapping!r})'.format(self)
-
-
-class KeysView(MappingView, Set):
-
-    @classmethod
-    def _from_iterable(self, it):
-        return set(it)
-
-    def __contains__(self, key):
-        return key in self._mapping
-
-    def __iter__(self):
-        for key in self._mapping:
-            yield key
-
-
-class ItemsView(MappingView, Set):
-
-    @classmethod
-    def _from_iterable(self, it):
-        return set(it)
-
-    def __contains__(self, item):
-        key, value = item
-        try:
-            v = self._mapping[key]
-        except KeyError:
-            return False
-        else:
-            return v == value
-
-    def __iter__(self):
-        for key in self._mapping:
-            yield (key, self._mapping[key])
-
-
-class ValuesView(MappingView):
-
-    def __contains__(self, value):
-        for key in self._mapping:
-            if value == self._mapping[key]:
-                return True
-        return False
-
-    def __iter__(self):
-        for key in self._mapping:
-            yield self._mapping[key]
-
-
-class MutableMapping(Mapping):
-
-    """A MutableMapping is a generic container for associating
-    key/value pairs.
-
-    This class provides concrete generic implementations of all
-    methods except for __getitem__, __setitem__, __delitem__,
-    __iter__, and __len__.
-
-    """
-
-    @abstractmethod
-    def __setitem__(self, key, value):
-        raise KeyError
-
-    @abstractmethod
-    def __delitem__(self, key):
-        raise KeyError
-
-    __marker = object()
-
-    def pop(self, key, default=__marker):
-        '''D.pop(k[,d]) -> v, remove specified key and return the corresponding value.
-          If key is not found, d is returned if given, otherwise KeyError is raised.
-        '''
-        try:
-            value = self[key]
-        except KeyError:
-            if default is self.__marker:
-                raise
-            return default
-        else:
-            del self[key]
-            return value
-
-    def popitem(self):
-        '''D.popitem() -> (k, v), remove and return some (key, value) pair
-           as a 2-tuple; but raise KeyError if D is empty.
-        '''
-        try:
-            key = next(iter(self))
-        except StopIteration:
-            raise KeyError
-        value = self[key]
-        del self[key]
-        return key, value
-
-    def clear(self):
-        'D.clear() -> None.  Remove all items from D.'
-        try:
-            while True:
-                self.popitem()
-        except KeyError:
-            pass
-
-    def update(*args, **kwds):
-        ''' D.update([E, ]**F) -> None.  Update D from mapping/iterable E and F.
-            If E present and has a .keys() method, does:     for k in E: D[k] = E[k]
-            If E present and lacks .keys() method, does:     for (k, v) in E: D[k] = v
-            In either case, this is followed by: for k, v in F.items(): D[k] = v
-        '''
-        if len(args) > 2:
-            raise TypeError("update() takes at most 2 positional "
-                            "arguments ({} given)".format(len(args)))
-        elif not args:
-            raise TypeError("update() takes at least 1 argument (0 given)")
-        self = args[0]
-        other = args[1] if len(args) >= 2 else ()
-
-        if isinstance(other, Mapping):
-            for key in other:
-                self[key] = other[key]
-        elif hasattr(other, "keys"):
-            for key in other.keys():
-                self[key] = other[key]
-        else:
-            for key, value in other:
-                self[key] = value
-        for key, value in kwds.items():
-            self[key] = value
-
-    def setdefault(self, key, default=None):
-        'D.setdefault(k[,d]) -> D.get(k,d), also set D[k]=d if k not in D'
-        try:
-            return self[key]
-        except KeyError:
-            self[key] = default
-        return default
-
-MutableMapping.register(dict)
-
-
-### SEQUENCES ###
-
-
-class Sequence(Sized, Iterable, Container):
-    """All the operations on a read-only sequence.
-
-    Concrete subclasses must override __new__ or __init__,
-    __getitem__, and __len__.
-    """
-
-    @abstractmethod
-    def __getitem__(self, index):
-        raise IndexError
-
-    def __iter__(self):
-        i = 0
-        try:
-            while True:
-                v = self[i]
-                yield v
-                i += 1
-        except IndexError:
-            return
-
-    def __contains__(self, value):
-        for v in self:
-            if v == value:
-                return True
-        return False
-
-    def __reversed__(self):
-        for i in reversed(range(len(self))):
-            yield self[i]
-
-    def index(self, value):
-        '''S.index(value) -> integer -- return first index of value.
-           Raises ValueError if the value is not present.
-        '''
-        for i, v in enumerate(self):
-            if v == value:
-                return i
-        raise ValueError
-
-    def count(self, value):
-        'S.count(value) -> integer -- return number of occurrences of value'
-        return sum(1 for v in self if v == value)
-
-Sequence.register(tuple)
-Sequence.register(basestring)
-Sequence.register(buffer)
-Sequence.register(xrange)
-
-
-class MutableSequence(Sequence):
-
-    """All the operations on a read-only sequence.
-
-    Concrete subclasses must provide __new__ or __init__,
-    __getitem__, __setitem__, __delitem__, __len__, and insert().
-
-    """
-
-    @abstractmethod
-    def __setitem__(self, index, value):
-        raise IndexError
-
-    @abstractmethod
-    def __delitem__(self, index):
-        raise IndexError
-
-    @abstractmethod
-    def insert(self, index, value):
-        'S.insert(index, object) -- insert object before index'
-        raise IndexError
-
-    def append(self, value):
-        'S.append(object) -- append object to the end of the sequence'
-        self.insert(len(self), value)
-
-    def reverse(self):
-        'S.reverse() -- reverse *IN PLACE*'
-        n = len(self)
-        for i in range(n//2):
-            self[i], self[n-i-1] = self[n-i-1], self[i]
-
-    def extend(self, values):
-        'S.extend(iterable) -- extend sequence by appending elements from the iterable'
-        for v in values:
-            self.append(v)
-
-    def pop(self, index=-1):
-        '''S.pop([index]) -> item -- remove and return item at index (default last).
-           Raise IndexError if list is empty or index is out of range.
-        '''
-        v = self[index]
-        del self[index]
-        return v
-
-    def remove(self, value):
-        '''S.remove(value) -- remove first occurrence of value.
-           Raise ValueError if the value is not present.
-        '''
-        del self[self.index(value)]
-
-    def __iadd__(self, values):
-        self.extend(values)
-        return self
-
-MutableSequence.register(list)

+ 8 - 8
dulwich/_pack.c

@@ -47,10 +47,10 @@ static int py_is_sha(PyObject *sha)
 }
 
 
-static size_t get_delta_header_size(uint8_t *delta, int *index, int length)
+static size_t get_delta_header_size(uint8_t *delta, size_t *index, size_t length)
 {
 	size_t size = 0;
-	int i = 0;
+	size_t i = 0;
 	while ((*index) < length) {
 		uint8_t cmd = delta[*index];
 		(*index)++;
@@ -89,10 +89,10 @@ static PyObject *py_chunked_as_string(PyObject *py_buf)
 static PyObject *py_apply_delta(PyObject *self, PyObject *args)
 {
 	uint8_t *src_buf, *delta;
-	int src_buf_len, delta_len;
+	size_t src_buf_len, delta_len;
 	size_t src_size, dest_size;
 	size_t outindex = 0;
-	int index;
+	size_t index;
 	uint8_t *out;
 	PyObject *ret, *py_src_buf, *py_delta, *ret_list;
 
@@ -110,16 +110,16 @@ static PyObject *py_apply_delta(PyObject *self, PyObject *args)
 	}
 
 	src_buf = (uint8_t *)PyString_AS_STRING(py_src_buf);
-	src_buf_len = PyString_GET_SIZE(py_src_buf);
+	src_buf_len = (size_t)PyString_GET_SIZE(py_src_buf);
 
 	delta = (uint8_t *)PyString_AS_STRING(py_delta);
-	delta_len = PyString_GET_SIZE(py_delta);
+	delta_len = (size_t)PyString_GET_SIZE(py_delta);
 
 	index = 0;
 	src_size = get_delta_header_size(delta, &index, delta_len);
 	if (src_size != src_buf_len) {
 		PyErr_Format(PyExc_ApplyDeltaError,
-					 "Unexpected source buffer size: %lu vs %d", src_size, src_buf_len);
+					 "Unexpected source buffer size: %lu vs %ld", src_size, src_buf_len);
 		Py_DECREF(py_src_buf);
 		Py_DECREF(py_delta);
 		return NULL;
@@ -134,7 +134,7 @@ static PyObject *py_apply_delta(PyObject *self, PyObject *args)
 	}
 	out = (uint8_t *)PyString_AsString(ret);
 	while (index < delta_len) {
-		char cmd = delta[index];
+		uint8_t cmd = delta[index];
 		index++;
 		if (cmd & 0x80) {
 			size_t cp_off = 0, cp_size = 0;

+ 32 - 18
dulwich/client.py

@@ -234,15 +234,17 @@ class GitClient(object):
         if determine_wants is None:
             determine_wants = target.object_store.determine_wants_all
         if CAPABILITY_THIN_PACK in self._fetch_capabilities:
-           # TODO(jelmer): Avoid reading entire file into memory and
-           # only processing it after the whole file has been fetched.
-           f = BytesIO()
-           def commit():
-              if f.tell():
-                f.seek(0)
-                target.object_store.add_thin_pack(f.read, None)
+            # TODO(jelmer): Avoid reading entire file into memory and
+            # only processing it after the whole file has been fetched.
+            f = BytesIO()
+            def commit():
+                if f.tell():
+                    f.seek(0)
+                    target.object_store.add_thin_pack(f.read, None)
+            def abort():
+                pass
         else:
-           f, commit, abort = target.object_store.add_pack()
+            f, commit, abort = target.object_store.add_pack()
         try:
             result = self.fetch_pack(
                 path, determine_wants, target.get_graph_walker(), f.write,
@@ -978,16 +980,21 @@ class HttpGitClient(GitClient):
         url = urlparse.urljoin(url, "info/refs")
         headers = {}
         if self.dumb is not False:
-            url += "?service=%s" % service
-            headers["Content-Type"] = "application/x-%s-request" % service
+            url += "?service=%s" % service.decode('ascii')
+            headers["Content-Type"] = "application/x-%s-request" % (
+                service.decode('ascii'))
         resp = self._http_request(url, headers)
         try:
-            self.dumb = (not resp.info().gettype().startswith("application/x-git-"))
+            content_type = resp.info().gettype()
+        except AttributeError:
+            content_type = resp.info().get_content_type()
+        try:
+            self.dumb = (not content_type.startswith("application/x-git-"))
             if not self.dumb:
                 proto = Protocol(resp.read, None)
                 # The first line should mention the service
                 pkts = list(proto.read_pkt_seq())
-                if pkts != [('# service=%s\n' % service)]:
+                if pkts != [b'# service=' + service + b'\n']:
                     raise GitProtocolError(
                         "unexpected first line %r from smart server" % pkts)
                 return read_pkt_refs(proto)
@@ -999,11 +1006,18 @@ class HttpGitClient(GitClient):
     def _smart_request(self, service, url, data):
         assert url[-1] == "/"
         url = urlparse.urljoin(url, service)
-        headers = {"Content-Type": "application/x-%s-request" % service}
+        headers = {
+            "Content-Type": "application/x-%s-request" % service
+        }
         resp = self._http_request(url, headers, data)
-        if resp.info().gettype() != ("application/x-%s-result" % service):
+        try:
+            content_type = resp.info().gettype()
+        except AttributeError:
+            content_type = resp.info().get_content_type()
+        if content_type != (
+                "application/x-%s-result" % service):
             raise GitProtocolError("Invalid content-type from server: %s"
-                % resp.info().gettype())
+                % content_type)
         return resp
 
     def send_pack(self, path, determine_wants, generate_pack_contents,
@@ -1043,7 +1057,7 @@ class HttpGitClient(GitClient):
         objects = generate_pack_contents(have, want)
         if len(objects) > 0:
             write_pack(req_proto.write_file(), objects)
-        resp = self._smart_request(b"git-receive-pack", url,
+        resp = self._smart_request("git-receive-pack", url,
                                    data=req_data.getvalue())
         try:
             resp_proto = Protocol(resp.read, None)
@@ -1081,7 +1095,7 @@ class HttpGitClient(GitClient):
             req_proto, negotiated_capabilities, graph_walker, wants,
             lambda: False)
         resp = self._smart_request(
-            b"git-upload-pack", url, data=req_data.getvalue())
+            "git-upload-pack", url, data=req_data.getvalue())
         try:
             resp_proto = Protocol(resp.read, None)
             self._handle_upload_pack_tail(resp_proto, negotiated_capabilities,
@@ -1112,7 +1126,7 @@ def get_transport_and_path_from_url(url, config=None, **kwargs):
     if parsed.scheme == 'git':
         return (TCPGitClient(parsed.hostname, port=parsed.port, **kwargs),
                 parsed.path)
-    elif parsed.scheme == 'git+ssh':
+    elif parsed.scheme in ('git+ssh', 'ssh'):
         path = parsed.path
         if path.startswith('/'):
             path = parsed.path[1:]

+ 4 - 10
dulwich/config.py

@@ -27,16 +27,10 @@ TODO:
 import errno
 import os
 
-try:
-    from collections import (
-        OrderedDict,
-        MutableMapping,
-        )
-except ImportError:
-    from dulwich._compat import (
-        OrderedDict,
-        MutableMapping
-        )
+from collections import (
+    OrderedDict,
+    MutableMapping,
+    )
 
 
 from dulwich.file import GitFile

+ 1 - 4
dulwich/contrib/test_swift.py

@@ -25,10 +25,7 @@ import posixpath
 
 from time import time
 from io import BytesIO
-try:
-    from unittest import skipIf
-except ImportError:
-    from unittest2 import skipIf
+from unittest import skipIf
 
 from dulwich.tests import (
     TestCase,

+ 23 - 16
dulwich/fastexport.py

@@ -20,6 +20,8 @@
 
 """Fast export/import functionality."""
 
+import sys
+
 from dulwich.index import (
     commit_tree,
     )
@@ -28,6 +30,9 @@ from dulwich.objects import (
     Commit,
     Tag,
     )
+from fastimport import __version__ as fastimport_version
+if fastimport_version <= (0, 9, 5) and sys.version_info[0] == 3 and sys.version_info[1] < 5:
+    raise ImportError("Older versions of fastimport don't support python3<3.5")
 from fastimport import (
     commands,
     errors as fastimport_errors,
@@ -39,8 +44,8 @@ import stat
 
 
 def split_email(text):
-    (name, email) = text.rsplit(" <", 1)
-    return (name, email.rstrip(">"))
+    (name, email) = text.rsplit(b" <", 1)
+    return (name, email.rstrip(b">"))
 
 
 class GitFastExporter(object):
@@ -53,11 +58,11 @@ class GitFastExporter(object):
         self._marker_idx = 0
 
     def print_cmd(self, cmd):
-        self.outf.write("%r\n" % cmd)
+        self.outf.write(getattr(cmd, "__bytes__", cmd.__repr__)() + b"\n")
 
     def _allocate_marker(self):
         self._marker_idx+=1
-        return str(self._marker_idx)
+        return ("%d" % (self._marker_idx,)).encode('ascii')
 
     def _export_blob(self, blob):
         marker = self._allocate_marker()
@@ -82,8 +87,10 @@ class GitFastExporter(object):
             if old_path != new_path and old_path is not None:
                 yield commands.FileRenameCommand(old_path, new_path)
             if old_mode != new_mode or old_hexsha != new_hexsha:
-                yield commands.FileModifyCommand(new_path, new_mode, marker,
-                    None)
+                prefixed_marker = b':' + marker
+                yield commands.FileModifyCommand(
+                    new_path, new_mode, prefixed_marker, None
+                )
 
     def _export_commit(self, commit, ref, base_tree=None):
         file_cmds = list(self._iter_files(base_tree, commit.tree))
@@ -148,10 +155,10 @@ class GitImportProcessor(processor.ImportProcessor):
         (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 = author_name + b" <" + author_email + b">"
         commit.author_timezone = author_timezone
         commit.author_time = int(author_timestamp)
-        commit.committer = "%s <%s>" % (committer_name, committer_email)
+        commit.committer = committer_name + b" <" + committer_email + b">"
         commit.commit_timezone = commit_timezone
         commit.commit_time = int(commit_timestamp)
         commit.message = cmd.message
@@ -159,32 +166,32 @@ class GitImportProcessor(processor.ImportProcessor):
         if cmd.from_:
             self._reset_base(cmd.from_)
         for filecmd in cmd.iter_files():
-            if filecmd.name == "filemodify":
+            if filecmd.name == b"filemodify":
                 if filecmd.data is not None:
                     blob = Blob.from_string(filecmd.data)
                     self.repo.object_store.add(blob)
                     blob_id = blob.id
                 else:
-                    assert filecmd.dataref[0] == ":", \
-                        "non-marker refs not supported yet"
+                    assert filecmd.dataref.startswith(b":"), \
+                        "non-marker refs not supported yet (%r)" % filecmd.dataref
                     blob_id = self.markers[filecmd.dataref[1:]]
                 self._contents[filecmd.path] = (filecmd.mode, blob_id)
-            elif filecmd.name == "filedelete":
+            elif filecmd.name == b"filedelete":
                 del self._contents[filecmd.path]
-            elif filecmd.name == "filecopy":
+            elif filecmd.name == b"filecopy":
                 self._contents[filecmd.dest_path] = self._contents[
                     filecmd.src_path]
-            elif filecmd.name == "filerename":
+            elif filecmd.name == b"filerename":
                 self._contents[filecmd.new_path] = self._contents[
                     filecmd.old_path]
                 del self._contents[filecmd.old_path]
-            elif filecmd.name == "filedeleteall":
+            elif filecmd.name == b"filedeleteall":
                 self._contents = {}
             else:
                 raise Exception("Command %s not supported" % filecmd.name)
         commit.tree = commit_tree(self.repo.object_store,
             ((path, hexsha, mode) for (path, (mode, hexsha)) in
-                self._contents.iteritems()))
+                self._contents.items()))
         if self.last_commit is not None:
             commit.parents.append(self.last_commit)
         commit.parents += cmd.merges

+ 4 - 4
dulwich/object_store.py

@@ -716,7 +716,7 @@ class MemoryObjectStore(BaseObjectStore):
         return obj.type_num, obj.as_raw_string()
 
     def __getitem__(self, name):
-        return self._data[self._to_hexsha(name)]
+        return self._data[self._to_hexsha(name)].copy()
 
     def __delitem__(self, name):
         """Delete an object from this store, for testing only."""
@@ -726,7 +726,7 @@ class MemoryObjectStore(BaseObjectStore):
         """Add a single object to this object store.
 
         """
-        self._data[obj.id] = obj
+        self._data[obj.id] = obj.copy()
 
     def add_objects(self, objects):
         """Add a set of objects to this object store.
@@ -734,7 +734,7 @@ class MemoryObjectStore(BaseObjectStore):
         :param objects: Iterable over a list of objects.
         """
         for obj, path in objects:
-            self._data[obj.id] = obj
+            self.add_object(obj)
 
     def add_pack(self):
         """Add a new pack to this object store.
@@ -750,7 +750,7 @@ class MemoryObjectStore(BaseObjectStore):
             p = PackData.from_file(BytesIO(f.getvalue()), f.tell())
             f.close()
             for obj in PackInflater.for_pack_data(p, self.get_raw):
-                self._data[obj.id] = obj
+                self.add_object(obj)
         def abort():
             pass
         return f, commit, abort

+ 26 - 3
dulwich/objects.py

@@ -602,17 +602,39 @@ def _parse_message(chunks):
     f = BytesIO(b''.join(chunks))
     k = None
     v = ""
+    eof = False
+
+    # Parse the headers
+    #
+    # Headers can contain newlines. The next line is indented with a space.
+    # We store the latest key as 'k', and the accumulated value as 'v'.
     for l in f:
         if l.startswith(b' '):
+            # Indented continuation of the previous line
             v += l[1:]
         else:
             if k is not None:
+                # We parsed a new header, return its value
                 yield (k, v.rstrip(b'\n'))
             if l == b'\n':
                 # Empty line indicates end of headers
                 break
             (k, v) = l.split(b' ', 1)
-    yield (None, f.read())
+
+    else:
+        # We reached end of file before the headers ended. We still need to
+        # return the previous header, then we need to return a None field for
+        # the text.
+        eof = True
+        if k is not None:
+            yield (k, v.rstrip(b'\n'))
+        yield (None, None)
+
+    if not eof:
+        # We didn't reach the end of file while parsing headers. We can return
+        # the rest of the file as a message.
+        yield (None, f.read())
+
     f.close()
 
 
@@ -679,8 +701,9 @@ class Tag(ShaFile):
                 chunks.append(git_line(
                     _TAGGER_HEADER, self._tagger, str(self._tag_time).encode('ascii'),
                     format_timezone(self._tag_timezone, self._tag_timezone_neg_utc)))
-        chunks.append(b'\n') # To close headers
-        chunks.append(self._message)
+        if self._message is not None:
+            chunks.append(b'\n') # To close headers
+            chunks.append(self._message)
         return chunks
 
     def _deserialize(self, chunks):

+ 6 - 1
dulwich/porcelain.py

@@ -663,7 +663,12 @@ def get_tree_changes(repo):
             'delete': [],
             'modify': [],
         }
-        for change in index.changes_from_tree(r.object_store, r[b'HEAD'].tree):
+        try:
+            tree_id = r[b'HEAD'].tree
+        except KeyError:
+            tree_id = None
+
+        for change in index.changes_from_tree(r.object_store, tree_id):
             if not change[0][0]:
                 tracked_changes['add'].append(change[0][1])
             elif not change[0][1]:

+ 1 - 1
dulwich/server.py

@@ -266,7 +266,7 @@ class PackHandler(Handler):
 
 
 class UploadPackHandler(PackHandler):
-    """Protocol handler for uploading a pack to the server."""
+    """Protocol handler for uploading a pack to the client."""
 
     def __init__(self, backend, args, proto, http_req=None,
                  advertise_refs=False):

+ 1 - 4
dulwich/tests/__init__.py

@@ -29,10 +29,7 @@ import tempfile
 
 # If Python itself provides an exception, use that
 import unittest
-if sys.version_info < (2, 7):
-    from unittest2 import SkipTest, TestCase as _TestCase, skipIf, expectedFailure
-else:
-    from unittest import SkipTest, TestCase as _TestCase, skipIf, expectedFailure
+from unittest import SkipTest, TestCase as _TestCase, skipIf, expectedFailure
 
 
 def get_safe_env(env=None):

+ 12 - 12
dulwich/tests/compat/test_client.py

@@ -62,9 +62,6 @@ from dulwich.tests import (
     SkipTest,
     expectedFailure,
     )
-from dulwich.tests.utils import (
-    skipIfPY3,
-    )
 from dulwich.tests.compat.utils import (
     CompatTestCase,
     check_for_daemon,
@@ -436,10 +433,7 @@ class GitHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
                         if len(authorization) == 2:
                             env['REMOTE_USER'] = authorization[0]
         # XXX REMOTE_IDENT
-        if self.headers.typeheader is None:
-            env['CONTENT_TYPE'] = self.headers.type
-        else:
-            env['CONTENT_TYPE'] = self.headers.typeheader
+        env['CONTENT_TYPE'] = self.headers.get('content-type')
         length = self.headers.get('content-length')
         if length:
             env['CONTENT_LENGTH'] = length
@@ -456,9 +450,9 @@ class GitHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
         ua = self.headers.get('user-agent')
         if ua:
             env['HTTP_USER_AGENT'] = ua
-        co = filter(None, self.headers.getheaders('cookie'))
+        co = self.headers.get('cookie')
         if co:
-            env['HTTP_COOKIE'] = ', '.join(co)
+            env['HTTP_COOKIE'] = co
         # XXX Other HTTP_* headers
         # Since we're setting the env in the parent, provide empty
         # values to override previously set values
@@ -466,7 +460,11 @@ class GitHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
                   'HTTP_USER_AGENT', 'HTTP_COOKIE', 'HTTP_REFERER'):
             env.setdefault(k, "")
 
-        self.send_response(200, "Script output follows")
+        self.wfile.write(b"HTTP/1.1 200 Script output follows\r\n")
+        self.wfile.write(
+            ("Server: %s\r\n" % self.server.server_name).encode('ascii'))
+        self.wfile.write(
+            ("Date: %s\r\n" % self.date_time_string()).encode('ascii'))
 
         decoded_query = query.replace('+', ' ')
 
@@ -502,7 +500,6 @@ class HTTPGitServer(BaseHTTPServer.HTTPServer):
         return 'http://%s:%s/' % (self.server_name, self.server_port)
 
 
-@skipIfPY3
 class DulwichHttpClientTest(CompatTestCase, DulwichClientTestBase):
 
     min_git_version = (1, 7, 0, 2)
@@ -528,7 +525,10 @@ class DulwichHttpClientTest(CompatTestCase, DulwichClientTestBase):
         return client.HttpGitClient(self._httpd.get_url())
 
     def _build_path(self, path):
-        return path
+        if sys.version_info[0] == 3:
+            return path.decode('ascii')
+        else:
+            return path
 
     def test_archive(self):
         raise SkipTest("exporting archives not supported over http")

+ 8 - 14
dulwich/tests/compat/test_web.py

@@ -37,9 +37,6 @@ from dulwich.tests import (
     SkipTest,
     skipIf,
     )
-from dulwich.tests.utils import (
-    skipIfPY3,
-    )
 from dulwich.web import (
     make_wsgi_chain,
     HTTPGitApplication,
@@ -81,7 +78,6 @@ class WebTests(ServerTests):
 
 
 @skipIf(sys.platform == 'win32', 'Broken on windows, with very long fail time.')
-@skipIfPY3
 class SmartWebTestCase(WebTests, CompatTestCase):
     """Test cases for smart HTTP server.
 
@@ -91,12 +87,12 @@ class SmartWebTestCase(WebTests, CompatTestCase):
     min_git_version = (1, 6, 6)
 
     def _handlers(self):
-        return {'git-receive-pack': NoSideBand64kReceivePackHandler}
+        return {b'git-receive-pack': NoSideBand64kReceivePackHandler}
 
     def _check_app(self, app):
         receive_pack_handler_cls = app.handlers[b'git-receive-pack']
         caps = receive_pack_handler_cls.capabilities()
-        self.assertFalse(b'side-band-64k' in caps)
+        self.assertNotIn(b'side-band-64k', caps)
 
     def _make_app(self, backend):
         app = make_wsgi_chain(backend, handlers=self._handlers())
@@ -121,7 +117,6 @@ def patch_capabilities(handler, caps_removed):
 
 
 @skipIf(sys.platform == 'win32', 'Broken on windows, with very long fail time.')
-@skipIfPY3
 class SmartWebSideBand64kTestCase(SmartWebTestCase):
     """Test cases for smart HTTP server with side-band-64k support."""
 
@@ -129,8 +124,8 @@ class SmartWebSideBand64kTestCase(SmartWebTestCase):
     min_git_version = (1, 7, 0, 2)
 
     def setUp(self):
-        self.o_uph_cap = patch_capabilities(UploadPackHandler, ("no-done",))
-        self.o_rph_cap = patch_capabilities(ReceivePackHandler, ("no-done",))
+        self.o_uph_cap = patch_capabilities(UploadPackHandler, (b"no-done",))
+        self.o_rph_cap = patch_capabilities(ReceivePackHandler, (b"no-done",))
         super(SmartWebSideBand64kTestCase, self).setUp()
 
     def tearDown(self):
@@ -144,8 +139,8 @@ class SmartWebSideBand64kTestCase(SmartWebTestCase):
     def _check_app(self, app):
         receive_pack_handler_cls = app.handlers[b'git-receive-pack']
         caps = receive_pack_handler_cls.capabilities()
-        self.assertTrue(b'side-band-64k' in caps)
-        self.assertFalse(b'no-done' in caps)
+        self.assertIn(b'side-band-64k', caps)
+        self.assertNotIn(b'no-done', caps)
 
 
 class SmartWebSideBand64kNoDoneTestCase(SmartWebTestCase):
@@ -162,12 +157,11 @@ class SmartWebSideBand64kNoDoneTestCase(SmartWebTestCase):
     def _check_app(self, app):
         receive_pack_handler_cls = app.handlers[b'git-receive-pack']
         caps = receive_pack_handler_cls.capabilities()
-        self.assertTrue(b'side-band-64k' in caps)
-        self.assertTrue(b'no-done' in caps)
+        self.assertIn(b'side-band-64k', caps)
+        self.assertIn(b'no-done', caps)
 
 
 @skipIf(sys.platform == 'win32', 'Broken on windows, with very long fail time.')
-@skipIfPY3
 class DumbWebTestCase(WebTests, CompatTestCase):
     """Test cases for dumb HTTP server."""
 

+ 0 - 3
dulwich/tests/test_archive.py

@@ -19,7 +19,6 @@
 """Tests for archive support."""
 
 from io import BytesIO
-import sys
 import tarfile
 
 from dulwich.archive import tar_stream
@@ -41,8 +40,6 @@ from dulwich.tests.utils import (
 class ArchiveTests(TestCase):
 
     def test_empty(self):
-        if sys.version_info[:2] <= (2, 6):
-            self.skipTest("archive creation known failing on Python2.6")
         store = MemoryObjectStore()
         c1, c2, c3 = build_commit_graph(store, [[1], [2, 1], [3, 1, 2]])
         tree = store[c3.tree]

+ 9 - 1
dulwich/tests/test_client.py

@@ -310,7 +310,7 @@ class TestGetTransportAndPath(TestCase):
         self.assertEqual(1234, c._port)
         self.assertEqual('/bar/baz', path)
 
-    def test_ssh_explicit(self):
+    def test_git_ssh_explicit(self):
         c, path = get_transport_and_path('git+ssh://foo.com/bar/baz')
         self.assertTrue(isinstance(c, SSHGitClient))
         self.assertEqual('foo.com', c.host)
@@ -318,6 +318,14 @@ class TestGetTransportAndPath(TestCase):
         self.assertEqual(None, c.username)
         self.assertEqual('bar/baz', path)
 
+    def test_ssh_explicit(self):
+        c, path = get_transport_and_path('ssh://foo.com/bar/baz')
+        self.assertTrue(isinstance(c, SSHGitClient))
+        self.assertEqual('foo.com', c.host)
+        self.assertEqual(None, c.port)
+        self.assertEqual(None, c.username)
+        self.assertEqual('bar/baz', path)
+
     def test_ssh_port_explicit(self):
         c, path = get_transport_and_path(
             'git+ssh://foo.com:1234/bar/baz')

+ 43 - 43
dulwich/tests/test_fastexport.py

@@ -56,25 +56,25 @@ class GitFastExporterTests(TestCase):
 
     def test_emit_blob(self):
         b = Blob()
-        b.data = "fooBAR"
+        b.data = b"fooBAR"
         self.fastexporter.emit_blob(b)
-        self.assertEqual('blob\nmark :1\ndata 6\nfooBAR\n',
+        self.assertEqual(b'blob\nmark :1\ndata 6\nfooBAR\n',
             self.stream.getvalue())
 
     def test_emit_commit(self):
         b = Blob()
-        b.data = "FOO"
+        b.data = b"FOO"
         t = Tree()
-        t.add("foo", stat.S_IFREG | 0o644, b.id)
+        t.add(b"foo", stat.S_IFREG | 0o644, b.id)
         c = Commit()
-        c.committer = c.author = "Jelmer <jelmer@host>"
+        c.committer = c.author = b"Jelmer <jelmer@host>"
         c.author_time = c.commit_time = 1271345553
         c.author_timezone = c.commit_timezone = 0
-        c.message = "msg"
+        c.message = b"msg"
         c.tree = t.id
         self.store.add_objects([(b, None), (t, None), (c, None)])
-        self.fastexporter.emit_commit(c, "refs/heads/master")
-        self.assertEqual("""blob
+        self.fastexporter.emit_commit(c, b"refs/heads/master")
+        self.assertEqual(b"""blob
 mark :1
 data 3
 FOO
@@ -84,7 +84,7 @@ author Jelmer <jelmer@host> 1271345553 +0000
 committer Jelmer <jelmer@host> 1271345553 +0000
 data 3
 msg
-M 644 1 foo
+M 644 :1 foo
 """, self.stream.getvalue())
 
 
@@ -103,30 +103,30 @@ class GitImportProcessorTests(TestCase):
     def test_reset_handler(self):
         from fastimport import commands
         [c1] = build_commit_graph(self.repo.object_store, [[1]])
-        cmd = commands.ResetCommand("refs/heads/foo", c1.id)
+        cmd = commands.ResetCommand(b"refs/heads/foo", c1.id)
         self.processor.reset_handler(cmd)
-        self.assertEqual(c1.id, self.repo.get_refs()["refs/heads/foo"])
+        self.assertEqual(c1.id, self.repo.get_refs()[b"refs/heads/foo"])
 
     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, [], [])
+        cmd = commands.CommitCommand(b"refs/heads/foo",  b"mrkr",
+            (b"Jelmer", b"jelmer@samba.org", 432432432.0, 3600),
+            (b"Jelmer", b"jelmer@samba.org", 432432432.0, 3600),
+            b"FOO", None, [], [])
         self.processor.commit_handler(cmd)
         commit = self.repo[self.processor.last_commit]
-        self.assertEqual("Jelmer <jelmer@samba.org>", commit.author)
-        self.assertEqual("Jelmer <jelmer@samba.org>", commit.committer)
-        self.assertEqual("FOO", commit.message)
+        self.assertEqual(b"Jelmer <jelmer@samba.org>", commit.author)
+        self.assertEqual(b"Jelmer <jelmer@samba.org>", commit.committer)
+        self.assertEqual(b"FOO", commit.message)
         self.assertEqual([], commit.parents)
         self.assertEqual(432432432.0, commit.commit_time)
         self.assertEqual(432432432.0, commit.author_time)
         self.assertEqual(3600, commit.commit_timezone)
         self.assertEqual(3600, commit.author_timezone)
-        self.assertEqual(commit, self.repo["refs/heads/foo"])
+        self.assertEqual(commit, self.repo[b"refs/heads/foo"])
 
     def test_import_stream(self):
-        markers = self.processor.import_stream(BytesIO("""blob
+        markers = self.processor.import_stream(BytesIO(b"""blob
 mark :1
 data 11
 text for a
@@ -140,31 +140,31 @@ M 100644 :1 a
 
 """))
         self.assertEqual(2, len(markers))
-        self.assertTrue(isinstance(self.repo[markers["1"]], Blob))
-        self.assertTrue(isinstance(self.repo[markers["2"]], Commit))
+        self.assertTrue(isinstance(self.repo[markers[b"1"]], Blob))
+        self.assertTrue(isinstance(self.repo[markers[b"2"]], Commit))
 
     def test_file_add(self):
         from fastimport import commands
-        cmd = commands.BlobCommand("23", "data")
+        cmd = commands.BlobCommand(b"23", b"data")
         self.processor.blob_handler(cmd)
-        cmd = commands.CommitCommand("refs/heads/foo", "mrkr",
-            ("Jelmer", "jelmer@samba.org", 432432432.0, 3600),
-            ("Jelmer", "jelmer@samba.org", 432432432.0, 3600),
-            "FOO", None, [], [commands.FileModifyCommand("path", 0o100644, ":23", None)])
+        cmd = commands.CommitCommand(b"refs/heads/foo", b"mrkr",
+            (b"Jelmer", b"jelmer@samba.org", 432432432.0, 3600),
+            (b"Jelmer", b"jelmer@samba.org", 432432432.0, 3600),
+            b"FOO", None, [], [commands.FileModifyCommand(b"path", 0o100644, b":23", None)])
         self.processor.commit_handler(cmd)
         commit = self.repo[self.processor.last_commit]
         self.assertEqual([
-            ('path', 0o100644, '6320cd248dd8aeaab759d5871f8781b5c0505172')],
+            (b'path', 0o100644, b'6320cd248dd8aeaab759d5871f8781b5c0505172')],
             self.repo[commit.tree].items())
 
     def simple_commit(self):
         from fastimport import commands
-        cmd = commands.BlobCommand("23", "data")
+        cmd = commands.BlobCommand(b"23", b"data")
         self.processor.blob_handler(cmd)
-        cmd = commands.CommitCommand("refs/heads/foo", "mrkr",
-            ("Jelmer", "jelmer@samba.org", 432432432.0, 3600),
-            ("Jelmer", "jelmer@samba.org", 432432432.0, 3600),
-            "FOO", None, [], [commands.FileModifyCommand("path", 0o100644, ":23", None)])
+        cmd = commands.CommitCommand(b"refs/heads/foo", b"mrkr",
+            (b"Jelmer", b"jelmer@samba.org", 432432432.0, 3600),
+            (b"Jelmer", b"jelmer@samba.org", 432432432.0, 3600),
+            b"FOO", None, [], [commands.FileModifyCommand(b"path", 0o100644, b":23", None)])
         self.processor.commit_handler(cmd)
         commit = self.repo[self.processor.last_commit]
         return commit
@@ -176,34 +176,34 @@ M 100644 :1 a
         :return: The created commit object
         """
         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, [], file_cmds)
+        cmd = commands.CommitCommand(b"refs/heads/foo", b"mrkr",
+            (b"Jelmer", b"jelmer@samba.org", 432432432.0, 3600),
+            (b"Jelmer", b"jelmer@samba.org", 432432432.0, 3600),
+            b"FOO", None, [], file_cmds)
         self.processor.commit_handler(cmd)
         return self.repo[self.processor.last_commit]
 
     def test_file_copy(self):
         from fastimport import commands
         self.simple_commit()
-        commit = self.make_file_commit([commands.FileCopyCommand("path", "new_path")])
+        commit = self.make_file_commit([commands.FileCopyCommand(b"path", b"new_path")])
         self.assertEqual([
-            ('new_path', 0o100644, '6320cd248dd8aeaab759d5871f8781b5c0505172'),
-            ('path', 0o100644, '6320cd248dd8aeaab759d5871f8781b5c0505172'),
+            (b'new_path', 0o100644, b'6320cd248dd8aeaab759d5871f8781b5c0505172'),
+            (b'path', 0o100644, b'6320cd248dd8aeaab759d5871f8781b5c0505172'),
             ], self.repo[commit.tree].items())
 
     def test_file_move(self):
         from fastimport import commands
         self.simple_commit()
-        commit = self.make_file_commit([commands.FileRenameCommand("path", "new_path")])
+        commit = self.make_file_commit([commands.FileRenameCommand(b"path", b"new_path")])
         self.assertEqual([
-            ('new_path', 0o100644, '6320cd248dd8aeaab759d5871f8781b5c0505172'),
+            (b'new_path', 0o100644, b'6320cd248dd8aeaab759d5871f8781b5c0505172'),
             ], self.repo[commit.tree].items())
 
     def test_file_delete(self):
         from fastimport import commands
         self.simple_commit()
-        commit = self.make_file_commit([commands.FileDeleteCommand("path")])
+        commit = self.make_file_commit([commands.FileDeleteCommand(b"path")])
         self.assertEqual([], self.repo[commit.tree].items())
 
     def test_file_deleteall(self):

+ 14 - 0
dulwich/tests/test_object_store.py

@@ -87,6 +87,20 @@ class ObjectStoreTests(object):
         # access to a serialized form.
         self.store.add_objects([])
 
+    def test_store_resilience(self):
+        """Test if updating an existing stored object doesn't erase the
+        object from the store.
+        """
+        test_object = make_object(Blob, data=b'data')
+
+        self.store.add_object(test_object)
+        test_object_id = test_object.id
+        test_object.data = test_object.data + b'update'
+        stored_test_object = self.store[test_object_id]
+
+        self.assertNotEqual(test_object.id, stored_test_object.id)
+        self.assertEqual(stored_test_object.id, test_object_id)
+
     def test_add_object(self):
         self.store.add_object(testobject)
         self.assertEqual(set([testobject.id]), set(self.store))

+ 26 - 1
dulwich/tests/test_objects.py

@@ -818,6 +818,20 @@ class TagSerializeTests(TestCase):
                           b'\n'
                           b'Tag 0.1'), x.as_raw_string())
 
+    def test_serialize_none_message(self):
+        x = make_object(Tag,
+                        tagger=b'Jelmer Vernooij <jelmer@samba.org>',
+                        name=b'0.1',
+                        message=None,
+                        object=(Blob, b'd80c186a03f423a81b39df39dc87fd269736ca86'),
+                        tag_time=423423423,
+                        tag_timezone=0)
+        self.assertEqual((b'object d80c186a03f423a81b39df39dc87fd269736ca86\n'
+                          b'type blob\n'
+                          b'tag 0.1\n'
+                          b'tagger Jelmer Vernooij <jelmer@samba.org> '
+                          b'423423423 +0000\n'), x.as_raw_string())
+
 
 default_tagger = (b'Linus Torvalds <torvalds@woody.linux-foundation.org> '
                   b'1183319674 -0700')
@@ -849,8 +863,8 @@ class TagParseTests(ShaFileCheckTests):
             lines.append(b'tag ' + name)
         if tagger is not None:
             lines.append(b'tagger ' + tagger)
-        lines.append(b'')
         if message is not None:
+            lines.append(b'')
             lines.append(message)
         return lines
 
@@ -877,6 +891,17 @@ class TagParseTests(ShaFileCheckTests):
         self.assertEqual(None, x.tagger)
         self.assertEqual(b'v2.6.22-rc7', x.name)
 
+    def test_parse_no_message(self):
+        x = Tag()
+        x.set_raw_string(self.make_tag_text(message=None))
+        self.assertEqual(None, x.message)
+        self.assertEqual(
+            b'Linus Torvalds <torvalds@woody.linux-foundation.org>', x.tagger)
+        self.assertEqual(datetime.datetime.utcfromtimestamp(x.tag_time),
+                          datetime.datetime(2007, 7, 1, 19, 54, 34))
+        self.assertEqual(-25200, x.tag_timezone)
+        self.assertEqual(b'v2.6.22-rc7', x.name)
+
     def test_check(self):
         self.assertCheckSucceeds(Tag, self.make_tag_text())
         self.assertCheckFails(Tag, self.make_tag_text(object_sha=None))

+ 0 - 4
dulwich/tests/test_patch.py

@@ -19,7 +19,6 @@
 """Tests for patch.py."""
 
 from io import BytesIO, StringIO
-import sys
 
 from dulwich.objects import (
     Blob,
@@ -76,9 +75,6 @@ class WriteCommitPatchTests(TestCase):
 class ReadGitAmPatch(TestCase):
 
     def test_extract_string(self):
-        if sys.version_info[:2] <= (2, 6):
-            raise SkipTest("email.parser.Parser.parsestr() inserts extra lines")
-
         text = b"""From ff643aae102d8870cac88e8f007e70f58f3a7363 Mon Sep 17 00:00:00 2001
 From: Jelmer Vernooij <jelmer@samba.org>
 Date: Thu, 15 Apr 2010 15:40:28 +0200

+ 7 - 0
dulwich/tests/test_porcelain.py

@@ -528,6 +528,13 @@ class PullTests(PorcelainTestCase):
 
 class StatusTests(PorcelainTestCase):
 
+    def test_empty(self):
+        results = porcelain.status(self.repo)
+        self.assertEqual(
+            {'add': [], 'delete': [], 'modify': []},
+            results.staged)
+        self.assertEqual([], results.unstaged)
+
     def test_status(self):
         """Integration test for `status` functionality."""
 

+ 44 - 22
dulwich/tests/test_web.py

@@ -64,6 +64,30 @@ from dulwich.tests.utils import (
     )
 
 
+class MinimalistWSGIInputStream(object):
+    """WSGI input stream with no 'seek()' and 'tell()' methods."""
+    def __init__(self, data):
+        self.data = data
+        self.pos = 0
+
+    def read(self, howmuch):
+        start = self.pos
+        end = self.pos + howmuch
+        if start >= len(self.data):
+            return ''
+        self.pos = end
+        return self.data[start:end]
+
+
+class MinimalistWSGIInputStream2(MinimalistWSGIInputStream):
+    """WSGI input stream with no *working* 'seek()' and 'tell()' methods."""
+    def seek(self, pos):
+        raise NotImplementedError
+
+    def tell(self):
+        raise NotImplementedError
+
+
 class TestHTTPGitRequest(HTTPGitRequest):
     """HTTPGitRequest with overridden methods to help test caching."""
 
@@ -193,10 +217,12 @@ class DumbHandlersTestCase(WebTestCase):
         backend = _test_backend([blob])
         mat = re.search('^(..)(.{38})$', blob.id.decode('ascii'))
 
-        def as_legacy_object_error():
+        def as_legacy_object_error(self):
             raise IOError
 
-        blob.as_legacy_object = as_legacy_object_error
+        self.addCleanup(
+            setattr, Blob, 'as_legacy_object', Blob.as_legacy_object)
+        Blob.as_legacy_object = as_legacy_object_error
         list(get_loose_object(self._req, backend, mat))
         self.assertEqual(HTTP_ERROR, self._status)
 
@@ -297,13 +323,13 @@ class SmartHandlersTestCase(WebTestCase):
         return self._handler
 
     def _handlers(self):
-        return {'git-upload-pack': self._make_handler}
+        return {b'git-upload-pack': self._make_handler}
 
     def test_handle_service_request_unknown(self):
         mat = re.search('.*', '/git-evil-handler')
         content = list(handle_service_request(self._req, 'backend', mat))
         self.assertEqual(HTTP_FORBIDDEN, self._status)
-        self.assertFalse('git-evil-handler' in "".join(content))
+        self.assertFalse(b'git-evil-handler' in b"".join(content))
         self.assertFalse(self._req.cached)
 
     def _run_handle_service_request(self, content_length=None):
@@ -311,11 +337,11 @@ class SmartHandlersTestCase(WebTestCase):
         if content_length is not None:
             self._environ['CONTENT_LENGTH'] = content_length
         mat = re.search('.*', '/git-upload-pack')
-        handler_output = ''.join(
+        handler_output = b''.join(
           handle_service_request(self._req, 'backend', mat))
         write_output = self._output.getvalue()
         # Ensure all output was written via the write callback.
-        self.assertEqual('', handler_output)
+        self.assertEqual(b'', handler_output)
         self.assertEqual(b'handled input: foo', write_output)
         self.assertContentTypeEquals('application/x-git-upload-pack-result')
         self.assertFalse(self._handler.advertise_refs)
@@ -334,7 +360,7 @@ class SmartHandlersTestCase(WebTestCase):
     def test_get_info_refs_unknown(self):
         self._environ['QUERY_STRING'] = 'service=git-evil-handler'
         content = list(get_info_refs(self._req, b'backend', None))
-        self.assertFalse('git-evil-handler' in "".join(content))
+        self.assertFalse(b'git-evil-handler' in b"".join(content))
         self.assertEqual(HTTP_FORBIDDEN, self._status)
         self.assertFalse(self._req.cached)
 
@@ -381,7 +407,7 @@ class HTTPGitRequestTestCase(WebTestCase):
     def test_not_found(self):
         self._req.cache_forever()  # cache headers should be discarded
         message = 'Something not found'
-        self.assertEqual(message, self._req.not_found(message))
+        self.assertEqual(message.encode('ascii'), self._req.not_found(message))
         self.assertEqual(HTTP_NOT_FOUND, self._status)
         self.assertEqual(set([('Content-Type', 'text/plain')]),
                           set(self._headers))
@@ -389,7 +415,7 @@ class HTTPGitRequestTestCase(WebTestCase):
     def test_forbidden(self):
         self._req.cache_forever()  # cache headers should be discarded
         message = 'Something not found'
-        self.assertEqual(message, self._req.forbidden(message))
+        self.assertEqual(message.encode('ascii'), self._req.forbidden(message))
         self.assertEqual(HTTP_FORBIDDEN, self._status)
         self.assertEqual(set([('Content-Type', 'text/plain')]),
                           set(self._headers))
@@ -497,19 +523,15 @@ class GunzipTestCase(HTTPGitApplicationTestCase):
         'wsgi.input' except for '.read()'.  (In particular, it shouldn't
         require '.seek()'. See https://github.com/jelmer/dulwich/issues/140.)
         """
-        class MinimalistWSGIInputStream(object):
-            def __init__(self, data):
-                self.data = data
-                self.pos = 0
-
-            def read(self, howmuch):
-                start = self.pos
-                end = self.pos + howmuch
-                if start >= len(self.data):
-                    return ''
-                self.pos = end
-                return self.data[start:end]
-
         zstream, zlength = self._get_zstream(self.example_text)
         self._test_call(self.example_text,
             MinimalistWSGIInputStream(zstream.read()), zlength)
+
+    def test_call_no_working_seek(self):
+        """
+        Similar to 'test_call_no_seek', but this time the methods are available
+        (but defunct).  See https://github.com/jonashaag/klaus/issues/154.
+        """
+        zstream, zlength = self._get_zstream(self.example_text)
+        self._test_call(self.example_text,
+            MinimalistWSGIInputStream2(zstream.read()), zlength)

+ 0 - 2
dulwich/tests/utils.py

@@ -360,5 +360,3 @@ def setup_warning_catcher():
         warnings.showwarning = original_showwarning
 
     return caught_warnings, restore_showwarning
-
-skipIfPY3 = skipIf(sys.version_info[0] == 3, "Feature not yet ported to python3.")

+ 14 - 13
dulwich/web.py

@@ -175,7 +175,7 @@ def get_info_refs(req, backend, mat):
     params = parse_qs(req.environ['QUERY_STRING'])
     service = params.get('service', [None])[0]
     if service and not req.dumb:
-        handler_cls = req.handlers.get(service, None)
+        handler_cls = req.handlers.get(service.encode('ascii'), None)
         if handler_cls is None:
             yield req.forbidden('Unsupported service')
             return
@@ -231,7 +231,7 @@ class _LengthLimitedFile(object):
 def handle_service_request(req, backend, mat):
     service = mat.group().lstrip('/')
     logger.info('Handling service request for %s', service)
-    handler_cls = req.handlers.get(service, None)
+    handler_cls = req.handlers.get(service.encode('ascii'), None)
     if handler_cls is None:
         yield req.forbidden('Unsupported service')
         return
@@ -275,21 +275,21 @@ class HTTPGitRequest(object):
         self._cache_headers = []
         logger.info('Not found: %s', message)
         self.respond(HTTP_NOT_FOUND, 'text/plain')
-        return message
+        return message.encode('ascii')
 
     def forbidden(self, message):
         """Begin a HTTP 403 response and return the text of a message."""
         self._cache_headers = []
         logger.info('Forbidden: %s', message)
         self.respond(HTTP_FORBIDDEN, 'text/plain')
-        return message
+        return message.encode('ascii')
 
     def error(self, message):
         """Begin a HTTP 500 response and return the text of a message."""
         self._cache_headers = []
         logger.error('Error: %s', message)
         self.respond(HTTP_ERROR, 'text/plain')
-        return message
+        return message.encode('ascii')
 
     def nocache(self):
         """Set the response to never be cached by the client."""
@@ -356,7 +356,7 @@ class HTTPGitApplication(object):
             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 [req.not_found('Sorry, that method is not supported')]
 
         return handler(req, self.backend, mat)
 
@@ -371,13 +371,14 @@ class GunzipFilter(object):
 
     def __call__(self, environ, start_response):
         if environ.get('HTTP_CONTENT_ENCODING', '') == 'gzip':
-            if hasattr(environ['wsgi.input'], 'seek'):
+            try:
+                environ['wsgi.input'].tell()
                 wsgi_input = environ['wsgi.input']
-            else:
+            except (AttributeError, IOError, NotImplementedError):
                 # The gzip implementation in the standard library of Python 2.x
-                # requires the '.seek()' and '.tell()' methods to be available
-                # on the input stream.  Read the data into a temporary file to
-                # work around this limitation.
+                # requires working '.seek()' and '.tell()' methods on the input
+                # stream.  Read the data into a temporary file to work around
+                # this limitation.
                 wsgi_input = tempfile.SpooledTemporaryFile(16 * 1024 * 1024)
                 shutil.copyfileobj(environ['wsgi.input'], wsgi_input)
                 wsgi_input.seek(0)
@@ -415,7 +416,7 @@ def make_wsgi_chain(*args, **kwargs):
     correctly wrapped with needed middleware.
     """
     app = HTTPGitApplication(*args, **kwargs)
-    wrapped_app = GunzipFilter(LimitedInputFilter(app))
+    wrapped_app = LimitedInputFilter(GunzipFilter(app))
     return wrapped_app
 
 
@@ -423,7 +424,7 @@ class ServerHandlerLogger(ServerHandler):
     """ServerHandler that uses dulwich's logger for logging exceptions."""
 
     def log_exception(self, exc_info):
-        if sys.version < (2, 7):
+        if sys.version_info < (2, 7):
             logger.exception('Exception happened during processing of request')
         else:
             logger.exception('Exception happened during processing of request',

+ 1 - 3
setup.py

@@ -8,7 +8,7 @@ except ImportError:
     from distutils.core import setup, Extension
 from distutils.core import Distribution
 
-dulwich_version_string = '0.12.0'
+dulwich_version_string = '0.13.0'
 
 include_dirs = []
 # Windows MSVC support
@@ -51,8 +51,6 @@ if sys.version_info[0] == 2:
     if not '__pypy__' in sys.modules and not sys.platform == 'win32':
         tests_require.extend([
             'gevent', 'geventhttpclient', 'mock', 'setuptools>=17.1'])
-    if sys.version_info < (2, 7):
-        tests_require.append('unittest2')
 else:
     # fastimport, gevent, geventhttpclient are not available for PY3
     # mock only used for test_swift, which requires gevent/geventhttpclient

+ 4 - 3
tox.ini

@@ -1,10 +1,8 @@
 [tox]
 downloadcache = {toxworkdir}/cache/
-envlist = py26, py27, pypy, py27-noext, pypy-noext, py34, py34-noext
+envlist = py27, pypy, py27-noext, pypy-noext, py34, py34-noext, py35, py35-noext
 
 [testenv]
-deps =
-    unittest2
 
 commands = make check
 recreate = True
@@ -18,3 +16,6 @@ commands = make check-noextensions
 
 [testenv:py34-noext]
 commands = make check-noextensions
+
+[testenv:py35-noext]
+commands = make check-noextensions