Browse Source

Imported Upstream version 0.9.8

Jelmer Vernooij 10 years ago
parent
commit
6ef7cf24ea
65 changed files with 2360 additions and 642 deletions
  1. 1 0
      AUTHORS
  2. 1 1
      Makefile
  3. 47 0
      NEWS
  4. 8 2
      PKG-INFO
  5. 9 0
      README.md
  6. 2 2
      bin/dul-receive-pack
  7. 2 2
      bin/dul-upload-pack
  8. 0 23
      bin/dul-web
  9. 79 2
      bin/dulwich
  10. 1 1
      docs/tutorial/conclusion.txt
  11. 8 2
      dulwich.egg-info/PKG-INFO
  12. 1 1
      dulwich.egg-info/SOURCES.txt
  13. 1 1
      dulwich/__init__.py
  14. 963 0
      dulwich/_compat.py
  15. 7 7
      dulwich/_diff_tree.c
  16. 41 31
      dulwich/client.py
  17. 12 12
      dulwich/config.py
  18. 13 13
      dulwich/contrib/swift.py
  19. 35 32
      dulwich/contrib/test_swift.py
  20. 6 1
      dulwich/diff_tree.py
  21. 4 10
      dulwich/hooks.py
  22. 33 26
      dulwich/index.py
  23. 9 27
      dulwich/object_store.py
  24. 35 14
      dulwich/objects.py
  25. 33 36
      dulwich/pack.py
  26. 176 13
      dulwich/porcelain.py
  27. 1 1
      dulwich/protocol.py
  28. 8 15
      dulwich/refs.py
  29. 4 16
      dulwich/repo.py
  30. 60 42
      dulwich/server.py
  31. 6 3
      dulwich/tests/__init__.py
  32. 3 3
      dulwich/tests/compat/server_utils.py
  33. 42 16
      dulwich/tests/compat/test_client.py
  34. 3 1
      dulwich/tests/compat/test_pack.py
  35. 2 0
      dulwich/tests/compat/test_repository.py
  36. 7 8
      dulwich/tests/compat/test_utils.py
  37. 3 3
      dulwich/tests/compat/test_web.py
  38. 5 3
      dulwich/tests/compat/utils.py
  39. 7 2
      dulwich/tests/test_blackbox.py
  40. 19 0
      dulwich/tests/test_client.py
  41. 10 0
      dulwich/tests/test_config.py
  42. 4 0
      dulwich/tests/test_diff_tree.py
  43. 1 1
      dulwich/tests/test_fastexport.py
  44. 6 1
      dulwich/tests/test_file.py
  45. 6 0
      dulwich/tests/test_grafts.py
  46. 4 1
      dulwich/tests/test_greenthreads.py
  47. 8 24
      dulwich/tests/test_hooks.py
  48. 18 20
      dulwich/tests/test_index.py
  49. 5 0
      dulwich/tests/test_lru_cache.py
  50. 4 0
      dulwich/tests/test_missing_obj_finder.py
  51. 64 35
      dulwich/tests/test_object_store.py
  52. 148 1
      dulwich/tests/test_objects.py
  53. 3 0
      dulwich/tests/test_objectspec.py
  54. 41 29
      dulwich/tests/test_pack.py
  55. 7 1
      dulwich/tests/test_patch.py
  56. 161 20
      dulwich/tests/test_porcelain.py
  57. 6 0
      dulwich/tests/test_protocol.py
  58. 5 1
      dulwich/tests/test_refs.py
  59. 14 39
      dulwich/tests/test_repository.py
  60. 35 12
      dulwich/tests/test_server.py
  61. 2 0
      dulwich/tests/test_walk.py
  62. 5 0
      dulwich/tests/test_web.py
  63. 10 4
      dulwich/tests/utils.py
  64. 74 69
      dulwich/web.py
  65. 22 12
      setup.py

+ 1 - 0
AUTHORS

@@ -4,6 +4,7 @@ John Carr <john.carr@unrouted.co.uk>
 Dave Borowitz <dborowitz@google.com>
 Chris Eberle <eberle1080@gmail.com>
 "milki" <milki@rescomp.berkeley.edu>
+Gary van der Merwe <garyvdm@gmail.com>
 
 Hervé Cauwelier <herve@itaapy.com> wrote the original tutorial.
 

+ 1 - 1
Makefile

@@ -8,7 +8,7 @@ TESTRUNNER ?= unittest
 else
 TESTRUNNER ?= unittest2.__main__
 endif
-RUNTEST = PYTHONPATH=.:$(PYTHONPATH) $(PYTHON) -m $(TESTRUNNER)
+RUNTEST = PYTHONHASHSEED=random PYTHONPATH=.:$(PYTHONPATH) $(PYTHON) -m $(TESTRUNNER) $(TEST_OPTIONS)
 
 DESTDIR=/
 

+ 47 - 0
NEWS

@@ -1,3 +1,50 @@
+0.9.8	2014-11-30
+
+ BUG FIXES
+
+  * Various fixes to improve test suite running on Windows.
+    (Gary van der Merwe)
+
+  * Limit delta copy length to 64K in v2 pack files. (Robert Brown)
+
+  * Strip newline from final ACKed SHA while fetching packs.
+    (Michael Edgar)
+
+  * Remove assignment to PyList_SIZE() that was causing segfaults on
+    pypy. (Jelmer Vernooij, #196)
+
+ IMPROVEMENTS
+
+  * Add porcelain 'receive-pack' and 'upload-pack'. (Jelmer Vernooij)
+
+  * Handle SIGINT signals in bin/dulwich. (Jelmer Vernooij)
+
+  * Add 'status' support to bin/dulwich. (Jelmer Vernooij)
+
+  * Add 'branch_create', 'branch_list', 'branch_delete' porcelain.
+    (Jelmer Vernooij)
+
+  * Add 'fetch' porcelain. (Jelmer Vernooij)
+
+  * Add 'tag_delete' porcelain. (Jelmer Vernooij)
+
+  * Add support for serializing/deserializing 'gpgsig' attributes in Commit.
+    (Jelmer Vernooij)
+
+ CHANGES
+
+  * dul-web is now available as 'dulwich web-daemon'.
+    (Jelmer Vernooij)
+
+  * dulwich.porcelain.tag has been renamed to tag_create.
+    dulwich.porcelain.list_tags has been renamed to tag_list.
+    (Jelmer Vernooij)
+
+ API CHANGES
+
+  * Restore support for Python 2.6. (Jelmer Vernooij, Gary van der Merwe)
+
+
 0.9.7	2014-06-08
 
  BUG FIXES

+ 8 - 2
PKG-INFO

@@ -1,6 +1,6 @@
-Metadata-Version: 1.0
+Metadata-Version: 1.1
 Name: dulwich
-Version: 0.9.7
+Version: 0.9.8
 Summary: Python Git Library
 Home-page: https://samba.org/~jelmer/dulwich
 Author: Jelmer Vernooij
@@ -18,3 +18,9 @@ Description:
               
 Keywords: git
 Platform: UNKNOWN
+Classifier: Development Status :: 4 - Beta
+Classifier: License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)
+Classifier: Programming Language :: Python :: 2.6
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Operating System :: POSIX
+Classifier: Topic :: Software Development :: Version Control

+ 9 - 0
README.md

@@ -1,5 +1,7 @@
 This is the Dulwich project.
 
+[![Build Status](https://travis-ci.org/jelmer/dulwich.png?branch=master)](https://travis-ci.org/jelmer/dulwich)
+
 It aims to provide an interface to git repos (both local and remote) that
 doesn't call out to git directly but instead uses pure Python.
 
@@ -25,3 +27,10 @@ Help
 
 There is a #dulwich IRC channel on Freenode, and a dulwich mailing list at
 https://launchpad.net/~dulwich-users.
+
+Python3
+-------
+
+The process of porting to Python3 is ongoing. Please not that although the
+test suite pass in python3, this is due to the tests of features that are not
+yet ported being skipped, and *not* an indication that the port is complete.

+ 2 - 2
bin/dul-receive-pack

@@ -17,7 +17,7 @@
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 # MA  02110-1301, USA.
 
-from dulwich.server import serve_command, ReceivePackHandler
+from dulwich.porcelain import receive_pack
 import os
 import sys
 
@@ -25,4 +25,4 @@ if len(sys.argv) < 2:
     sys.stderr.write("usage: %s <git-dir>\n" % os.path.basename(sys.argv[0]))
     sys.exit(1)
 
-sys.exit(serve_command(ReceivePackHandler))
+sys.exit(receive_pack(sys.argv[1]))

+ 2 - 2
bin/dul-upload-pack

@@ -17,7 +17,7 @@
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 # MA  02110-1301, USA.
 
-from dulwich.server import serve_command, UploadPackHandler
+from dulwich.porcelain import upload_pack
 import os
 import sys
 
@@ -25,4 +25,4 @@ if len(sys.argv) < 2:
     sys.stderr.write("usage: %s <git-dir>\n" % os.path.basename(sys.argv[0]))
     sys.exit(1)
 
-sys.exit(serve_command(UploadPackHandler))
+sys.exit(upload_pack(sys.argv[1]))

+ 0 - 23
bin/dul-web

@@ -1,23 +0,0 @@
-#!/usr/bin/python
-# dul-web - HTTP-based git server
-# Copyright (C) 2010 Google, Inc. <dborowitz@google.com>
-#
-# 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
-# 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.
-
-from dulwich.web import main
-
-if __name__ == '__main__':
-    main()

+ 79 - 2
bin/dulwich

@@ -30,6 +30,12 @@ import os
 import sys
 from getopt import getopt
 import optparse
+import signal
+
+def signal_int(signal, frame):
+    sys.exit(1)
+
+signal.signal(signal.SIGINT, signal_int)
 
 from dulwich import porcelain
 from dulwich.client import get_transport_and_path
@@ -259,8 +265,29 @@ def cmd_daemon(args):
     options, args = parser.parse_args(args)
 
     log_utils.default_logging_config()
-    if len(args) > 1:
-        gitdir = args[1]
+    if len(args) >= 1:
+        gitdir = args[0]
+    else:
+        gitdir = '.'
+    from dulwich import porcelain
+    porcelain.daemon(gitdir, address=options.listen_address,
+                     port=options.port)
+
+
+def cmd_web_daemon(args):
+    from dulwich import log_utils
+    parser = optparse.OptionParser()
+    parser.add_option("-l", "--listen_address", dest="listen_address",
+                      default="",
+                      help="Binding IP address.")
+    parser.add_option("-p", "--port", dest="port", type=int,
+                      default=8000,
+                      help="Binding TCP port.")
+    options, args = parser.parse_args(args)
+
+    log_utils.default_logging_config()
+    if len(args) >= 1:
+        gitdir = args[0]
     else:
         gitdir = '.'
     from dulwich import porcelain
@@ -268,6 +295,52 @@ def cmd_daemon(args):
                      port=options.port)
 
 
+def cmd_receive_pack(args):
+    parser = optparse.OptionParser()
+    options, args = parser.parse_args(args)
+    if len(args) >= 1:
+        gitdir = args[0]
+    else:
+        gitdir = '.'
+    porcelain.receive_pack(gitdir)
+
+
+def cmd_upload_pack(args):
+    parser = optparse.OptionParser()
+    options, args = parser.parse_args(args)
+    if len(args) >= 1:
+        gitdir = args[0]
+    else:
+        gitdir = '.'
+    porcelain.upload_pack(gitdir)
+
+
+def cmd_status(args):
+    parser = optparse.OptionParser()
+    options, args = parser.parse_args(args)
+    if len(args) >= 1:
+        gitdir = args[0]
+    else:
+        gitdir = '.'
+    status = porcelain.status(gitdir)
+    if status.staged:
+        sys.stdout.write("Changes to be committed:\n\n")
+        for kind, names in status.staged.iteritems():
+            for name in names:
+                sys.stdout.write("\t%s: %s\n" % (kind, name))
+        sys.stdout.write("\n")
+    if status.unstaged:
+        sys.stdout.write("Changes not staged for commit:\n\n")
+        for name in status.unstaged:
+            sys.stdout.write("\t%s\n" % name)
+        sys.stdout.write("\n")
+    if status.untracked:
+        sys.stdout.write("Untracked files:\n\n")
+        for name in status.untracked:
+            sys.stdout.write("\t%s\n" % name)
+        sys.stdout.write("\n")
+
+
 commands = {
     "add": cmd_add,
     "archive": cmd_archive,
@@ -283,13 +356,17 @@ commands = {
     "fetch": cmd_fetch,
     "init": cmd_init,
     "log": cmd_log,
+    "receive-pack": cmd_receive_pack,
     "reset": cmd_reset,
     "rev-list": cmd_rev_list,
     "rm": cmd_rm,
     "show": cmd_show,
+    "status": cmd_status,
     "symbolic-ref": cmd_symbolic_ref,
     "tag": cmd_tag,
     "update-server-info": cmd_update_server_info,
+    "upload-pack": cmd_upload_pack,
+    "web-daemon": cmd_web_daemon,
     }
 
 if len(sys.argv) < 2:

+ 1 - 1
docs/tutorial/conclusion.txt

@@ -4,7 +4,7 @@ Conclusion
 ==========
 
 This tutorial currently only covers a small (but important) part of Dulwich.
-It still needs to be extended to cover packs, tags, refs, reflogs and network
+It still needs to be extended to cover packs, refs, reflogs and network
 communication.
 
 Dulwich is abstracting much of the Git plumbing, so there would be more to

+ 8 - 2
dulwich.egg-info/PKG-INFO

@@ -1,6 +1,6 @@
-Metadata-Version: 1.0
+Metadata-Version: 1.1
 Name: dulwich
-Version: 0.9.7
+Version: 0.9.8
 Summary: Python Git Library
 Home-page: https://samba.org/~jelmer/dulwich
 Author: Jelmer Vernooij
@@ -18,3 +18,9 @@ Description:
               
 Keywords: git
 Platform: UNKNOWN
+Classifier: Development Status :: 4 - Beta
+Classifier: License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)
+Classifier: Programming Language :: Python :: 2.6
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Operating System :: POSIX
+Classifier: Topic :: Software Development :: Version Control

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

@@ -8,7 +8,6 @@ README.md
 setup.py
 bin/dul-receive-pack
 bin/dul-upload-pack
-bin/dul-web
 bin/dulwich
 docs/Makefile
 docs/conf.py
@@ -26,6 +25,7 @@ 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, 9, 7)
+__version__ = (0, 9, 8)

+ 963 - 0
dulwich/_compat.py

@@ -0,0 +1,963 @@
+# _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)

+ 7 - 7
dulwich/_diff_tree.c

@@ -193,12 +193,9 @@ static PyObject *py_merge_entries(PyObject *self, PyObject *args)
 	if (!entries2)
 		goto error;
 
-	result = PyList_New(n1 + n2);
+	result = PyList_New(0);
 	if (!result)
 		goto error;
-	/* PyList_New sets the len of the list, not its allocated size, so we
-	 * need to trim it to the size we actually use. */
-	Py_SIZE(result) = 0;
 
 	while (i1 < n1 && i2 < n2) {
 		cmp = entry_path_cmp(entries1[i1], entries2[i2]);
@@ -217,20 +214,23 @@ static PyObject *py_merge_entries(PyObject *self, PyObject *args)
 		pair = PyTuple_Pack(2, e1, e2);
 		if (!pair)
 			goto error;
-		PyList_SET_ITEM(result, Py_SIZE(result)++, pair);
+		PyList_Append(result, pair);
+		Py_DECREF(pair);
 	}
 
 	while (i1 < n1) {
 		pair = PyTuple_Pack(2, entries1[i1++], null_entry);
 		if (!pair)
 			goto error;
-		PyList_SET_ITEM(result, Py_SIZE(result)++, pair);
+		PyList_Append(result, pair);
+		Py_DECREF(pair);
 	}
 	while (i2 < n2) {
 		pair = PyTuple_Pack(2, null_entry, entries2[i2++]);
 		if (!pair)
 			goto error;
-		PyList_SET_ITEM(result, Py_SIZE(result)++, pair);
+		PyList_Append(result, pair);
+		Py_DECREF(pair);
 	}
 	goto done;
 

+ 41 - 31
dulwich/client.py

@@ -43,8 +43,14 @@ import dulwich
 import select
 import socket
 import subprocess
-import urllib2
-import urlparse
+import sys
+
+try:
+    import urllib2
+    import urlparse
+except ImportError:
+    import urllib.request as urllib2
+    import urllib.parse as urlparse
 
 from dulwich.errors import (
     GitProtocolError,
@@ -177,13 +183,15 @@ class GitClient(object):
             self._fetch_capabilities.remove('thin-pack')
 
     def send_pack(self, path, determine_wants, generate_pack_contents,
-                  progress=None):
+                  progress=None, write_pack=write_pack_objects):
         """Upload a pack to a remote repository.
 
         :param path: Repository path
         :param generate_pack_contents: Function that can return a sequence of
             the shas of the objects to upload.
         :param progress: Optional progress function
+        :param write_pack: Function called with (file, iterable of objects) to
+            write the objects returned by generate_pack_contents to the server.
 
         :raises SendPackError: if server rejects the pack data
         :raises UpdateRefsError: if the server supports report-status
@@ -389,7 +397,7 @@ class GitClient(object):
         while pkt:
             parts = pkt.rstrip('\n').split(' ')
             if parts[0] == 'ACK':
-                graph_walker.ack(pkt.split(' ')[1])
+                graph_walker.ack(parts[1])
             if len(parts) < 3 or parts[2] not in (
                     'ready', 'continue', 'common'):
                 break
@@ -425,13 +433,15 @@ class TraditionalGitClient(GitClient):
         raise NotImplementedError()
 
     def send_pack(self, path, determine_wants, generate_pack_contents,
-                  progress=None):
+                  progress=None, write_pack=write_pack_objects):
         """Upload a pack to a remote repository.
 
         :param path: Repository path
         :param generate_pack_contents: Function that can return a sequence of
             the shas of the objects to upload.
         :param progress: Optional callback called with progress updates
+        :param write_pack: Function called with (file, iterable of objects) to
+            write the objects returned by generate_pack_contents to the server.
 
         :raises SendPackError: if server rejects the pack data
         :raises UpdateRefsError: if the server supports report-status
@@ -454,21 +464,15 @@ class TraditionalGitClient(GitClient):
 
             if not 'delete-refs' in server_capabilities:
                 # Server does not support deletions. Fail later.
-                def remove_del(pair):
-                    if pair[1] == ZERO_SHA:
+                new_refs = dict(orig_new_refs)
+                for ref, sha in orig_new_refs.iteritems():
+                    if sha == ZERO_SHA:
                         if 'report-status' in negotiated_capabilities:
                             report_status_parser._ref_statuses.append(
                                 'ng %s remote does not support deleting refs'
-                                % pair[1])
+                                % sha)
                             report_status_parser._ref_status_ok = False
-                        return False
-                    else:
-                        return True
-
-                new_refs = dict(
-                    filter(
-                        remove_del,
-                        [(ref, sha) for ref, sha in new_refs.iteritems()]))
+                        del new_refs[ref]
 
             if new_refs is None:
                 proto.write_pkt_line(None)
@@ -477,8 +481,8 @@ class TraditionalGitClient(GitClient):
             if len(new_refs) == 0 and len(orig_new_refs):
                 # NOOP - Original new refs filtered out by policy
                 proto.write_pkt_line(None)
-                if self._report_status_parser is not None:
-                    self._report_status_parser.check()
+                if report_status_parser is not None:
+                    report_status_parser.check()
                 return old_refs
 
             (have, want) = self._handle_receive_pack_head(
@@ -486,16 +490,13 @@ class TraditionalGitClient(GitClient):
             if not want and old_refs == new_refs:
                 return new_refs
             objects = generate_pack_contents(have, want)
-            if len(objects) > 0:
-                entries, sha = write_pack_objects(proto.write_file(), objects)
-            elif len(set(new_refs.values()) - set([ZERO_SHA])) > 0:
-                # Check for valid create/update refs
-                filtered_new_refs = \
-                    dict([(ref, sha) for ref, sha in new_refs.iteritems()
-                         if sha != ZERO_SHA])
-                if len(set(filtered_new_refs.iteritems()) -
-                        set(old_refs.iteritems())) > 0:
-                    entries, sha = write_pack_objects(proto.write_file(), objects)
+
+            dowrite = len(objects) > 0
+            dowrite = dowrite or any(old_refs.get(ref) != sha
+                                     for (ref, sha) in new_refs.iteritems()
+                                     if sha != ZERO_SHA)
+            if dowrite:
+                write_pack(proto.write_file(), objects)
 
             self._handle_receive_pack_tail(
                 proto, negotiated_capabilities, progress)
@@ -664,13 +665,15 @@ class LocalGitClient(GitClient):
         # Ignore the thin_packs argument
 
     def send_pack(self, path, determine_wants, generate_pack_contents,
-                  progress=None):
+                  progress=None, write_pack=write_pack_objects):
         """Upload a pack to a remote repository.
 
         :param path: Repository path
         :param generate_pack_contents: Function that can return a sequence of
             the shas of the objects to upload.
         :param progress: Optional progress function
+        :param write_pack: Function called with (file, iterable of objects) to
+            write the objects returned by generate_pack_contents to the server.
 
         :raises SendPackError: if server rejects the pack data
         :raises UpdateRefsError: if the server supports report-status
@@ -979,13 +982,15 @@ class HttpGitClient(GitClient):
         return resp
 
     def send_pack(self, path, determine_wants, generate_pack_contents,
-                  progress=None):
+                  progress=None, write_pack=write_pack_objects):
         """Upload a pack to a remote repository.
 
         :param path: Repository path
         :param generate_pack_contents: Function that can return a sequence of
             the shas of the objects to upload.
         :param progress: Optional progress function
+        :param write_pack: Function called with (file, iterable of objects) to
+            write the objects returned by generate_pack_contents to the server.
 
         :raises SendPackError: if server rejects the pack data
         :raises UpdateRefsError: if the server supports report-status
@@ -1012,7 +1017,7 @@ class HttpGitClient(GitClient):
             return new_refs
         objects = generate_pack_contents(have, want)
         if len(objects) > 0:
-            entries, sha = write_pack_objects(req_proto.write_file(), objects)
+            write_pack(req_proto.write_file(), objects)
         resp = self._smart_request("git-receive-pack", url,
                                    data=req_data.getvalue())
         try:
@@ -1106,6 +1111,11 @@ def get_transport_and_path(location, **kwargs):
     except ValueError:
         pass
 
+    if (sys.platform == 'win32' and
+            location[0].isalpha() and location[1:2] == ':\\'):
+        # Windows local path
+        return default_local_git_client_cls(**kwargs), location
+
     if ':' in location and not '@' in location:
         # SSH with no user@, zero or one leading slash.
         (hostname, path) = location.split(':')

+ 12 - 12
dulwich/config.py

@@ -28,10 +28,16 @@ import errno
 import os
 import re
 
-from collections import (
-    OrderedDict,
-    MutableMapping,
-    )
+try:
+    from collections import (
+        OrderedDict,
+        MutableMapping,
+        )
+except ImportError:
+    from dulwich._compat import (
+        OrderedDict,
+        MutableMapping
+        )
 
 
 from dulwich.file import GitFile
@@ -305,23 +311,17 @@ class ConfigFile(ConfigDict):
     @classmethod
     def from_path(cls, path):
         """Read configuration from a file on disk."""
-        f = GitFile(path, 'rb')
-        try:
+        with GitFile(path, 'rb') as f:
             ret = cls.from_file(f)
             ret.path = path
             return ret
-        finally:
-            f.close()
 
     def write_to_path(self, path=None):
         """Write configuration to a file on disk."""
         if path is None:
             path = self.path
-        f = GitFile(path, 'wb')
-        try:
+        with GitFile(path, 'wb') as f:
             self.write_to_file(f)
-        finally:
-            f.close()
 
     def write_to_file(self, f):
         """Write configuration to a file-like object."""

+ 13 - 13
dulwich/contrib/swift.py

@@ -32,7 +32,7 @@ import tempfile
 import posixpath
 
 from urlparse import urlparse
-from cStringIO import StringIO
+from io import BytesIO
 from ConfigParser import ConfigParser
 from geventhttpclient import HTTPClient
 
@@ -162,7 +162,7 @@ def load_conf(path=None, file=None):
     :param path: The path to the configuration file
     :param file: If provided read instead the file like object
     """
-    conf = ConfigParser(allow_no_value=True)
+    conf = ConfigParser()
     if file:
         conf.readfp(file)
         return conf
@@ -211,7 +211,7 @@ def pack_info_create(pack_data, pack_index):
             info[obj.id] = None
         # Tag
         elif obj.type_num == Tag.type_num:
-            info[obj.id] = (obj.type_num, obj._object_sha)
+            info[obj.id] = (obj.type_num, obj.object[1])
     return zlib.compress(json_dumps(info))
 
 
@@ -453,7 +453,7 @@ class SwiftConnector(object):
 
         if range:
             return content
-        return StringIO(content)
+        return BytesIO(content)
 
     def del_object(self, name):
         """Delete an object
@@ -718,7 +718,7 @@ class SwiftObjectStore(PackBasedObjectStore):
         :return: Fileobject to write to and a commit function to
             call when the pack is finished.
         """
-        f = StringIO()
+        f = BytesIO()
 
         def commit():
             f.seek(0)
@@ -729,7 +729,7 @@ class SwiftObjectStore(PackBasedObjectStore):
                                           "pack-%s" %
                                           iter_sha1(entry[0] for
                                                     entry in entries))
-                index = StringIO()
+                index = BytesIO()
                 write_pack_index_v2(index, entries, pack.get_stored_checksum())
                 self.scon.put_object(basename + ".pack", f)
                 f.close()
@@ -808,7 +808,7 @@ class SwiftObjectStore(PackBasedObjectStore):
 
         # Write the index.
         filename = pack_base_name + '.idx'
-        index_file = StringIO()
+        index_file = BytesIO()
         write_pack_index_v2(index_file, entries, pack_sha)
         self.scon.put_object(filename, index_file)
 
@@ -820,7 +820,7 @@ class SwiftObjectStore(PackBasedObjectStore):
         serialized_pack_info = pack_info_create(pack_data, pack_index)
         f.close()
         index_file.close()
-        pack_info_file = StringIO(serialized_pack_info)
+        pack_info_file = BytesIO(serialized_pack_info)
         filename = pack_base_name + '.info'
         self.scon.put_object(filename, pack_info_file)
         pack_info_file.close()
@@ -842,7 +842,7 @@ class SwiftInfoRefsContainer(InfoRefsContainer):
         self.store = store
         f = self.scon.get_object(self.filename)
         if not f:
-            f = StringIO('')
+            f = BytesIO('')
         super(SwiftInfoRefsContainer, self).__init__(f)
 
     def _load_check_ref(self, name, old_ref):
@@ -857,7 +857,7 @@ class SwiftInfoRefsContainer(InfoRefsContainer):
         return refs
 
     def _write_refs(self, refs):
-        f = StringIO()
+        f = BytesIO()
         f.writelines(write_info_refs(refs, self.store))
         self.scon.put_object(self.filename, f)
 
@@ -928,7 +928,7 @@ class SwiftRepo(BaseRepo):
         :param filename: the path to the object to put on Swift
         :param contents: the content as bytestring
         """
-        f = StringIO()
+        f = BytesIO()
         f.write(contents)
         self.scon.put_object(filename, f)
         f.close()
@@ -944,7 +944,7 @@ class SwiftRepo(BaseRepo):
         scon.create_root()
         for obj in [posixpath.join(OBJECTDIR, PACKDIR),
                     posixpath.join(INFODIR, 'refs')]:
-            scon.put_object(obj, StringIO(''))
+            scon.put_object(obj, BytesIO(''))
         ret = cls(scon.root, conf)
         ret._init_files(True)
         return ret
@@ -985,7 +985,7 @@ def cmd_daemon(args):
         sys.exit(1)
     import gevent.monkey
     gevent.monkey.patch_socket()
-    from dulwich.swift import load_conf
+    from dulwich.contrib.swift import load_conf
     from dulwich import log_utils
     logger = log_utils.getLogger(__name__)
     conf = load_conf(options.swift_config)

+ 35 - 32
dulwich/contrib/test_swift.py

@@ -24,8 +24,11 @@
 import posixpath
 
 from time import time
-from cStringIO import StringIO
-from unittest import skipIf
+from io import BytesIO
+try:
+    from unittest import skipIf
+except ImportError:
+    from unittest2 import skipIf
 
 from dulwich.tests import (
     TestCase,
@@ -221,7 +224,7 @@ class FakeSwiftConnector(object):
         name = posixpath.join(self.root, name)
         if not range:
             try:
-                return StringIO(self.store[name])
+                return BytesIO(self.store[name])
             except KeyError:
                 return None
         else:
@@ -257,14 +260,14 @@ class TestSwiftObjectStore(TestCase):
 
     def setUp(self):
         super(TestSwiftObjectStore, self).setUp()
-        self.conf = swift.load_conf(file=StringIO(config_file %
+        self.conf = swift.load_conf(file=BytesIO(config_file %
                                                   def_config_file))
         self.fsc = FakeSwiftConnector('fakerepo', conf=self.conf)
 
     def _put_pack(self, sos, commit_amount=1, marker='Default'):
         odata = create_commits(length=commit_amount, marker=marker)
         data = [(d.type_num, d.as_raw_string()) for d in odata]
-        f = StringIO()
+        f = BytesIO()
         build_pack(f, data, store=sos)
         sos.add_thin_pack(f.read, None)
         return odata
@@ -365,7 +368,7 @@ class TestSwiftObjectStore(TestCase):
                 (tree.type_num, tree.as_raw_string()),
                 (cmt.type_num, cmt.as_raw_string()),
                 (tag.type_num, tag.as_raw_string())]
-        f = StringIO()
+        f = BytesIO()
         build_pack(f, data, store=sos)
         sos.add_thin_pack(f.read, None)
         self.assertEqual(len(self.fsc.store), 6)
@@ -376,7 +379,7 @@ class TestSwiftRepo(TestCase):
 
     def setUp(self):
         super(TestSwiftRepo, self).setUp()
-        self.conf = swift.load_conf(file=StringIO(config_file %
+        self.conf = swift.load_conf(file=BytesIO(config_file %
                                                   def_config_file))
 
     def test_init(self):
@@ -425,15 +428,15 @@ class TestSwiftRepo(TestCase):
 @skipIf(missing_libs, skipmsg)
 class TestPackInfoLoadDump(TestCase):
     def setUp(self):
-        conf = swift.load_conf(file=StringIO(config_file %
+        conf = swift.load_conf(file=BytesIO(config_file %
                                              def_config_file))
         sos = swift.SwiftObjectStore(
             FakeSwiftConnector('fakerepo', conf=conf))
         commit_amount = 10
         self.commits = create_commits(length=commit_amount, marker="m")
         data = [(d.type_num, d.as_raw_string()) for d in self.commits]
-        f = StringIO()
-        fi = StringIO()
+        f = BytesIO()
+        fi = BytesIO()
         expected = build_pack(f, data, store=sos)
         entries = [(sha, ofs, checksum) for
                    ofs, _, _, sha, checksum in expected]
@@ -452,14 +455,14 @@ class TestPackInfoLoadDump(TestCase):
 #            dump_time.append(time() - start)
 #        for i in xrange(0, 100):
 #            start = time()
-#            pack_infos = swift.load_pack_info('', file=StringIO(dumps))
+#            pack_infos = swift.load_pack_info('', file=BytesIO(dumps))
 #            load_time.append(time() - start)
 #        print sum(dump_time) / float(len(dump_time))
 #        print sum(load_time) / float(len(load_time))
 
     def test_pack_info(self):
         dumps = swift.pack_info_create(self.pack_data, self.pack_index)
-        pack_infos = swift.load_pack_info('', file=StringIO(dumps))
+        pack_infos = swift.load_pack_info('', file=BytesIO(dumps))
         for obj in self.commits:
             self.assertIn(obj.id, pack_infos)
 
@@ -473,7 +476,7 @@ class TestSwiftInfoRefsContainer(TestCase):
             "22effb216e3a82f97da599b8885a6cadb488b4c5\trefs/heads/master\n" + \
             "cca703b0e1399008b53a1a236d6b4584737649e4\trefs/heads/dev"
         self.store = {'fakerepo/info/refs': content}
-        self.conf = swift.load_conf(file=StringIO(config_file %
+        self.conf = swift.load_conf(file=BytesIO(config_file %
                                                   def_config_file))
         self.fsc = FakeSwiftConnector('fakerepo', conf=self.conf)
         self.object_store = {}
@@ -507,7 +510,7 @@ class TestSwiftConnector(TestCase):
 
     def setUp(self):
         super(TestSwiftConnector, self).setUp()
-        self.conf = swift.load_conf(file=StringIO(config_file %
+        self.conf = swift.load_conf(file=BytesIO(config_file %
                                                   def_config_file))
         with patch('geventhttpclient.HTTPClient.request',
                    fake_auth_request_v1):
@@ -553,18 +556,18 @@ class TestSwiftConnector(TestCase):
 
     def test_create_root(self):
         with patch('dulwich.contrib.swift.SwiftConnector.test_root_exists',
-                lambda *args: None), \
-             patch('geventhttpclient.HTTPClient.request',
+                lambda *args: None):
+            with patch('geventhttpclient.HTTPClient.request',
                 lambda *args: Response()):
-            self.assertEqual(self.conn.create_root(), None)
+                self.assertEqual(self.conn.create_root(), None)
 
     def test_create_root_fails(self):
         with patch('dulwich.contrib.swift.SwiftConnector.test_root_exists',
-                   lambda *args: None), \
-             patch('geventhttpclient.HTTPClient.request',
-                   lambda *args: Response(status=404)):
-            self.assertRaises(swift.SwiftException,
-                              lambda: self.conn.create_root())
+                   lambda *args: None):
+            with patch('geventhttpclient.HTTPClient.request',
+                       lambda *args: Response(status=404)):
+                self.assertRaises(swift.SwiftException,
+                                  lambda: self.conn.create_root())
 
     def test_get_container_objects(self):
         with patch('geventhttpclient.HTTPClient.request',
@@ -591,7 +594,7 @@ class TestSwiftConnector(TestCase):
     def test_put_object(self):
         with patch('geventhttpclient.HTTPClient.request',
                    lambda *args, **kwargs: Response()):
-            self.assertEqual(self.conn.put_object('a', StringIO('content')),
+            self.assertEqual(self.conn.put_object('a', BytesIO('content')),
                              None)
 
     def test_put_object_fails(self):
@@ -599,7 +602,7 @@ class TestSwiftConnector(TestCase):
                    lambda *args, **kwargs: Response(status=400)):
             self.assertRaises(swift.SwiftException,
                               lambda: self.conn.put_object(
-                                  'a', StringIO('content')))
+                                  'a', BytesIO('content')))
 
     def test_get_object(self):
         with patch('geventhttpclient.HTTPClient.request',
@@ -621,13 +624,13 @@ class TestSwiftConnector(TestCase):
 
     def test_del_root(self):
         with patch('dulwich.contrib.swift.SwiftConnector.del_object',
-                   lambda *args: None), \
-             patch('dulwich.contrib.swift.SwiftConnector.'
-                   'get_container_objects',
-                   lambda *args: ({'name': 'a'}, {'name': 'b'})), \
-             patch('geventhttpclient.HTTPClient.request',
-                    lambda *args: Response()):
-            self.assertEqual(self.conn.del_root(), None)
+                   lambda *args: None):
+            with patch('dulwich.contrib.swift.SwiftConnector.'
+                       'get_container_objects',
+                       lambda *args: ({'name': 'a'}, {'name': 'b'})):
+                with patch('geventhttpclient.HTTPClient.request',
+                           lambda *args: Response()):
+                    self.assertEqual(self.conn.del_root(), None)
 
 
 @skipIf(missing_libs, skipmsg)
@@ -635,7 +638,7 @@ class SwiftObjectStoreTests(ObjectStoreTests, TestCase):
 
     def setUp(self):
         TestCase.setUp(self)
-        conf = swift.load_conf(file=StringIO(config_file %
+        conf = swift.load_conf(file=BytesIO(config_file %
                                def_config_file))
         fsc = FakeSwiftConnector('fakerepo', conf=conf)
         self.store = swift.SwiftObjectStore(fsc)

+ 6 - 1
dulwich/diff_tree.py

@@ -24,9 +24,14 @@ from collections import (
     )
 
 from io import BytesIO
-from itertools import chain, izip
+from itertools import chain
 import stat
 
+try:
+    from itertools import izip
+except ImportError:
+    # Python3
+    izip = zip
 from dulwich.objects import (
     S_ISGITLINK,
     TreeEntry,

+ 4 - 10
dulwich/hooks.py

@@ -76,8 +76,8 @@ class ShellHook(Hook):
 
         if len(args) != self.numparam:
             raise HookError("Hook %s executed with wrong number of args. \
-                            Expected %d. Saw %d. %s"
-                            % (self.name, self.numparam, len(args)))
+                            Expected %d. Saw %d. args: %s"
+                            % (self.name, self.numparam, len(args), args))
 
         if (self.pre_exec_callback is not None):
             args = self.pre_exec_callback(*args)
@@ -127,21 +127,15 @@ class CommitMsgShellHook(ShellHook):
         def prepare_msg(*args):
             (fd, path) = tempfile.mkstemp()
 
-            f = os.fdopen(fd, 'wb')
-            try:
+            with os.fdopen(fd, 'wb') as f:
                 f.write(args[0])
-            finally:
-                f.close()
 
             return (path,)
 
         def clean_msg(success, *args):
             if success:
-                f = open(args[0], 'rb')
-                try:
+                with open(args[0], 'rb') as f:
                     new_msg = f.read()
-                finally:
-                    f.close()
                 os.unlink(args[0])
                 return new_msg
             os.unlink(args[0])

+ 33 - 26
dulwich/index.py

@@ -402,6 +402,35 @@ def index_entry_from_stat(stat_val, hex_sha, flags, mode=None):
             stat_val.st_gid, stat_val.st_size, hex_sha, flags)
 
 
+def build_file_from_blob(blob, mode, target_path, honor_filemode=True):
+    """Build a file or symlink on disk based on a Git object.
+
+    :param obj: The git object
+    :param mode: File mode
+    :param target_path: Path to write to
+    :param honor_filemode: An optional flag to honor core.filemode setting in
+        config file, default is core.filemode=True, change executable bit
+    """
+    if stat.S_ISLNK(mode):
+        # FIXME: This will fail on Windows. What should we do instead?
+        src_path = blob.as_raw_string()
+        try:
+            os.symlink(src_path, target_path)
+        except OSError as e:
+            if e.errno == errno.EEXIST:
+                os.unlink(target_path)
+                os.symlink(src_path, target_path)
+            else:
+                raise
+    else:
+        with open(target_path, 'wb') as f:
+            # Write out file
+            f.write(blob.as_raw_string())
+
+        if honor_filemode:
+            os.chmod(target_path, mode)
+
+
 def build_index_from_tree(prefix, index_path, object_store, tree_id,
                           honor_filemode=True):
     """Generate and materialize index from a tree
@@ -426,28 +455,9 @@ def build_index_from_tree(prefix, index_path, object_store, tree_id,
             os.makedirs(os.path.dirname(full_path))
 
         # FIXME: Merge new index into working tree
-        if stat.S_ISLNK(entry.mode):
-            # FIXME: This will fail on Windows. What should we do instead?
-            src_path = object_store[entry.sha].as_raw_string()
-            try:
-                os.symlink(src_path, full_path)
-            except OSError as e:
-                if e.errno == errno.EEXIST:
-                    os.unlink(full_path)
-                    os.symlink(src_path, full_path)
-                else:
-                    raise
-        else:
-            f = open(full_path, 'wb')
-            try:
-                # Write out file
-                f.write(object_store[entry.sha].as_raw_string())
-            finally:
-                f.close()
-
-            if honor_filemode:
-                os.chmod(full_path, entry.mode)
-
+        obj = object_store[entry.sha]
+        build_file_from_blob(obj, entry.mode, full_path,
+            honor_filemode=honor_filemode)
         # Add file to index
         st = os.lstat(full_path)
         index[entry.path] = index_entry_from_stat(st, entry.sha, 0)
@@ -464,11 +474,8 @@ def blob_from_path_and_stat(path, st):
     """
     blob = Blob()
     if not stat.S_ISLNK(st.st_mode):
-        f = open(path, 'rb')
-        try:
+        with open(path, 'rb') as f:
             blob.data = f.read()
-        finally:
-            f.close()
     else:
         blob.data = os.readlink(path)
     return blob

+ 9 - 27
dulwich/object_store.py

@@ -431,7 +431,7 @@ class DiskObjectStore(PackBasedObjectStore):
                 return []
             raise
         ret = []
-        try:
+        with f:
             for l in f.readlines():
                 l = l.rstrip("\n")
                 if l[0] == "#":
@@ -441,8 +441,6 @@ class DiskObjectStore(PackBasedObjectStore):
                 else:
                     ret.append(os.path.join(self.path, l))
             return ret
-        finally:
-            f.close()
 
     def add_alternate_path(self, path):
         """Add an alternate path to this object store.
@@ -453,21 +451,16 @@ class DiskObjectStore(PackBasedObjectStore):
             if e.errno != errno.EEXIST:
                 raise
         alternates_path = os.path.join(self.path, "info/alternates")
-        f = GitFile(alternates_path, 'wb')
-        try:
+        with GitFile(alternates_path, 'wb') as f:
             try:
                 orig_f = open(alternates_path, 'rb')
             except (OSError, IOError) as e:
                 if e.errno != errno.ENOENT:
                     raise
             else:
-                try:
+                with orig_f:
                     f.write(orig_f.read())
-                finally:
-                    orig_f.close()
             f.write("%s\n" % path)
-        finally:
-            f.close()
 
         if not os.path.isabs(path):
             path = os.path.join(self.path, path)
@@ -600,16 +593,12 @@ class DiskObjectStore(PackBasedObjectStore):
             objects/pack directory.
         """
         fd, path = tempfile.mkstemp(dir=self.path, prefix='tmp_pack_')
-        f = os.fdopen(fd, 'w+b')
-
-        try:
+        with os.fdopen(fd, 'w+b') as f:
             indexer = PackIndexer(f, resolve_ext_ref=self.get_raw)
             copier = PackStreamCopier(read_all, read_some, f,
                                       delta_iter=indexer)
             copier.verify()
             return self._complete_thin_pack(f, path, copier, indexer)
-        finally:
-            f.close()
 
     def move_in_pack(self, path):
         """Move a specific file containing a pack into the pack directory.
@@ -619,18 +608,12 @@ class DiskObjectStore(PackBasedObjectStore):
 
         :param path: Path to the pack file.
         """
-        p = PackData(path)
-        try:
+        with PackData(path) as p:
             entries = p.sorted_entries()
             basename = os.path.join(self.pack_dir,
                 "pack-%s" % iter_sha1(entry[0] for entry in entries))
-            f = GitFile(basename+".idx", "wb")
-            try:
+            with GitFile(basename+".idx", "wb") as f:
                 write_pack_index_v2(f, entries, p.get_stored_checksum())
-            finally:
-                f.close()
-        finally:
-            p.close()
         os.rename(path, basename + ".pack")
         final_pack = Pack(basename)
         self._add_known_pack(basename, final_pack)
@@ -672,11 +655,8 @@ class DiskObjectStore(PackBasedObjectStore):
         path = os.path.join(dir, obj.id[2:])
         if os.path.exists(path):
             return # Already there, no need to write again
-        f = GitFile(path, 'wb')
-        try:
+        with GitFile(path, 'wb') as f:
             f.write(obj.as_legacy_object())
-        finally:
-            f.close()
 
     @classmethod
     def init(cls, path):
@@ -1074,6 +1054,8 @@ class ObjectStoreGraphWalker(object):
 
     def ack(self, sha):
         """Ack that a revision and its ancestors are present in the source."""
+        if len(sha) != 40:
+            raise ValueError("unexpected sha %r received" % sha)
         ancestors = set([sha])
 
         # stop if we run out of heads to remove

+ 35 - 14
dulwich/objects.py

@@ -48,6 +48,7 @@ _AUTHOR_HEADER = "author"
 _COMMITTER_HEADER = "committer"
 _ENCODING_HEADER = "encoding"
 _MERGETAG_HEADER = "mergetag"
+_GPGSIG_HEADER = "gpgsig"
 
 # Header fields for objects
 _OBJECT_HEADER = "object"
@@ -359,11 +360,8 @@ class ShaFile(object):
         raise NotImplementedError(self._serialize)
 
     def _parse_path(self):
-        f = GitFile(self._path, 'rb')
-        try:
+        with GitFile(self._path, 'rb') as f:
             self._parse_file(f)
-        finally:
-            f.close()
 
     def _parse_file(self, f):
         magic = self._magic
@@ -378,16 +376,13 @@ class ShaFile(object):
     @classmethod
     def from_path(cls, path):
         """Open a SHA file from disk."""
-        f = GitFile(path, 'rb')
-        try:
+        with GitFile(path, 'rb') as f:
             obj = cls.from_file(f)
             obj._path = path
             obj._sha = FixedSha(filename_to_hex(path))
             obj._file = None
             obj._magic = None
             return obj
-        finally:
-            f.close()
 
     @classmethod
     def from_file(cls, f):
@@ -493,6 +488,14 @@ class ShaFile(object):
             self._sha = new_sha
         return self._sha
 
+    def copy(self):
+        """Create a new copy of this SHA1 object from its raw string"""
+        obj_class = object_class(self.get_type())
+        return obj_class.from_raw_string(
+            self.get_type(),
+            self.as_raw_string(),
+            self.id)
+
     @property
     def id(self):
         """The hex SHA of this object."""
@@ -1027,7 +1030,7 @@ def parse_commit(chunks):
 
     :param chunks: Chunks to parse
     :return: Tuple of (tree, parents, author_info, commit_info,
-        encoding, mergetag, message, extra)
+        encoding, mergetag, gpgsig, message, extra)
     """
     parents = []
     extra = []
@@ -1037,8 +1040,10 @@ def parse_commit(chunks):
     encoding = None
     mergetag = []
     message = None
+    gpgsig = None
 
     for field, value in _parse_message(chunks):
+        # TODO(jelmer): Enforce ordering
         if field == _TREE_HEADER:
             tree = value
         elif field == _PARENT_HEADER:
@@ -1055,12 +1060,14 @@ def parse_commit(chunks):
             encoding = value
         elif field == _MERGETAG_HEADER:
             mergetag.append(Tag.from_string(value + "\n"))
+        elif field == _GPGSIG_HEADER:
+            gpgsig = value
         elif field is None:
             message = value
         else:
             extra.append((field, value))
     return (tree, parents, author_info, commit_info, encoding, mergetag,
-            message, extra)
+            gpgsig, message, extra)
 
 
 class Commit(ShaFile):
@@ -1073,13 +1080,14 @@ class Commit(ShaFile):
                  '_commit_timezone_neg_utc', '_commit_time',
                  '_author_time', '_author_timezone', '_commit_timezone',
                  '_author', '_committer', '_parents', '_extra',
-                 '_encoding', '_tree', '_message', '_mergetag')
+                 '_encoding', '_tree', '_message', '_mergetag', '_gpgsig')
 
     def __init__(self):
         super(Commit, self).__init__()
         self._parents = []
         self._encoding = None
         self._mergetag = []
+        self._gpgsig = None
         self._extra = []
         self._author_timezone_neg_utc = False
         self._commit_timezone_neg_utc = False
@@ -1093,7 +1101,7 @@ class Commit(ShaFile):
 
     def _deserialize(self, chunks):
         (self._tree, self._parents, author_info, commit_info, self._encoding,
-                self._mergetag, self._message, self._extra) = (
+                self._mergetag, self._gpgsig, self._message, self._extra) = (
                         parse_commit(chunks))
         (self._author, self._author_time, (self._author_timezone,
              self._author_timezone_neg_utc)) = author_info
@@ -1166,6 +1174,11 @@ class Commit(ShaFile):
                 raise AssertionError(
                     "newline in extra data: %r -> %r" % (k, v))
             chunks.append("%s %s\n" % (k, v))
+        if self.gpgsig:
+            sig_chunks = self.gpgsig.split("\n")
+            chunks.append("%s %s\n" % (_GPGSIG_HEADER, sig_chunks[0]))
+            for chunk in sig_chunks[1:]:
+                chunks.append(" %s\n" % chunk)
         chunks.append("\n")  # There must be a new line after the headers
         chunks.append(self._message)
         return chunks
@@ -1184,14 +1197,19 @@ class Commit(ShaFile):
         self._needs_serialization = True
         self._parents = value
 
-    parents = property(_get_parents, _set_parents)
+    parents = property(_get_parents, _set_parents,
+                       doc="Parents of this commit, by their SHA1.")
 
     def _get_extra(self):
         """Return extra settings of this commit."""
         self._ensure_parsed()
         return self._extra
 
-    extra = property(_get_extra)
+    extra = property(_get_extra,
+        doc="Extra header fields not understood (presumably added in a "
+            "newer version of git). Kept verbatim so the object can "
+            "be correctly reserialized. For private commit metadata, use "
+            "pseudo-headers in Commit.message, rather than this field.")
 
     author = serializable_property("author",
         "The name of the author of the commit")
@@ -1221,6 +1239,9 @@ class Commit(ShaFile):
     mergetag = serializable_property(
         "mergetag", "Associated signed tag.")
 
+    gpgsig = serializable_property(
+        "gpgsig", "GPG Signature.")
+
 
 OBJECT_CLASSES = (
     Commit,

+ 33 - 36
dulwich/pack.py

@@ -33,13 +33,19 @@ a pointer in to the corresponding packfile.
 from collections import defaultdict
 
 import binascii
-from io import BytesIO
+from io import BytesIO, UnsupportedOperation
 from collections import (
     deque,
     )
 import difflib
 
-from itertools import chain, imap, izip
+from itertools import chain
+try:
+    from itertools import imap, izip
+except ImportError:
+    # Python3
+    imap = map
+    izip = zip
 
 try:
     import mmap
@@ -55,7 +61,6 @@ from os import (
     )
 import struct
 from struct import unpack_from
-import warnings
 import zlib
 
 from dulwich.errors import (
@@ -258,18 +263,17 @@ def load_pack_index(path):
     :param filename: Path to the index file
     :return: A PackIndex loaded from the given path
     """
-    f = GitFile(path, 'rb')
-    try:
+    with GitFile(path, 'rb') as f:
         return load_pack_index_file(path, f)
-    finally:
-        f.close()
 
 
 def _load_file_contents(f, size=None):
-    fileno = getattr(f, 'fileno', None)
-    # Attempt to use mmap if possible
-    if fileno is not None:
+    try:
         fd = f.fileno()
+    except (UnsupportedOperation, AttributeError):
+        fd = None
+    # Attempt to use mmap if possible
+    if fd is not None:
         if size is None:
             size = os.fstat(fd).st_size
         if has_mmap:
@@ -919,7 +923,13 @@ def compute_file_sha(f, start_ofs=0, end_ofs=0, buffer_size=1<<16):
     """
     sha = sha1()
     f.seek(0, SEEK_END)
-    todo = f.tell() + end_ofs - start_ofs
+    length = f.tell()
+    if (end_ofs < 0 and length + end_ofs < start_ofs) or end_ofs > length:
+        raise AssertionError(
+            "Attempt to read beyond file length. "
+            "start_ofs: %d, end_ofs: %d, file length: %d" % (
+                start_ofs, end_ofs, length))
+    todo = length + end_ofs - start_ofs
     f.seek(start_ofs)
     while todo:
         data = f.read(min(todo, buffer_size))
@@ -934,7 +944,7 @@ class PackData(object):
     Pack files can be accessed both sequentially for exploding a pack, and
     directly with the help of an index to retrieve a specific object.
 
-    The objects within are either complete or a delta aginst another.
+    The objects within are either complete or a delta against another.
 
     The header is variable length. If the MSB of each byte is set then it
     indicates that the subsequent byte is still part of the header.
@@ -1128,11 +1138,8 @@ class PackData(object):
         :return: Checksum of index file
         """
         entries = self.sorted_entries(progress=progress)
-        f = GitFile(filename, 'wb')
-        try:
+        with GitFile(filename, 'wb') as f:
             return write_pack_index_v1(f, entries, self.calculate_checksum())
-        finally:
-            f.close()
 
     def create_index_v2(self, filename, progress=None):
         """Create a version 2 index file for this data file.
@@ -1142,11 +1149,8 @@ class PackData(object):
         :return: Checksum of index file
         """
         entries = self.sorted_entries(progress=progress)
-        f = GitFile(filename, 'wb')
-        try:
+        with GitFile(filename, 'wb') as f:
             return write_pack_index_v2(f, entries, self.calculate_checksum())
-        finally:
-            f.close()
 
     def create_index(self, filename, progress=None,
                      version=2):
@@ -1457,19 +1461,13 @@ def write_pack(filename, objects, deltify=None, delta_window_size=None):
     :param deltify: Whether to deltify pack objects
     :return: Tuple with checksum of pack file and index file
     """
-    f = GitFile(filename + '.pack', 'wb')
-    try:
+    with GitFile(filename + '.pack', 'wb') as f:
         entries, data_sum = write_pack_objects(f, objects,
             delta_window_size=delta_window_size, deltify=deltify)
-    finally:
-        f.close()
     entries = [(k, v[0], v[1]) for (k, v) in entries.iteritems()]
     entries.sort()
-    f = GitFile(filename + '.idx', 'wb')
-    try:
+    with GitFile(filename + '.idx', 'wb') as f:
         return data_sum, write_pack_index_v2(f, entries, data_sum)
-    finally:
-        f.close()
 
 
 def write_pack_header(f, num_objects):
@@ -1602,9 +1600,10 @@ def _delta_encode_size(size):
     return ret
 
 
-# copy operations in git's delta format can be at most this long -
-# after this you have to decompose the copy into multiple operations.
-_MAX_COPY_LEN = 0xffffff
+# The length of delta compression copy operations in version 2 packs is limited
+# to 64K.  To copy more, we use several copy operations.  Version 3 packs allow
+# 24-bit lengths in copy operations, but we always make version 2 packs.
+_MAX_COPY_LEN = 0xffff
 
 def _encode_copy_operation(start, length):
     scratch = ''
@@ -1613,7 +1612,7 @@ def _encode_copy_operation(start, length):
         if start & 0xff << i*8:
             scratch += chr((start >> i*8) & 0xff)
             op |= 1 << i
-    for i in range(3):
+    for i in range(2):
         if length & 0xff << i*8:
             scratch += chr((length >> i*8) & 0xff)
             op |= 1 << (4+i)
@@ -1701,6 +1700,7 @@ def apply_delta(src_buf, delta):
                     index += 1
                     cp_off |= x << (i * 8)
             cp_size = 0
+            # Version 3 packs can contain copy sizes larger than 64K.
             for i in range(3):
                 if cmd & (1 << (4+i)):
                     x = ord(delta[index])
@@ -1918,13 +1918,10 @@ class Pack(object):
         :return: The path of the .keep file, as a string.
         """
         keepfile_name = '%s.keep' % self._basename
-        keepfile = GitFile(keepfile_name, 'wb')
-        try:
+        with GitFile(keepfile_name, 'wb') as keepfile:
             if msg:
                 keepfile.write(msg)
                 keepfile.write('\n')
-        finally:
-            keepfile.close()
         return keepfile_name
 
 

+ 176 - 13
dulwich/porcelain.py

@@ -21,19 +21,22 @@
 Currently implemented:
  * archive
  * add
+ * branch{_create,_delete,_list}
  * clone
  * commit
  * commit-tree
  * daemon
  * diff-tree
+ * fetch
  * init
- * list-tags
  * pull
  * push
  * rm
+ * receive-pack
  * reset
  * rev-list
- * tag
+ * tag{_create,_delete,_list}
+ * upload-pack
  * update-server-info
  * status
  * symbolic-ref
@@ -62,8 +65,16 @@ from dulwich.objects import (
     )
 from dulwich.objectspec import parse_object
 from dulwich.patch import write_tree_diff
+from dulwich.protocol import Protocol
 from dulwich.repo import (BaseRepo, Repo)
-from dulwich.server import update_server_info as server_update_server_info
+from dulwich.server import (
+    FileSystemBackend,
+    TCPGitServer,
+    ReceivePackHandler,
+    UploadPackHandler,
+    update_server_info as server_update_server_info,
+    )
+
 
 # Module level tuple definition for status output
 GitStatus = namedtuple('GitStatus', 'staged unstaged untracked')
@@ -368,7 +379,13 @@ def rev_list(repo, commits, outstream=sys.stdout):
         outstream.write("%s\n" % entry.commit.id)
 
 
-def tag(repo, tag, author=None, message=None, annotated=False,
+def tag(*args, **kwargs):
+    import warnings
+    warnings.warn(DeprecationWarning, "tag has been deprecated in favour of tag_create.")
+    return tag_create(*args, **kwargs)
+
+
+def tag_create(repo, tag, author=None, message=None, annotated=False,
         objectish="HEAD", tag_time=None, tag_timezone=None):
     """Creates a tag in git via dulwich calls:
 
@@ -412,7 +429,13 @@ def tag(repo, tag, author=None, message=None, annotated=False,
     r.refs['refs/tags/' + tag] = tag_id
 
 
-def list_tags(repo, outstream=sys.stdout):
+def list_tags(*args, **kwargs):
+    import warnings
+    warnings.warn(DeprecationWarning, "list_tags has been deprecated in favour of tag_list.")
+    return tag_list(*args, **kwargs)
+
+
+def tag_list(repo, outstream=sys.stdout):
     """List all tags.
 
     :param repo: Path to repository
@@ -424,6 +447,23 @@ def list_tags(repo, outstream=sys.stdout):
     return tags
 
 
+def tag_delete(repo, name):
+    """Remove a tag.
+
+    :param repo: Path to repository
+    :param name: Name of tag to remove
+    """
+    r = open_repo(repo)
+    if isinstance(name, str):
+        names = [name]
+    elif isinstance(name, list):
+        names = name
+    else:
+        raise TypeError("Unexpected tag name type %r" % name)
+    for name in names:
+        del r.refs["refs/tags/" + name]
+
+
 def reset(repo, mode, committish="HEAD"):
     """Reset current HEAD to the specified state.
 
@@ -497,19 +537,21 @@ def pull(repo, remote_location, refs_path,
     index.build_index_from_tree(r.path, indexfile, r.object_store, tree)
 
 
-def status(repo):
+def status(repo="."):
     """Returns staged, unstaged, and untracked changes relative to the HEAD.
 
-    :param repo: Path to repository
+    :param repo: Path to repository or repository object
     :return: GitStatus tuple,
         staged -    list of staged paths (diff index/HEAD)
         unstaged -  list of unstaged paths (diff index/working-tree)
         untracked - list of untracked, un-ignored & non-.git paths
     """
+    r = open_repo(repo)
+
     # 1. Get status of staged
-    tracked_changes = get_tree_changes(repo)
+    tracked_changes = get_tree_changes(r)
     # 2. Get status of unstaged
-    unstaged_changes = list(get_unstaged_changes(repo.open_index(), repo.path))
+    unstaged_changes = list(get_unstaged_changes(r.open_index(), r.path))
     # TODO - Status of untracked - add untracked changes, need gitignore.
     untracked_changes = []
     return GitStatus(tracked_changes, unstaged_changes, untracked_changes)
@@ -526,6 +568,7 @@ def get_tree_changes(repo):
 
     # Compares the Index to the HEAD & determines changes
     # Iterate through the changes and report add/delete/modify
+    # TODO: call out to dulwich.diff_tree somehow.
     tracked_changes = {
         'add': [],
         'delete': [],
@@ -547,12 +590,132 @@ def daemon(path=".", address=None, port=None):
     """Run a daemon serving Git requests over TCP/IP.
 
     :param path: Path to the directory to serve.
+    :param address: Optional address to listen on (defaults to ::)
+    :param port: Optional port to listen on (defaults to TCP_GIT_PORT)
     """
     # TODO(jelmer): Support git-daemon-export-ok and --export-all.
-    from dulwich.server import (
-        FileSystemBackend,
-        TCPGitServer,
-        )
     backend = FileSystemBackend(path)
     server = TCPGitServer(backend, address, port)
     server.serve_forever()
+
+
+def web_daemon(path=".", address=None, port=None):
+    """Run a daemon serving Git requests over HTTP.
+
+    :param path: Path to the directory to serve
+    :param address: Optional address to listen on (defaults to ::)
+    :param port: Optional port to listen on (defaults to 80)
+    """
+    from dulwich.web import (
+        make_wsgi_chain,
+        make_server,
+        WSGIRequestHandlerLogger,
+        WSGIServerLogger)
+
+    backend = FileSystemBackend(path)
+    app = make_wsgi_chain(backend)
+    server = make_server(address, port, app,
+                         handler_class=WSGIRequestHandlerLogger,
+                         server_class=WSGIServerLogger)
+    server.serve_forever()
+
+
+def upload_pack(path=".", inf=sys.stdin, outf=sys.stdout):
+    """Upload a pack file after negotiating its contents using smart protocol.
+
+    :param path: Path to the repository
+    :param inf: Input stream to communicate with client
+    :param outf: Output stream to communicate with client
+    """
+    backend = FileSystemBackend()
+    def send_fn(data):
+        outf.write(data)
+        outf.flush()
+    proto = Protocol(inf.read, send_fn)
+    handler = UploadPackHandler(backend, [path], proto)
+    # FIXME: Catch exceptions and write a single-line summary to outf.
+    handler.handle()
+    return 0
+
+
+def receive_pack(path=".", inf=sys.stdin, outf=sys.stdout):
+    """Receive a pack file after negotiating its contents using smart protocol.
+
+    :param path: Path to the repository
+    :param inf: Input stream to communicate with client
+    :param outf: Output stream to communicate with client
+    """
+    backend = FileSystemBackend()
+    def send_fn(data):
+        outf.write(data)
+        outf.flush()
+    proto = Protocol(inf.read, send_fn)
+    handler = ReceivePackHandler(backend, [path], proto)
+    # FIXME: Catch exceptions and write a single-line summary to outf.
+    handler.handle()
+    return 0
+
+
+def branch_delete(repo, name):
+    """Delete a branch.
+
+    :param repo: Path to the repository
+    :param name: Name of the branch
+    """
+    r = open_repo(repo)
+    if isinstance(name, str):
+        names = [name]
+    elif isinstance(name, list):
+        names = name
+    else:
+        raise TypeError("Unexpected branch name type %r" % name)
+    for name in names:
+        del r.refs["refs/heads/" + name]
+
+
+def branch_create(repo, name, objectish=None, force=False):
+    """Create a branch.
+
+    :param repo: Path to the repository
+    :param name: Name of the new branch
+    :param objectish: Target object to point new branch at (defaults to HEAD)
+    :param force: Force creation of branch, even if it already exists
+    """
+    r = open_repo(repo)
+    if isinstance(name, str):
+        names = [name]
+    elif isinstance(name, list):
+        names = name
+    else:
+        raise TypeError("Unexpected branch name type %r" % name)
+    if objectish is None:
+        objectish = "HEAD"
+    object = parse_object(r, objectish)
+    refname = "refs/heads/" + name
+    if refname in r.refs and not force:
+        raise KeyError("Branch with name %s already exists." % name)
+    r.refs[refname] = object.id
+
+
+def branch_list(repo):
+    """List all branches.
+
+    :param repo: Path to the repository
+    """
+    r = open_repo(repo)
+    return r.refs.keys(base="refs/heads/")
+
+
+def fetch(repo, remote_location, outstream=sys.stdout, errstream=sys.stderr):
+    """Fetch objects from a remote server.
+
+    :param repo: Path to the repository
+    :param remote_location: String identifying a remote server
+    :param outstream: Output stream (defaults to stdout)
+    :param errstream: Error stream (defaults to stderr)
+    :return: Dictionary with refs on the remote
+    """
+    r = open_repo(repo)
+    client, path = get_transport_and_path(remote_location)
+    remote_refs = client.fetch(path, r, progress=errstream.write)
+    return remote_refs

+ 1 - 1
dulwich/protocol.py

@@ -121,7 +121,7 @@ class Protocol(object):
                 self.report_activity(size, 'read')
             pkt_contents = read(size-4)
             if len(pkt_contents) + 4 != size:
-                raise AssertionError('Length of pkt read {:04x} does not match length prefix {:04x}.'
+                raise AssertionError('Length of pkt read %04x does not match length prefix %04x.'
                                      .format(len(pkt_contents) + 4, size))
             return pkt_contents
         except socket.error as e:

+ 8 - 15
dulwich/refs.py

@@ -38,6 +38,7 @@ from dulwich.file import (
 
 
 SYMREF = 'ref: '
+LOCAL_BRANCH_PREFIX = 'refs/heads/'
 
 
 def check_ref_format(refname):
@@ -446,7 +447,7 @@ class DiskRefsContainer(RefsContainer):
                 if e.errno == errno.ENOENT:
                     return {}
                 raise
-            try:
+            with f:
                 first_line = next(iter(f)).rstrip()
                 if (first_line.startswith("# pack-refs") and " peeled" in
                         first_line):
@@ -458,8 +459,6 @@ class DiskRefsContainer(RefsContainer):
                     f.seek(0)
                     for sha, name in read_packed_refs(f):
                         self._packed_refs[name] = sha
-            finally:
-                f.close()
         return self._packed_refs
 
     def get_peeled(self, name):
@@ -493,8 +492,7 @@ class DiskRefsContainer(RefsContainer):
         """
         filename = self.refpath(name)
         try:
-            f = GitFile(filename, 'rb')
-            try:
+            with GitFile(filename, 'rb') as f:
                 header = f.read(len(SYMREF))
                 if header == SYMREF:
                     # Read only the first line
@@ -502,8 +500,6 @@ class DiskRefsContainer(RefsContainer):
                 else:
                     # Read only the first 40 bytes
                     return header + f.read(40 - len(SYMREF))
-            finally:
-                f.close()
         except IOError as e:
             if e.errno == errno.ENOENT:
                 return None
@@ -568,8 +564,7 @@ class DiskRefsContainer(RefsContainer):
             realname = name
         filename = self.refpath(realname)
         ensure_dir_exists(os.path.dirname(filename))
-        f = GitFile(filename, 'wb')
-        try:
+        with GitFile(filename, 'wb') as f:
             if old_ref is not None:
                 try:
                     # read again while holding the lock
@@ -587,8 +582,6 @@ class DiskRefsContainer(RefsContainer):
             except (OSError, IOError):
                 f.abort()
                 raise
-        finally:
-            f.close()
         return True
 
     def add_if_new(self, name, ref):
@@ -610,8 +603,7 @@ class DiskRefsContainer(RefsContainer):
         self._check_refname(realname)
         filename = self.refpath(realname)
         ensure_dir_exists(os.path.dirname(filename))
-        f = GitFile(filename, 'wb')
-        try:
+        with GitFile(filename, 'wb') as f:
             if os.path.exists(filename) or name in self.get_packed_refs():
                 f.abort()
                 return False
@@ -620,8 +612,6 @@ class DiskRefsContainer(RefsContainer):
             except (OSError, IOError):
                 f.abort()
                 raise
-        finally:
-            f.close()
         return True
 
     def remove_if_equals(self, name, old_ref):
@@ -763,3 +753,6 @@ def write_info_refs(refs, store):
         yield '%s\t%s\n' % (o.id, name)
         if o.id != peeled.id:
             yield '%s\t%s^{}\n' % (peeled.id, name)
+
+
+is_local_branch = lambda x: x.startswith("refs/heads/")

+ 4 - 16
dulwich/repo.py

@@ -647,11 +647,8 @@ class Repo(BaseRepo):
             self._controldir = root
         elif (os.path.isfile(os.path.join(root, ".git"))):
             import re
-            f = open(os.path.join(root, ".git"), 'r')
-            try:
+            with open(os.path.join(root, ".git"), 'r') as f:
                 _, path = re.match('(gitdir: )(.+$)', f.read()).groups()
-            finally:
-                f.close()
             self.bare = False
             self._controldir = os.path.join(root, path)
         else:
@@ -689,11 +686,8 @@ class Repo(BaseRepo):
         :param contents: A string to write to the file.
         """
         path = path.lstrip(os.path.sep)
-        f = GitFile(os.path.join(self.controldir(), path), 'wb')
-        try:
+        with GitFile(os.path.join(self.controldir(), path), 'wb') as f:
             f.write(contents)
-        finally:
-            f.close()
 
     def get_named_file(self, path):
         """Get a file from the control dir with a specific name.
@@ -834,11 +828,8 @@ class Repo(BaseRepo):
         """
         path = os.path.join(self._controldir, 'description')
         try:
-            f = GitFile(path, 'rb')
-            try:
+            with GitFile(path, 'rb') as f:
                 return f.read()
-            finally:
-                f.close()
         except (IOError, OSError) as e:
             if e.errno != errno.ENOENT:
                 raise
@@ -854,11 +845,8 @@ class Repo(BaseRepo):
         """
 
         path = os.path.join(self._controldir, 'description')
-        f = open(path, 'w')
-        try:
+        with open(path, 'w') as f:
             f.write(description)
-        finally:
-            f.close()
 
     @classmethod
     def _init_maybe_bare(cls, path, bare):

+ 60 - 42
dulwich/server.py

@@ -36,18 +36,20 @@ Currently supported capabilities:
  * no-progress
  * report-status
  * delete-refs
-
-Known capabilities that are not supported:
- * shallow (http://pad.lv/909524)
+ * shallow
 """
 
 import collections
 import os
 import socket
-import SocketServer
 import sys
 import zlib
 
+try:
+    import SocketServer
+except ImportError:
+    import socketserver as SocketServer
+
 from dulwich.errors import (
     ApplyDeltaError,
     ChecksumMismatch,
@@ -162,9 +164,16 @@ class DictBackend(Backend):
 class FileSystemBackend(Backend):
     """Simple backend that looks up Git repositories in the local file system."""
 
+    def __init__(self, root="/"):
+        super(FileSystemBackend, self).__init__()
+        self.root = (os.path.abspath(root) + "/").replace("//", "/")
+
     def open_repository(self, path):
         logger.debug('opening repository at %s', path)
-        return Repo(path)
+        abspath = os.path.abspath(os.path.join(self.root, path)) + "/"
+        if not abspath.startswith(self.root):
+            raise NotGitRepository("Invalid path %r" % path)
+        return Repo(abspath)
 
 
 class Handler(object):
@@ -365,6 +374,46 @@ def _find_shallow(store, heads, depth):
     return shallow, not_shallow
 
 
+def _want_satisfied(store, haves, want, earliest):
+    o = store[want]
+    pending = collections.deque([o])
+    while pending:
+        commit = pending.popleft()
+        if commit.id in haves:
+            return True
+        if commit.type_name != "commit":
+            # non-commit wants are assumed to be satisfied
+            continue
+        for parent in commit.parents:
+            parent_obj = store[parent]
+            # TODO: handle parents with later commit times than children
+            if parent_obj.commit_time >= earliest:
+                pending.append(parent_obj)
+    return False
+
+
+def _all_wants_satisfied(store, haves, wants):
+    """Check whether all the current wants are satisfied by a set of haves.
+
+    :param store: Object store to retrieve objects from
+    :param haves: A set of commits we know the client has.
+    :param wants: A set of commits the client wants
+    :note: Wants are specified with set_wants rather than passed in since
+        in the current interface they are determined outside this class.
+    """
+    haves = set(haves)
+    if haves:
+        earliest = min([store[h].commit_time for h in haves])
+    else:
+        earliest = 0
+    unsatisfied_wants = set()
+    for want in wants:
+        if not _want_satisfied(store, haves, want, earliest):
+            return False
+
+    return True
+
+
 class ProtocolGraphWalker(object):
     """A graph walker that knows the git protocol.
 
@@ -464,6 +513,8 @@ class ProtocolGraphWalker(object):
         self.proto.unread_pkt_line('%s %s' % (command, value))
 
     def ack(self, have_ref):
+        if len(have_ref) != 40:
+            raise ValueError("invalid sha %r" % have_ref)
         return self._impl.ack(have_ref)
 
     def reset(self):
@@ -526,34 +577,6 @@ class ProtocolGraphWalker(object):
     def set_wants(self, wants):
         self._wants = wants
 
-    def _is_satisfied(self, haves, want, earliest):
-        """Check whether a want is satisfied by a set of haves.
-
-        A want, typically a branch tip, is "satisfied" only if there exists a
-        path back from that want to one of the haves.
-
-        :param haves: A set of commits we know the client has.
-        :param want: The want to check satisfaction for.
-        :param earliest: A timestamp beyond which the search for haves will be
-            terminated, presumably because we're searching too far down the
-            wrong branch.
-        """
-        o = self.store[want]
-        pending = collections.deque([o])
-        while pending:
-            commit = pending.popleft()
-            if commit.id in haves:
-                return True
-            if commit.type_name != "commit":
-                # non-commit wants are assumed to be satisfied
-                continue
-            for parent in commit.parents:
-                parent_obj = self.store[parent]
-                # TODO: handle parents with later commit times than children
-                if parent_obj.commit_time >= earliest:
-                    pending.append(parent_obj)
-        return False
-
     def all_wants_satisfied(self, haves):
         """Check whether all the current wants are satisfied by a set of haves.
 
@@ -561,12 +584,7 @@ class ProtocolGraphWalker(object):
         :note: Wants are specified with set_wants rather than passed in since
             in the current interface they are determined outside this class.
         """
-        haves = set(haves)
-        earliest = min([self.store[h].commit_time for h in haves])
-        for want in self._wants:
-            if not self._is_satisfied(haves, want, earliest):
-                return False
-        return True
+        return _all_wants_satisfied(self.store, haves, self._wants)
 
     def set_ack_type(self, ack_type):
         impl_classes = {
@@ -774,9 +792,9 @@ class ReceivePackHandler(Handler):
         flush()
 
     def handle(self):
-        refs = sorted(self.repo.get_refs().iteritems())
-
         if self.advertise_refs or not self.http_req:
+            refs = sorted(self.repo.get_refs().iteritems())
+
             if refs:
                 self.proto.write_pkt_line(
                   "%s %s\x00%s\n" % (refs[0][1], refs[0][0],
@@ -880,7 +898,7 @@ def main(argv=sys.argv):
     options, args = parser.parse_args(argv)
 
     log_utils.default_logging_config()
-    if len(argv) > 1:
+    if len(args) > 1:
         gitdir = args[1]
     else:
         gitdir = '.'

+ 6 - 3
dulwich/tests/__init__.py

@@ -29,7 +29,10 @@ import tempfile
 
 # If Python itself provides an exception, use that
 import unittest
-from unittest import TestCase as _TestCase
+if sys.version_info < (2, 7):
+    from unittest2 import SkipTest, TestCase as _TestCase, skipIf
+else:
+    from unittest import SkipTest, TestCase as _TestCase, skipIf
 
 
 def get_safe_env(env=None):
@@ -101,7 +104,6 @@ class BlackboxTestCase(TestCase):
         return subprocess.Popen(argv,
             stdout=subprocess.PIPE,
             stdin=subprocess.PIPE, stderr=subprocess.PIPE,
-            universal_newlines=True,
             env=env)
 
 
@@ -177,7 +179,8 @@ def compat_test_suite():
 def test_suite():
     result = unittest.TestSuite()
     result.addTests(self_test_suite())
-    result.addTests(tutorial_test_suite())
+    if sys.version_info[0] == 2:
+        result.addTests(tutorial_test_suite())
     from dulwich.tests.compat import test_suite as compat_test_suite
     result.addTests(compat_test_suite())
     from dulwich.contrib import test_suite as contrib_test_suite

+ 3 - 3
dulwich/tests/compat/server_utils.py

@@ -32,6 +32,7 @@ from dulwich.server import (
     )
 from dulwich.tests.utils import (
     tear_down_repo,
+    skipIfPY3,
     )
 from dulwich.tests.compat.utils import (
     import_repo,
@@ -53,18 +54,17 @@ def _get_shallow(repo):
     if not shallow_file:
         return []
     shallows = []
-    try:
+    with shallow_file:
         for line in shallow_file:
             sha = line.strip()
             if not sha:
                 continue
             hex_to_sha(sha)
             shallows.append(sha)
-    finally:
-        shallow_file.close()
     return shallows
 
 
+@skipIfPY3
 class ServerTests(object):
     """Base tests for testing servers.
 

+ 42 - 16
dulwich/tests/compat/test_client.py

@@ -20,19 +20,28 @@
 """Compatibilty tests between the Dulwich client and the cgit server."""
 
 from io import BytesIO
-import BaseHTTPServer
-import SimpleHTTPServer
 import copy
 import os
 import select
 import shutil
 import signal
 import subprocess
+import sys
 import tarfile
 import tempfile
 import threading
 import urllib
-from unittest import SkipTest
+
+try:
+    import BaseHTTPServer
+    import SimpleHTTPServer
+except ImportError:
+    import http.server
+    BaseHTTPServer = http.server
+    SimpleHTTPServer = http.server
+
+if sys.platform == 'win32':
+    import ctypes
 
 from dulwich import (
     client,
@@ -45,16 +54,21 @@ from dulwich import (
     )
 from dulwich.tests import (
     get_safe_env,
+    SkipTest,
+    )
+from dulwich.tests.utils import (
+    skipIfPY3,
     )
-
 from dulwich.tests.compat.utils import (
     CompatTestCase,
     check_for_daemon,
     import_repo_to_dir,
     run_git_or_fail,
+    _DEFAULT_GIT,
     )
 
 
+@skipIfPY3
 class DulwichClientTestBase(object):
     """Tests for client/server compatibility."""
 
@@ -235,25 +249,37 @@ class DulwichTCPClientTest(CompatTestCase, DulwichClientTestBase):
         if check_for_daemon(limit=1):
             raise SkipTest('git-daemon was already running on port %s' %
                               protocol.TCP_GIT_PORT)
+        env = get_safe_env()
         fd, self.pidfile = tempfile.mkstemp(prefix='dulwich-test-git-client',
                                             suffix=".pid")
         os.fdopen(fd).close()
-        run_git_or_fail(
-            ['daemon', '--verbose', '--export-all',
-             '--pid-file=%s' % self.pidfile, '--base-path=%s' % self.gitroot,
-             '--detach', '--reuseaddr', '--enable=receive-pack',
-             '--enable=upload-archive', '--listen=localhost', self.gitroot], cwd=self.gitroot)
+        args = [_DEFAULT_GIT, 'daemon', '--verbose', '--export-all',
+                '--pid-file=%s' % self.pidfile,
+                '--base-path=%s' % self.gitroot,
+                '--enable=receive-pack', '--enable=upload-archive',
+                '--listen=localhost', '--reuseaddr',
+                self.gitroot]
+        self.process = subprocess.Popen(
+            args, env=env, cwd=self.gitroot,
+            stdout=subprocess.PIPE, stderr=subprocess.PIPE)
         if not check_for_daemon():
             raise SkipTest('git-daemon failed to start')
 
     def tearDown(self):
-        try:
-            with open(self.pidfile) as f:
-                pid = f.read()
-            os.kill(int(pid.strip()), signal.SIGKILL)
-            os.unlink(self.pidfile)
-        except (OSError, IOError):
-            pass
+        with open(self.pidfile) as f:
+            pid = int(f.read().strip())
+        if sys.platform == 'win32':
+            PROCESS_TERMINATE = 1
+            handle = ctypes.windll.kernel32.OpenProcess(
+                PROCESS_TERMINATE, False, pid)
+            ctypes.windll.kernel32.TerminateProcess(handle, -1)
+            ctypes.windll.kernel32.CloseHandle(handle)
+        else:
+            try:
+                os.kill(pid, signal.SIGKILL)
+                os.unlink(self.pidfile)
+            except (OSError, IOError):
+                pass
         DulwichClientTestBase.tearDown(self)
         CompatTestCase.tearDown(self)
 

+ 3 - 1
dulwich/tests/compat/test_pack.py

@@ -25,7 +25,6 @@ import os
 import re
 import shutil
 import tempfile
-from unittest import SkipTest
 
 from dulwich.pack import (
     write_pack,
@@ -33,6 +32,9 @@ from dulwich.pack import (
 from dulwich.objects import (
     Blob,
     )
+from dulwich.tests import (
+    SkipTest,
+    )
 from dulwich.tests.test_pack import (
     a_sha,
     pack1_sha,

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

@@ -32,6 +32,7 @@ from dulwich.repo import (
     )
 from dulwich.tests.utils import (
     tear_down_repo,
+    skipIfPY3,
     )
 
 from dulwich.tests.compat.utils import (
@@ -41,6 +42,7 @@ from dulwich.tests.compat.utils import (
     )
 
 
+@skipIfPY3
 class ObjectStoreTestCase(CompatTestCase):
     """Tests for git repository compatibility."""
 

+ 7 - 8
dulwich/tests/compat/test_utils.py

@@ -19,9 +19,8 @@
 
 """Tests for git compatibility utilities."""
 
-from unittest import SkipTest
-
 from dulwich.tests import (
+    SkipTest,
     TestCase,
     )
 from dulwich.tests.compat import utils
@@ -44,19 +43,19 @@ class GitVersionTests(TestCase):
         utils.run_git = self._orig_run_git
 
     def test_git_version_none(self):
-        self._version_str = 'not a git version'
+        self._version_str = b'not a git version'
         self.assertEqual(None, utils.git_version())
 
     def test_git_version_3(self):
-        self._version_str = 'git version 1.6.6'
+        self._version_str = b'git version 1.6.6'
         self.assertEqual((1, 6, 6, 0), utils.git_version())
 
     def test_git_version_4(self):
-        self._version_str = 'git version 1.7.0.2'
+        self._version_str = b'git version 1.7.0.2'
         self.assertEqual((1, 7, 0, 2), utils.git_version())
 
     def test_git_version_extra(self):
-        self._version_str = 'git version 1.7.0.3.295.gd8fa2'
+        self._version_str = b'git version 1.7.0.3.295.gd8fa2'
         self.assertEqual((1, 7, 0, 3), utils.git_version())
 
     def assertRequireSucceeds(self, required_version):
@@ -71,7 +70,7 @@ class GitVersionTests(TestCase):
 
     def test_require_git_version(self):
         try:
-            self._version_str = 'git version 1.6.6'
+            self._version_str = b'git version 1.6.6'
             self.assertRequireSucceeds((1, 6, 6))
             self.assertRequireSucceeds((1, 6, 6, 0))
             self.assertRequireSucceeds((1, 6, 5))
@@ -81,7 +80,7 @@ class GitVersionTests(TestCase):
             self.assertRaises(ValueError, utils.require_git_version,
                               (1, 6, 6, 0, 0))
 
-            self._version_str = 'git version 1.7.0.2'
+            self._version_str = b'git version 1.7.0.2'
             self.assertRequireSucceeds((1, 6, 6))
             self.assertRequireSucceeds((1, 6, 6, 0))
             self.assertRequireSucceeds((1, 7, 0))

+ 3 - 3
dulwich/tests/compat/test_web.py

@@ -25,14 +25,14 @@ warning: these tests should be fairly stable, but when writing/debugging new
 """
 
 import threading
-from unittest import (
-    SkipTest,
-    )
 from wsgiref import simple_server
 
 from dulwich.server import (
     DictBackend,
     )
+from dulwich.tests import (
+    SkipTest,
+    )
 from dulwich.web import (
     make_wsgi_chain,
     HTTPGitApplication,

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

@@ -25,13 +25,13 @@ import socket
 import subprocess
 import tempfile
 import time
-from unittest import SkipTest
 
 from dulwich.repo import Repo
 from dulwich.protocol import TCP_GIT_PORT
 
 from dulwich.tests import (
     get_safe_env,
+    SkipTest,
     TestCase,
     )
 
@@ -53,11 +53,11 @@ def git_version(git_path=_DEFAULT_GIT):
         output = run_git_or_fail(['--version'], git_path=git_path)
     except OSError:
         return None
-    version_prefix = 'git version '
+    version_prefix = b'git version '
     if not output.startswith(version_prefix):
         return None
 
-    parts = output[len(version_prefix):].split('.')
+    parts = output[len(version_prefix):].split(b'.')
     nums = []
     for part in parts:
         try:
@@ -194,6 +194,8 @@ def check_for_daemon(limit=10, delay=0.1, timeout=0.1, port=TCP_GIT_PORT):
         try:
             s.connect(('localhost', port))
             return True
+        except socket.timeout:
+            pass
         except socket.error as e:
             if getattr(e, 'errno', False) and e.errno != errno.ECONNREFUSED:
                 raise

+ 7 - 2
dulwich/tests/test_blackbox.py

@@ -26,8 +26,12 @@ from dulwich.repo import (
 from dulwich.tests import (
     BlackboxTestCase,
     )
+from dulwich.tests.utils import (
+    skipIfPY3,
+    )
 
 
+@skipIfPY3
 class GitReceivePackTests(BlackboxTestCase):
     """Blackbox tests for dul-receive-pack."""
 
@@ -46,11 +50,12 @@ class GitReceivePackTests(BlackboxTestCase):
     def test_missing_arg(self):
         process = self.run_command("dul-receive-pack", [])
         (stdout, stderr) = process.communicate()
-        self.assertEqual('usage: dul-receive-pack <git-dir>\n', stderr)
+        self.assertEqual(['usage: dul-receive-pack <git-dir>'], stderr.splitlines())
         self.assertEqual('', stdout)
         self.assertEqual(1, process.returncode)
 
 
+@skipIfPY3
 class GitUploadPackTests(BlackboxTestCase):
     """Blackbox tests for dul-upload-pack."""
 
@@ -62,6 +67,6 @@ class GitUploadPackTests(BlackboxTestCase):
     def test_missing_arg(self):
         process = self.run_command("dul-upload-pack", [])
         (stdout, stderr) = process.communicate()
-        self.assertEqual('usage: dul-upload-pack <git-dir>\n', stderr)
+        self.assertEqual(['usage: dul-upload-pack <git-dir>'], stderr.splitlines())
         self.assertEqual('', stdout)
         self.assertEqual(1, process.returncode)

+ 19 - 0
dulwich/tests/test_client.py

@@ -17,6 +17,11 @@
 # MA  02110-1301, USA.
 
 from io import BytesIO
+import sys
+try:
+    from unittest import skipIf
+except ImportError:
+    from unittest2 import skipIf
 
 from dulwich import (
     client,
@@ -51,6 +56,7 @@ from dulwich.objects import (
 from dulwich.repo import MemoryRepo
 from dulwich.tests.utils import (
     open_repo,
+    skipIfPY3,
     )
 
 
@@ -67,6 +73,7 @@ class DummyClient(TraditionalGitClient):
 
 
 # TODO(durin42): add unit-level tests of GitClient
+@skipIfPY3
 class GitClientTests(TestCase):
 
     def setUp(self):
@@ -283,6 +290,7 @@ class GitClientTests(TestCase):
         self.assertEqual(self.rout.getvalue(), '0000')
 
 
+@skipIfPY3
 class TestGetTransportAndPath(TestCase):
 
     def test_tcp(self):
@@ -384,6 +392,12 @@ class TestGetTransportAndPath(TestCase):
         self.assertTrue(isinstance(c, SubprocessGitClient))
         self.assertEqual('foo.bar/baz', path)
 
+    @skipIf(sys.platform != 'win32', 'Behaviour only happens on windows.')
+    def test_local_abs_windows_path(self):
+        c, path = get_transport_and_path('C:\\foo.bar\\baz')
+        self.assertTrue(isinstance(c, SubprocessGitClient))
+        self.assertEqual('C:\\foo.bar\\baz', path)
+
     def test_error(self):
         # Need to use a known urlparse.uses_netloc URL scheme to get the
         # expected parsing of the URL on Python versions less than 2.6.5
@@ -397,6 +411,7 @@ class TestGetTransportAndPath(TestCase):
         self.assertEqual('/jelmer/dulwich', path)
 
 
+@skipIfPY3
 class TestGetTransportAndPathFromUrl(TestCase):
 
     def test_tcp(self):
@@ -475,6 +490,7 @@ class TestGetTransportAndPathFromUrl(TestCase):
         self.assertEqual('/home/jelmer/foo', path)
 
 
+@skipIfPY3
 class TestSSHVendor(object):
 
     def __init__(self):
@@ -497,6 +513,7 @@ class TestSSHVendor(object):
         return Subprocess()
 
 
+@skipIfPY3
 class SSHGitClientTests(TestCase):
 
     def setUp(self):
@@ -539,6 +556,7 @@ class SSHGitClientTests(TestCase):
                           server.command)
 
 
+@skipIfPY3
 class ReportStatusParserTests(TestCase):
 
     def test_invalid_pack(self):
@@ -563,6 +581,7 @@ class ReportStatusParserTests(TestCase):
         parser.check()
 
 
+@skipIfPY3
 class LocalGitClientTests(TestCase):
 
     def test_fetch_into_empty(self):

+ 10 - 0
dulwich/tests/test_config.py

@@ -31,8 +31,10 @@ from dulwich.config import (
     _unescape_value,
     )
 from dulwich.tests import TestCase
+from dulwich.tests.utils import skipIfPY3
 
 
+@skipIfPY3
 class ConfigFileTests(TestCase):
 
     def from_file(self, text):
@@ -151,6 +153,7 @@ class ConfigFileTests(TestCase):
         self.assertEqual("bar", cf.get(("branch", "foo"), "foo"))
 
 
+@skipIfPY3
 class ConfigDictTests(TestCase):
 
     def test_get_set(self):
@@ -206,12 +209,14 @@ class ConfigDictTests(TestCase):
 
 
 
+@skipIfPY3
 class StackedConfigTests(TestCase):
 
     def test_default_backends(self):
         StackedConfig.default_backends()
 
 
+@skipIfPY3
 class UnescapeTests(TestCase):
 
     def test_nothing(self):
@@ -227,6 +232,7 @@ class UnescapeTests(TestCase):
         self.assertEqual("\"foo\"", _unescape_value("\\\"foo\\\""))
 
 
+@skipIfPY3
 class EscapeValueTests(TestCase):
 
     def test_nothing(self):
@@ -239,6 +245,7 @@ class EscapeValueTests(TestCase):
         self.assertEqual("foo\\n", _escape_value("foo\n"))
 
 
+@skipIfPY3
 class FormatStringTests(TestCase):
 
     def test_quoted(self):
@@ -250,6 +257,7 @@ class FormatStringTests(TestCase):
         self.assertEqual('foo bar', _format_string("foo bar"))
 
 
+@skipIfPY3
 class ParseStringTests(TestCase):
 
     def test_quoted(self):
@@ -261,6 +269,7 @@ class ParseStringTests(TestCase):
         self.assertEqual('foo bar', _parse_string("foo bar"))
 
 
+@skipIfPY3
 class CheckVariableNameTests(TestCase):
 
     def test_invalid(self):
@@ -274,6 +283,7 @@ class CheckVariableNameTests(TestCase):
         self.assertTrue(_check_variable_name("foo-bar"))
 
 
+@skipIfPY3
 class CheckSectionNameTests(TestCase):
 
     def test_invalid(self):

+ 4 - 0
dulwich/tests/test_diff_tree.py

@@ -57,9 +57,11 @@ from dulwich.tests.utils import (
     make_object,
     functest_builder,
     ext_functest_builder,
+    skipIfPY3,
     )
 
 
+@skipIfPY3
 class DiffTestCase(TestCase):
 
     def setUp(self):
@@ -84,6 +86,7 @@ class DiffTestCase(TestCase):
         return self.store[commit_tree(self.store, commit_blobs)]
 
 
+@skipIfPY3
 class TreeChangesTest(DiffTestCase):
 
     def setUp(self):
@@ -465,6 +468,7 @@ class TreeChangesTest(DiffTestCase):
           [parent1, parent2], merge, rename_detector=self.detector)
 
 
+@skipIfPY3
 class RenameDetectionTest(DiffTestCase):
 
     def _do_test_count_blocks(self, count_blocks):

+ 1 - 1
dulwich/tests/test_fastexport.py

@@ -19,7 +19,6 @@
 
 from io import BytesIO
 import stat
-from unittest import SkipTest
 
 
 from dulwich.object_store import (
@@ -34,6 +33,7 @@ from dulwich.repo import (
     MemoryRepo,
     )
 from dulwich.tests import (
+    SkipTest,
     TestCase,
     )
 from dulwich.tests.utils import (

+ 6 - 1
dulwich/tests/test_file.py

@@ -22,14 +22,18 @@ import os
 import shutil
 import sys
 import tempfile
-from unittest import SkipTest
 
 from dulwich.file import GitFile, fancy_rename
 from dulwich.tests import (
+    SkipTest,
     TestCase,
     )
+from dulwich.tests.utils import (
+    skipIfPY3
+    )
 
 
+@skipIfPY3
 class FancyRenameTests(TestCase):
 
     def setUp(self):
@@ -87,6 +91,7 @@ class FancyRenameTests(TestCase):
         new_f.close()
 
 
+@skipIfPY3
 class GitFileTests(TestCase):
 
     def setUp(self):

+ 6 - 0
dulwich/tests/test_grafts.py

@@ -23,6 +23,7 @@ import shutil
 
 from dulwich.errors import ObjectFormatException
 from dulwich.tests import TestCase
+from dulwich.tests.utils import skipIfPY3
 from dulwich.objects import (
     Tree,
     )
@@ -38,6 +39,7 @@ def makesha(digit):
     return (str(digit) * 40)[:40]
 
 
+@skipIfPY3
 class GraftParserTests(TestCase):
 
     def assertParse(self, expected, graftpoints):
@@ -63,6 +65,7 @@ class GraftParserTests(TestCase):
              ' '.join([makesha(3), makesha(4), makesha(5)])])
 
 
+@skipIfPY3
 class GraftSerializerTests(TestCase):
 
     def assertSerialize(self, expected, graftpoints):
@@ -91,6 +94,7 @@ class GraftSerializerTests(TestCase):
              makesha(3): [makesha(4), makesha(5)]})
 
 
+@skipIfPY3
 class GraftsInRepositoryBase(object):
 
     def tearDown(self):
@@ -135,6 +139,7 @@ class GraftsInRepositoryBase(object):
             {self._shas[-1]: ['1']})
 
 
+@skipIfPY3
 class GraftsInRepoTests(GraftsInRepositoryBase, TestCase):
 
     def setUp(self):
@@ -178,6 +183,7 @@ class GraftsInRepoTests(GraftsInRepositoryBase, TestCase):
         self.assertEqual({self._shas[-1]: [self._shas[0]]}, r._graftpoints)
 
 
+@skipIfPY3
 class GraftsInMemoryRepoTests(GraftsInRepositoryBase, TestCase):
 
     def setUp(self):

+ 4 - 1
dulwich/tests/test_greenthreads.py

@@ -35,7 +35,10 @@ from dulwich.objects import (
     parse_timezone,
     )
 
-from unittest import skipIf
+try:
+    from unittest import skipIf
+except ImportError:
+    from unittest2 import skipIf
 
 try:
     import gevent

+ 8 - 24
dulwich/tests/test_hooks.py

@@ -31,8 +31,10 @@ from dulwich.hooks import (
 )
 
 from dulwich.tests import TestCase
+from dulwich.tests.utils import skipIfPY3
 
 
+@skipIfPY3
 class ShellHookTests(TestCase):
 
     def setUp(self):
@@ -55,20 +57,14 @@ exit 0
         pre_commit = os.path.join(repo_dir, 'hooks', 'pre-commit')
         hook = PreCommitShellHook(repo_dir)
 
-        f = open(pre_commit, 'wb')
-        try:
+        with open(pre_commit, 'wb') as f:
             f.write(pre_commit_fail)
-        finally:
-            f.close()
         os.chmod(pre_commit, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
 
         self.assertRaises(errors.HookError, hook.execute)
 
-        f = open(pre_commit, 'wb')
-        try:
+        with open(pre_commit, 'wb') as f:
             f.write(pre_commit_success)
-        finally:
-            f.close()
         os.chmod(pre_commit, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
 
         hook.execute()
@@ -90,20 +86,14 @@ exit 0
         commit_msg = os.path.join(repo_dir, 'hooks', 'commit-msg')
         hook = CommitMsgShellHook(repo_dir)
 
-        f = open(commit_msg, 'wb')
-        try:
+        with open(commit_msg, 'wb') as f:
             f.write(commit_msg_fail)
-        finally:
-            f.close()
         os.chmod(commit_msg, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
 
         self.assertRaises(errors.HookError, hook.execute, 'failed commit')
 
-        f = open(commit_msg, 'wb')
-        try:
+        with open(commit_msg, 'wb') as f:
             f.write(commit_msg_success)
-        finally:
-            f.close()
         os.chmod(commit_msg, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
 
         hook.execute('empty commit')
@@ -126,20 +116,14 @@ exit 1
         post_commit = os.path.join(repo_dir, 'hooks', 'post-commit')
         hook = PostCommitShellHook(repo_dir)
 
-        f = open(post_commit, 'wb')
-        try:
+        with open(post_commit, 'wb') as f:
             f.write(post_commit_msg_fail)
-        finally:
-            f.close()
         os.chmod(post_commit, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
 
         self.assertRaises(errors.HookError, hook.execute)
 
-        f = open(post_commit, 'wb')
-        try:
+        with open(post_commit, 'wb') as f:
             f.write(post_commit_msg)
-        finally:
-            f.close()
         os.chmod(post_commit, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
 
         hook.execute()

+ 18 - 20
dulwich/tests/test_index.py

@@ -48,8 +48,10 @@ from dulwich.objects import (
     )
 from dulwich.repo import Repo
 from dulwich.tests import TestCase
+from dulwich.tests.utils import skipIfPY3
 
 
+@skipIfPY3
 class IndexTestCase(TestCase):
 
     datadir = os.path.join(os.path.dirname(__file__), 'data/indexes')
@@ -58,6 +60,7 @@ class IndexTestCase(TestCase):
         return Index(os.path.join(self.datadir, name))
 
 
+@skipIfPY3
 class SimpleIndexTestCase(IndexTestCase):
 
     def test_len(self):
@@ -86,6 +89,7 @@ class SimpleIndexTestCase(IndexTestCase):
         self.assertEqual('e69de29bb2d1d6434b8b29ae775ad8c2e48c5391', newsha)
 
 
+@skipIfPY3
 class SimpleIndexWriterTestCase(IndexTestCase):
 
     def setUp(self):
@@ -101,18 +105,14 @@ class SimpleIndexWriterTestCase(IndexTestCase):
                     33188, 1000, 1000, 0,
                     'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391', 0)]
         filename = os.path.join(self.tempdir, 'test-simple-write-index')
-        x = open(filename, 'w+')
-        try:
+        with open(filename, 'w+') as x:
             write_index(x, entries)
-        finally:
-            x.close()
-        x = open(filename, 'r')
-        try:
+
+        with open(filename, 'r') as x:
             self.assertEqual(entries, list(read_index(x)))
-        finally:
-            x.close()
 
 
+@skipIfPY3
 class ReadIndexDictTests(IndexTestCase):
 
     def setUp(self):
@@ -128,18 +128,14 @@ class ReadIndexDictTests(IndexTestCase):
                     33188, 1000, 1000, 0,
                     'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391', 0)}
         filename = os.path.join(self.tempdir, 'test-simple-write-index')
-        x = open(filename, 'w+')
-        try:
+        with open(filename, 'w+') as x:
             write_index_dict(x, entries)
-        finally:
-            x.close()
-        x = open(filename, 'r')
-        try:
+
+        with open(filename, 'r') as x:
             self.assertEqual(entries, read_index_dict(x))
-        finally:
-            x.close()
 
 
+@skipIfPY3
 class CommitTreeTests(TestCase):
 
     def setUp(self):
@@ -171,6 +167,7 @@ class CommitTreeTests(TestCase):
                           set(self.store._data.keys()))
 
 
+@skipIfPY3
 class CleanupModeTests(TestCase):
 
     def test_file(self):
@@ -189,6 +186,7 @@ class CleanupModeTests(TestCase):
         self.assertEqual(0o160000, cleanup_mode(0o160744))
 
 
+@skipIfPY3
 class WriteCacheTimeTests(TestCase):
 
     def test_write_string(self):
@@ -211,6 +209,7 @@ class WriteCacheTimeTests(TestCase):
         self.assertEqual(struct.pack(">LL", 434343, 21), f.getvalue())
 
 
+@skipIfPY3
 class IndexEntryFromStatTests(TestCase):
 
     def test_simple(self):
@@ -249,6 +248,7 @@ class IndexEntryFromStatTests(TestCase):
             0))
 
 
+@skipIfPY3
 class BuildIndexTests(TestCase):
 
     def assertReasonableIndexEntry(self, index_entry, mode, filesize, sha):
@@ -260,11 +260,8 @@ class BuildIndexTests(TestCase):
         if symlink:
             self.assertEqual(os.readlink(path), contents)
         else:
-            f = open(path, 'rb')
-            try:
+            with open(path, 'rb') as f:
                 self.assertEqual(f.read(), contents)
-            finally:
-                f.close()
 
     def test_empty(self):
         repo_dir = tempfile.mkdtemp()
@@ -349,6 +346,7 @@ class BuildIndexTests(TestCase):
             sorted(os.listdir(os.path.join(repo.path, 'c'))))
 
 
+@skipIfPY3
 class GetUnstagedChangesTests(TestCase):
 
     def test_get_unstaged_changes(self):

+ 5 - 0
dulwich/tests/test_lru_cache.py

@@ -22,8 +22,12 @@ from dulwich import (
 from dulwich.tests import (
     TestCase,
     )
+from dulwich.tests.utils import (
+    skipIfPY3,
+    )
 
 
+@skipIfPY3
 class TestLRUCache(TestCase):
     """Test that LRU cache properly keeps track of entries."""
 
@@ -287,6 +291,7 @@ class TestLRUCache(TestCase):
         self.assertEqual([6, 7, 8, 9, 10, 11], sorted(cache.keys()))
 
 
+@skipIfPY3
 class TestLRUSizeCache(TestCase):
 
     def test_basic_init(self):

+ 4 - 0
dulwich/tests/test_missing_obj_finder.py

@@ -26,9 +26,11 @@ from dulwich.tests import TestCase
 from dulwich.tests.utils import (
     make_object,
     build_commit_graph,
+    skipIfPY3,
     )
 
 
+@skipIfPY3
 class MissingObjectFinderTest(TestCase):
 
     def setUp(self):
@@ -49,6 +51,7 @@ class MissingObjectFinderTest(TestCase):
             "some objects are not reported as missing: %s" % (expected, ))
 
 
+@skipIfPY3
 class MOFLinearRepoTest(MissingObjectFinderTest):
 
     def setUp(self):
@@ -108,6 +111,7 @@ class MOFLinearRepoTest(MissingObjectFinderTest):
         self.assertMissingMatch([self.cmt(3).id], [self.cmt(3).id], [])
 
 
+@skipIfPY3
 class MOFMergeForkRepoTest(MissingObjectFinderTest):
     # 1 --- 2 --- 4 --- 6 --- 7
     #          \        /

+ 64 - 35
dulwich/tests/test_object_store.py

@@ -54,6 +54,7 @@ from dulwich.tests import (
 from dulwich.tests.utils import (
     make_object,
     build_pack,
+    skipIfPY3,
     )
 
 
@@ -194,6 +195,7 @@ class ObjectStoreTests(object):
         self.store.close()
 
 
+@skipIfPY3
 class MemoryObjectStoreTests(ObjectStoreTests, TestCase):
 
     def setUp(self):
@@ -227,6 +229,16 @@ class MemoryObjectStoreTests(ObjectStoreTests, TestCase):
                          o.get_raw(packed_blob_sha))
 
 
+    def test_add_thin_pack_empty(self):
+        o = MemoryObjectStore()
+
+        f = BytesIO()
+        entries = build_pack(f, [], store=o)
+        self.assertEquals([], entries)
+        o.add_thin_pack(f.read, None)
+
+
+@skipIfPY3
 class PackBasedObjectStoreTests(ObjectStoreTests):
 
     def tearDown(self):
@@ -247,6 +259,7 @@ class PackBasedObjectStoreTests(ObjectStoreTests):
         self.assertEqual(0, self.store.pack_loose_objects())
 
 
+@skipIfPY3
 class DiskObjectStoreTests(PackBasedObjectStoreTests, TestCase):
 
     def setUp(self):
@@ -312,27 +325,36 @@ class DiskObjectStoreTests(PackBasedObjectStoreTests, TestCase):
 
     def test_add_thin_pack(self):
         o = DiskObjectStore(self.store_dir)
-        blob = make_object(Blob, data='yummy data')
-        o.add_object(blob)
-
-        f = BytesIO()
-        entries = build_pack(f, [
-          (REF_DELTA, (blob.id, 'more yummy data')),
-          ], store=o)
-        pack = o.add_thin_pack(f.read, None)
         try:
-            packed_blob_sha = sha_to_hex(entries[0][3])
-            pack.check_length_and_checksum()
-            self.assertEqual(sorted([blob.id, packed_blob_sha]), list(pack))
-            self.assertTrue(o.contains_packed(packed_blob_sha))
-            self.assertTrue(o.contains_packed(blob.id))
-            self.assertEqual((Blob.type_num, 'more yummy data'),
-                             o.get_raw(packed_blob_sha))
+            blob = make_object(Blob, data='yummy data')
+            o.add_object(blob)
+
+            f = BytesIO()
+            entries = build_pack(f, [
+              (REF_DELTA, (blob.id, 'more yummy data')),
+              ], store=o)
+
+            with o.add_thin_pack(f.read, None) as pack:
+                packed_blob_sha = sha_to_hex(entries[0][3])
+                pack.check_length_and_checksum()
+                self.assertEqual(sorted([blob.id, packed_blob_sha]), list(pack))
+                self.assertTrue(o.contains_packed(packed_blob_sha))
+                self.assertTrue(o.contains_packed(blob.id))
+                self.assertEqual((Blob.type_num, 'more yummy data'),
+                                 o.get_raw(packed_blob_sha))
         finally:
             o.close()
-            pack.close()
+
+    def test_add_thin_pack_empty(self):
+        o = DiskObjectStore(self.store_dir)
+
+        f = BytesIO()
+        entries = build_pack(f, [], store=o)
+        self.assertEquals([], entries)
+        o.add_thin_pack(f.read, None)
 
 
+@skipIfPY3
 class TreeLookupPathTests(TestCase):
 
     def setUp(self):
@@ -376,39 +398,46 @@ class TreeLookupPathTests(TestCase):
 
 # TODO: MissingObjectFinderTests
 
+@skipIfPY3
 class ObjectStoreGraphWalkerTests(TestCase):
 
     def get_walker(self, heads, parent_map):
-        return ObjectStoreGraphWalker(heads,
-            parent_map.__getitem__)
+        new_parent_map = dict([
+            (k * 40, [(p * 40) for p in ps]) for (k, ps) in parent_map.items()])
+        return ObjectStoreGraphWalker([x * 40 for x in heads],
+            new_parent_map.__getitem__)
+
+    def test_ack_invalid_value(self):
+        gw = self.get_walker([], {})
+        self.assertRaises(ValueError, gw.ack, "tooshort")
 
     def test_empty(self):
         gw = self.get_walker([], {})
         self.assertIs(None, next(gw))
-        gw.ack("aa" * 20)
+        gw.ack("a" * 40)
         self.assertIs(None, next(gw))
 
     def test_descends(self):
         gw = self.get_walker(["a"], {"a": ["b"], "b": []})
-        self.assertEqual("a", next(gw))
-        self.assertEqual("b", next(gw))
+        self.assertEqual("a" * 40, next(gw))
+        self.assertEqual("b" * 40, next(gw))
 
     def test_present(self):
         gw = self.get_walker(["a"], {"a": ["b"], "b": []})
-        gw.ack("a")
+        gw.ack("a" * 40)
         self.assertIs(None, next(gw))
 
     def test_parent_present(self):
         gw = self.get_walker(["a"], {"a": ["b"], "b": []})
-        self.assertEqual("a", next(gw))
-        gw.ack("a")
+        self.assertEqual("a" * 40, next(gw))
+        gw.ack("a" * 40)
         self.assertIs(None, next(gw))
 
     def test_child_ack_later(self):
         gw = self.get_walker(["a"], {"a": ["b"], "b": ["c"], "c": []})
-        self.assertEqual("a", next(gw))
-        self.assertEqual("b", next(gw))
-        gw.ack("a")
+        self.assertEqual("a" * 40, next(gw))
+        self.assertEqual("b" * 40, next(gw))
+        gw.ack("a" * 40)
         self.assertIs(None, next(gw))
 
     def test_only_once(self):
@@ -430,18 +459,18 @@ class ObjectStoreGraphWalkerTests(TestCase):
         walk.append(next(gw))
         # A branch (a, c) or (b, d) may be done after 2 steps or 3 depending on
         # the order walked: 3-step walks include (a, b, c) and (b, a, d), etc.
-        if walk == ["a", "c"] or walk == ["b", "d"]:
+        if walk == ["a" * 40, "c" * 40] or walk == ["b" * 40, "d" * 40]:
           gw.ack(walk[0])
           acked = True
 
         walk.append(next(gw))
-        if not acked and walk[2] == "c":
-          gw.ack("a")
-        elif not acked and walk[2] == "d":
-          gw.ack("b")
+        if not acked and walk[2] == "c" * 40:
+          gw.ack("a" * 40)
+        elif not acked and walk[2] == "d" * 40:
+          gw.ack("b" * 40)
         walk.append(next(gw))
         self.assertIs(None, next(gw))
 
-        self.assertEqual(["a", "b", "c", "d"], sorted(walk))
-        self.assertLess(walk.index("a"), walk.index("c"))
-        self.assertLess(walk.index("b"), walk.index("d"))
+        self.assertEqual(["a" * 40, "b" * 40, "c" * 40, "d" * 40], sorted(walk))
+        self.assertLess(walk.index("a" * 40), walk.index("c" * 40))
+        self.assertLess(walk.index("b" * 40), walk.index("d" * 40))

+ 148 - 1
dulwich/tests/test_objects.py

@@ -40,6 +40,7 @@ from dulwich.objects import (
     Commit,
     ShaFile,
     Tag,
+    TreeEntry,
     format_timezone,
     hex_to_sha,
     sha_to_hex,
@@ -47,7 +48,7 @@ from dulwich.objects import (
     check_hexsha,
     check_identity,
     parse_timezone,
-    TreeEntry,
+    object_class,
     parse_tree,
     _parse_tree_py,
     sorted_tree_items,
@@ -61,6 +62,7 @@ from dulwich.tests.utils import (
     make_object,
     functest_builder,
     ext_functest_builder,
+    skipIfPY3,
     )
 
 a_sha = '6f670c0fb53f9463760b7295fbb814e965fb20c8'
@@ -70,6 +72,7 @@ tree_sha = '70c190eb48fa8bbb50ddc692a17b44cb781af7f6'
 tag_sha = '71033db03a03c6a36721efcf1968dd8f8e0cf023'
 
 
+@skipIfPY3
 class TestHexToSha(TestCase):
 
     def test_simple(self):
@@ -79,6 +82,7 @@ class TestHexToSha(TestCase):
         self.assertEqual("abcd" * 10, sha_to_hex("\xab\xcd" * 10))
 
 
+@skipIfPY3
 class BlobReadTests(TestCase):
     """Test decompression of blobs"""
 
@@ -216,6 +220,7 @@ class BlobReadTests(TestCase):
         self.assertNotEqual(sha, c._make_sha())
 
 
+@skipIfPY3
 class ShaFileCheckTests(TestCase):
 
     def assertCheckFails(self, cls, data):
@@ -245,6 +250,7 @@ small_buffer_zlib_object = (
  )
 
 
+@skipIfPY3
 class ShaFileTests(TestCase):
 
     def test_deflated_smaller_window_buffer(self):
@@ -256,6 +262,7 @@ class ShaFileTests(TestCase):
         self.assertEqual(sf.tagger, " <@localhost>")
 
 
+@skipIfPY3
 class CommitSerializationTests(TestCase):
 
     def make_commit(self, **kwargs):
@@ -314,6 +321,52 @@ class CommitSerializationTests(TestCase):
         d._deserialize(c.as_raw_chunks())
         self.assertEqual(c, d)
 
+    def test_serialize_gpgsig(self):
+        commit = self.make_commit(gpgsig="""-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v1
+
+iQIcBAABCgAGBQJULCdfAAoJEACAbyvXKaRXuKwP/RyP9PA49uAvu8tQVCC/uBa8
+vi975+xvO14R8Pp8k2nps7lSxCdtCd+xVT1VRHs0wNhOZo2YCVoU1HATkPejqSeV
+NScTHcxnk4/+bxyfk14xvJkNp7FlQ3npmBkA+lbV0Ubr33rvtIE5jiJPyz+SgWAg
+xdBG2TojV0squj00GoH/euK6aX7GgZtwdtpTv44haCQdSuPGDcI4TORqR6YSqvy3
+GPE+3ZqXPFFb+KILtimkxitdwB7CpwmNse2vE3rONSwTvi8nq3ZoQYNY73CQGkUy
+qoFU0pDtw87U3niFin1ZccDgH0bB6624sLViqrjcbYJeg815Htsu4rmzVaZADEVC
+XhIO4MThebusdk0AcNGjgpf3HRHk0DPMDDlIjm+Oao0cqovvF6VyYmcb0C+RmhJj
+dodLXMNmbqErwTk3zEkW0yZvNIYXH7m9SokPCZa4eeIM7be62X6h1mbt0/IU6Th+
+v18fS0iTMP/Viug5und+05C/v04kgDo0CPphAbXwWMnkE4B6Tl9sdyUYXtvQsL7x
+0+WP1gL27ANqNZiI07Kz/BhbBAQI/+2TFT7oGr0AnFPQ5jHp+3GpUf6OKuT1wT3H
+ND189UFuRuubxb42vZhpcXRbqJVWnbECTKVUPsGZqat3enQUB63uM4i6/RdONDZA
+fDeF1m4qYs+cUXKNUZ03
+=X6RT
+-----END PGP SIGNATURE-----""")
+        self.maxDiff = None
+        self.assertMultiLineEqual("""\
+tree d80c186a03f423a81b39df39dc87fd269736ca86
+parent ab64bbdcc51b170d21588e5c5d391ee5c0c96dfd
+parent 4cffe90e0a41ad3f5190079d7c8f036bde29cbe6
+author James Westby <jw+debian@jameswestby.net> 1174773719 +0000
+committer James Westby <jw+debian@jameswestby.net> 1174773719 +0000
+gpgsig -----BEGIN PGP SIGNATURE-----
+ Version: GnuPG v1
+ 
+ iQIcBAABCgAGBQJULCdfAAoJEACAbyvXKaRXuKwP/RyP9PA49uAvu8tQVCC/uBa8
+ vi975+xvO14R8Pp8k2nps7lSxCdtCd+xVT1VRHs0wNhOZo2YCVoU1HATkPejqSeV
+ NScTHcxnk4/+bxyfk14xvJkNp7FlQ3npmBkA+lbV0Ubr33rvtIE5jiJPyz+SgWAg
+ xdBG2TojV0squj00GoH/euK6aX7GgZtwdtpTv44haCQdSuPGDcI4TORqR6YSqvy3
+ GPE+3ZqXPFFb+KILtimkxitdwB7CpwmNse2vE3rONSwTvi8nq3ZoQYNY73CQGkUy
+ qoFU0pDtw87U3niFin1ZccDgH0bB6624sLViqrjcbYJeg815Htsu4rmzVaZADEVC
+ XhIO4MThebusdk0AcNGjgpf3HRHk0DPMDDlIjm+Oao0cqovvF6VyYmcb0C+RmhJj
+ dodLXMNmbqErwTk3zEkW0yZvNIYXH7m9SokPCZa4eeIM7be62X6h1mbt0/IU6Th+
+ v18fS0iTMP/Viug5und+05C/v04kgDo0CPphAbXwWMnkE4B6Tl9sdyUYXtvQsL7x
+ 0+WP1gL27ANqNZiI07Kz/BhbBAQI/+2TFT7oGr0AnFPQ5jHp+3GpUf6OKuT1wT3H
+ ND189UFuRuubxb42vZhpcXRbqJVWnbECTKVUPsGZqat3enQUB63uM4i6/RdONDZA
+ fDeF1m4qYs+cUXKNUZ03
+ =X6RT
+ -----END PGP SIGNATURE-----
+
+Merge ../b
+""", commit.as_raw_string())
+
     def test_serialize_mergetag(self):
         tag = make_object(
             Tag, object=(Commit, "a38d6181ff27824c79fc7df825164a212eff6a3f"),
@@ -426,6 +479,7 @@ Merge ../b
 
 default_committer = 'James Westby <jw+debian@jameswestby.net> 1174773719 +0000'
 
+@skipIfPY3
 class CommitParseTests(ShaFileCheckTests):
 
     def make_commit_lines(self,
@@ -531,6 +585,50 @@ class CommitParseTests(ShaFileCheckTests):
             else:
                 self.assertCheckFails(Commit, text)
 
+    def test_parse_gpgsig(self):
+        c = Commit.from_string("""tree aaff74984cccd156a469afa7d9ab10e4777beb24
+author Jelmer Vernooij <jelmer@samba.org> 1412179807 +0200
+committer Jelmer Vernooij <jelmer@samba.org> 1412179807 +0200
+gpgsig -----BEGIN PGP SIGNATURE-----
+ Version: GnuPG v1
+ 
+ iQIcBAABCgAGBQJULCdfAAoJEACAbyvXKaRXuKwP/RyP9PA49uAvu8tQVCC/uBa8
+ vi975+xvO14R8Pp8k2nps7lSxCdtCd+xVT1VRHs0wNhOZo2YCVoU1HATkPejqSeV
+ NScTHcxnk4/+bxyfk14xvJkNp7FlQ3npmBkA+lbV0Ubr33rvtIE5jiJPyz+SgWAg
+ xdBG2TojV0squj00GoH/euK6aX7GgZtwdtpTv44haCQdSuPGDcI4TORqR6YSqvy3
+ GPE+3ZqXPFFb+KILtimkxitdwB7CpwmNse2vE3rONSwTvi8nq3ZoQYNY73CQGkUy
+ qoFU0pDtw87U3niFin1ZccDgH0bB6624sLViqrjcbYJeg815Htsu4rmzVaZADEVC
+ XhIO4MThebusdk0AcNGjgpf3HRHk0DPMDDlIjm+Oao0cqovvF6VyYmcb0C+RmhJj
+ dodLXMNmbqErwTk3zEkW0yZvNIYXH7m9SokPCZa4eeIM7be62X6h1mbt0/IU6Th+
+ v18fS0iTMP/Viug5und+05C/v04kgDo0CPphAbXwWMnkE4B6Tl9sdyUYXtvQsL7x
+ 0+WP1gL27ANqNZiI07Kz/BhbBAQI/+2TFT7oGr0AnFPQ5jHp+3GpUf6OKuT1wT3H
+ ND189UFuRuubxb42vZhpcXRbqJVWnbECTKVUPsGZqat3enQUB63uM4i6/RdONDZA
+ fDeF1m4qYs+cUXKNUZ03
+ =X6RT
+ -----END PGP SIGNATURE-----
+
+foo
+""")
+        self.assertEquals("foo\n", c.message)
+        self.assertEquals([], c.extra)
+        self.assertEquals("""-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v1
+
+iQIcBAABCgAGBQJULCdfAAoJEACAbyvXKaRXuKwP/RyP9PA49uAvu8tQVCC/uBa8
+vi975+xvO14R8Pp8k2nps7lSxCdtCd+xVT1VRHs0wNhOZo2YCVoU1HATkPejqSeV
+NScTHcxnk4/+bxyfk14xvJkNp7FlQ3npmBkA+lbV0Ubr33rvtIE5jiJPyz+SgWAg
+xdBG2TojV0squj00GoH/euK6aX7GgZtwdtpTv44haCQdSuPGDcI4TORqR6YSqvy3
+GPE+3ZqXPFFb+KILtimkxitdwB7CpwmNse2vE3rONSwTvi8nq3ZoQYNY73CQGkUy
+qoFU0pDtw87U3niFin1ZccDgH0bB6624sLViqrjcbYJeg815Htsu4rmzVaZADEVC
+XhIO4MThebusdk0AcNGjgpf3HRHk0DPMDDlIjm+Oao0cqovvF6VyYmcb0C+RmhJj
+dodLXMNmbqErwTk3zEkW0yZvNIYXH7m9SokPCZa4eeIM7be62X6h1mbt0/IU6Th+
+v18fS0iTMP/Viug5und+05C/v04kgDo0CPphAbXwWMnkE4B6Tl9sdyUYXtvQsL7x
+0+WP1gL27ANqNZiI07Kz/BhbBAQI/+2TFT7oGr0AnFPQ5jHp+3GpUf6OKuT1wT3H
+ND189UFuRuubxb42vZhpcXRbqJVWnbECTKVUPsGZqat3enQUB63uM4i6/RdONDZA
+fDeF1m4qYs+cUXKNUZ03
+=X6RT
+-----END PGP SIGNATURE-----""", c.gpgsig)
+
 
 _TREE_ITEMS = {
   'a.c': (0o100755, 'd80c186a03f423a81b39df39dc87fd269736ca86'),
@@ -545,6 +643,7 @@ _SORTED_TREE_ITEMS = [
   ]
 
 
+@skipIfPY3
 class TreeTests(ShaFileCheckTests):
 
     def test_add(self):
@@ -691,6 +790,7 @@ class TreeTests(ShaFileCheckTests):
         self.assertEqual(set(["foo"]), set(t))
 
 
+@skipIfPY3
 class TagSerializeTests(TestCase):
 
     def test_serialize_simple(self):
@@ -723,6 +823,7 @@ OK2XeQOiEeXtT76rV4t2WR4=
 """
 
 
+@skipIfPY3
 class TagParseTests(ShaFileCheckTests):
 
     def make_tag_lines(self,
@@ -804,6 +905,7 @@ class TagParseTests(ShaFileCheckTests):
                 self.assertCheckFails(Tag, text)
 
 
+@skipIfPY3
 class CheckTests(TestCase):
 
     def test_check_hexsha(self):
@@ -835,6 +937,7 @@ class CheckTests(TestCase):
                           "trailing characters")
 
 
+@skipIfPY3
 class TimezoneTests(TestCase):
 
     def test_parse_timezone_utc(self):
@@ -878,3 +981,47 @@ class TimezoneTests(TestCase):
             (int(((7 * 60)) * 60), False), parse_timezone("+700"))
         self.assertEqual(
             (int(((7 * 60)) * 60), True), parse_timezone("--700"))
+
+
+@skipIfPY3
+class ShaFileCopyTests(TestCase):
+
+    def assert_copy(self, orig):
+        oclass = object_class(orig.type_num)
+
+        copy = orig.copy()
+        self.assertTrue(isinstance(copy, oclass))
+        self.assertEqual(copy, orig)
+        self.assertTrue(copy is not orig)
+
+    def test_commit_copy(self):
+        attrs = {'tree': 'd80c186a03f423a81b39df39dc87fd269736ca86',
+                 'parents': ['ab64bbdcc51b170d21588e5c5d391ee5c0c96dfd',
+                             '4cffe90e0a41ad3f5190079d7c8f036bde29cbe6'],
+                 'author': 'James Westby <jw+debian@jameswestby.net>',
+                 'committer': 'James Westby <jw+debian@jameswestby.net>',
+                 'commit_time': 1174773719,
+                 'author_time': 1174773719,
+                 'commit_timezone': 0,
+                 'author_timezone': 0,
+                 'message':  'Merge ../b\n'}
+        commit = make_commit(**attrs)
+        self.assert_copy(commit)
+
+    def test_blob_copy(self):
+        blob = make_object(Blob, data="i am a blob")
+        self.assert_copy(blob)
+
+    def test_tree_copy(self):
+        blob = make_object(Blob, data="i am a blob")
+        tree = Tree()
+        tree['blob'] = (stat.S_IFREG, blob.id)
+        self.assert_copy(tree)
+
+    def test_tag_copy(self):
+        tag = make_object(
+            Tag, name='tag', message='',
+            tagger='Tagger <test@example.com>',
+            tag_time=12345, tag_timezone=0,
+            object=(Commit, '0' * 40))
+        self.assert_copy(tag)

+ 3 - 0
dulwich/tests/test_objectspec.py

@@ -35,9 +35,11 @@ from dulwich.tests import (
     )
 from dulwich.tests.utils import (
     build_commit_graph,
+    skipIfPY3,
     )
 
 
+@skipIfPY3
 class ParseObjectTests(TestCase):
     """Test parse_object."""
 
@@ -52,6 +54,7 @@ class ParseObjectTests(TestCase):
         self.assertEqual(b, parse_object(r, b.id))
 
 
+@skipIfPY3
 class ParseCommitRangeTests(TestCase):
     """Test parse_commit_range."""
 

+ 41 - 29
dulwich/tests/test_pack.py

@@ -73,6 +73,7 @@ from dulwich.tests import (
 from dulwich.tests.utils import (
     make_object,
     build_pack,
+    skipIfPY3,
     )
 
 pack1_sha = 'bc63ddad95e7321ee734ea11a7a62d314e0d7481'
@@ -82,6 +83,7 @@ tree_sha = 'b2a2766a2879c209ab1176e7e778b81ae422eeaa'
 commit_sha = 'f18faa16531ac570a3fdc8c7ca16682548dafd12'
 
 
+@skipIfPY3
 class PackTests(TestCase):
     """Base class for testing packs"""
 
@@ -111,6 +113,7 @@ class PackTests(TestCase):
             self.fail(e)
 
 
+@skipIfPY3
 class PackIndexTests(PackTests):
     """Class that tests the index of packfiles"""
 
@@ -151,6 +154,7 @@ class PackIndexTests(PackTests):
         self.assertEqual(set([tree_sha, commit_sha, a_sha]), set(p))
 
 
+@skipIfPY3
 class TestPackDeltas(TestCase):
 
     test_string1 = 'The answer was flailing in the wind'
@@ -168,20 +172,27 @@ class TestPackDeltas(TestCase):
     def test_nochange(self):
         self._test_roundtrip(self.test_string1, self.test_string1)
 
+    def test_nochange_huge(self):
+        self._test_roundtrip(self.test_string_huge, self.test_string_huge)
+
     def test_change(self):
         self._test_roundtrip(self.test_string1, self.test_string2)
 
     def test_rewrite(self):
         self._test_roundtrip(self.test_string1, self.test_string3)
 
-    def test_overflow(self):
+    def test_empty_to_big(self):
         self._test_roundtrip(self.test_string_empty, self.test_string_big)
 
-    def test_overflow_64k(self):
-        self.skipTest("big strings don't work yet")
-        self._test_roundtrip(self.test_string_huge, self.test_string_huge)
+    def test_empty_to_huge(self):
+        self._test_roundtrip(self.test_string_empty, self.test_string_huge)
 
+    def test_huge_copy(self):
+        self._test_roundtrip(self.test_string_huge + self.test_string1,
+                             self.test_string_huge + self.test_string2)
 
+
+@skipIfPY3
 class TestPackData(PackTests):
     """Tests getting the data from the packfile."""
 
@@ -259,7 +270,15 @@ class TestPackData(PackTests):
           sha1('1234').hexdigest(),
           compute_file_sha(f, start_ofs=4, end_ofs=-4).hexdigest())
 
+    def test_compute_file_sha_short_file(self):
+        f = BytesIO('abcd1234wxyz')
+        self.assertRaises(AssertionError, compute_file_sha, f, end_ofs=-20)
+        self.assertRaises(AssertionError, compute_file_sha, f, end_ofs=20)
+        self.assertRaises(AssertionError, compute_file_sha, f, start_ofs=10,
+            end_ofs=-12)
+
 
+@skipIfPY3
 class TestPack(PackTests):
 
     def test_len(self):
@@ -305,15 +324,12 @@ class TestPack(PackTests):
             self.assertEqual(obj.sha().hexdigest(), commit_sha)
 
     def test_copy(self):
-        origpack = self.get_pack(pack1_sha)
-
-        try:
+        with self.get_pack(pack1_sha) as origpack:
             self.assertSucceeds(origpack.index.check)
             basename = os.path.join(self.tempdir, 'Elch')
             write_pack(basename, origpack.pack_tuples())
-            newpack = Pack(basename)
 
-            try:
+            with Pack(basename) as newpack:
                 self.assertEqual(origpack, newpack)
                 self.assertSucceeds(newpack.index.check)
                 self.assertEqual(origpack.name(), newpack.name())
@@ -324,10 +340,6 @@ class TestPack(PackTests):
                 orig_checksum = origpack.index.get_stored_checksum()
                 new_checksum = newpack.index.get_stored_checksum()
                 self.assertTrue(wrong_version or orig_checksum == new_checksum)
-            finally:
-                newpack.close()
-        finally:
-            origpack.close()
 
     def test_commit_obj(self):
         with self.get_pack(pack1_sha) as p:
@@ -351,12 +363,9 @@ class TestPack(PackTests):
         # file should exist
         self.assertTrue(os.path.exists(keepfile_name))
 
-        f = open(keepfile_name, 'r')
-        try:
+        with open(keepfile_name, 'r') as f:
             buf = f.read()
             self.assertEqual('', buf)
-        finally:
-            f.close()
 
     def test_keep_message(self):
         with self.get_pack(pack1_sha) as p:
@@ -370,12 +379,9 @@ class TestPack(PackTests):
         self.assertTrue(os.path.exists(keepfile_name))
 
         # and contain the right message, with a linefeed
-        f = open(keepfile_name, 'r')
-        try:
+        with open(keepfile_name, 'r') as f:
             buf = f.read()
             self.assertEqual(msg + '\n', buf)
-        finally:
-            f.close()
 
     def test_name(self):
         with self.get_pack(pack1_sha) as p:
@@ -420,6 +426,7 @@ class TestPack(PackTests):
             self.assertTrue(isinstance(objs[commit_sha], Commit))
 
 
+@skipIfPY3
 class TestThinPack(PackTests):
 
     def setUp(self):
@@ -437,15 +444,12 @@ class TestThinPack(PackTests):
         self.addCleanup(shutil.rmtree, self.pack_dir)
         self.pack_prefix = os.path.join(self.pack_dir, 'pack')
 
-        f = open(self.pack_prefix + '.pack', 'wb')
-        try:
+        with open(self.pack_prefix + '.pack', 'wb') as f:
             build_pack(f, [
                 (REF_DELTA, (self.blobs['foo'].id, 'foo1234')),
                 (Blob.type_num, 'bar'),
                 (REF_DELTA, (self.blobs['bar'].id, 'bar2468'))],
                 store=self.store)
-        finally:
-            f.close()
 
         # Index the new pack.
         with self.make_pack(True) as pack:
@@ -479,6 +483,7 @@ class TestThinPack(PackTests):
                 sorted(o.id for o in p.iterobjects()))
 
 
+@skipIfPY3
 class WritePackTests(TestCase):
 
     def test_write_pack_header(self):
@@ -595,13 +600,11 @@ class BaseTestFilePackIndexWriting(BaseTestPackIndexWriting):
 
     def writeIndex(self, filename, entries, pack_checksum):
         # FIXME: Write to BytesIO instead rather than hitting disk ?
-        f = GitFile(filename, "wb")
-        try:
+        with GitFile(filename, "wb") as f:
             self._write_fn(f, entries, pack_checksum)
-        finally:
-            f.close()
 
 
+@skipIfPY3
 class TestMemoryIndexWriting(TestCase, BaseTestPackIndexWriting):
 
     def setUp(self):
@@ -616,6 +619,7 @@ class TestMemoryIndexWriting(TestCase, BaseTestPackIndexWriting):
         TestCase.tearDown(self)
 
 
+@skipIfPY3
 class TestPackIndexWritingv1(TestCase, BaseTestFilePackIndexWriting):
 
     def setUp(self):
@@ -631,6 +635,7 @@ class TestPackIndexWritingv1(TestCase, BaseTestFilePackIndexWriting):
         BaseTestFilePackIndexWriting.tearDown(self)
 
 
+@skipIfPY3
 class TestPackIndexWritingv2(TestCase, BaseTestFilePackIndexWriting):
 
     def setUp(self):
@@ -646,6 +651,7 @@ class TestPackIndexWritingv2(TestCase, BaseTestFilePackIndexWriting):
         BaseTestFilePackIndexWriting.tearDown(self)
 
 
+@skipIfPY3
 class ReadZlibTests(TestCase):
 
     decomp = (
@@ -727,6 +733,7 @@ class ReadZlibTests(TestCase):
         self.assertEqual(self.comp, ''.join(self.unpacked.comp_chunks))
 
 
+@skipIfPY3
 class DeltifyTests(TestCase):
 
     def test_empty(self):
@@ -749,6 +756,7 @@ class DeltifyTests(TestCase):
             list(deltify_pack_objects([(b1, ""), (b2, "")])))
 
 
+@skipIfPY3
 class TestPackStreamReader(TestCase):
 
     def test_read_objects_emtpy(self):
@@ -799,6 +807,7 @@ class TestPackStreamReader(TestCase):
         self.assertEqual([], list(reader.read_objects()))
 
 
+@skipIfPY3
 class TestPackIterator(DeltaChainIterator):
 
     _compute_crc32 = True
@@ -820,6 +829,7 @@ class TestPackIterator(DeltaChainIterator):
           offset, pack_type_num, base_chunks)
 
 
+@skipIfPY3
 class DeltaChainIteratorTests(TestCase):
 
     def setUp(self):
@@ -1031,6 +1041,7 @@ class DeltaChainIteratorTests(TestCase):
             self.assertEqual((sorted([b2.id, b3.id]),), (sorted(e.args[0]),))
 
 
+@skipIfPY3
 class DeltaEncodeSizeTests(TestCase):
 
     def test_basic(self):
@@ -1041,6 +1052,7 @@ class DeltaEncodeSizeTests(TestCase):
         self.assertEquals('\xa0\x8d\x06', _delta_encode_size(100000))
 
 
+@skipIfPY3
 class EncodeCopyOperationTests(TestCase):
 
     def test_basic(self):

+ 7 - 1
dulwich/tests/test_patch.py

@@ -19,7 +19,6 @@
 """Tests for patch.py."""
 
 from io import BytesIO
-from unittest import SkipTest
 
 from dulwich.objects import (
     Blob,
@@ -38,10 +37,15 @@ from dulwich.patch import (
     write_tree_diff,
     )
 from dulwich.tests import (
+    SkipTest,
     TestCase,
     )
+from dulwich.tests.utils import (
+    skipIfPY3,
+    )
 
 
+@skipIfPY3
 class WriteCommitPatchTests(TestCase):
 
     def test_simple(self):
@@ -72,6 +76,7 @@ class WriteCommitPatchTests(TestCase):
             self.assertEqual(lines[8], " 0 files changed\n")
 
 
+@skipIfPY3
 class ReadGitAmPatch(TestCase):
 
     def test_extract(self):
@@ -201,6 +206,7 @@ More help   : https://help.launchpad.net/ListHelp
         self.assertEqual(None, version)
 
 
+@skipIfPY3
 class DiffTests(TestCase):
     """Tests for write_blob_diff and write_tree_diff."""
 

+ 161 - 20
dulwich/tests/test_porcelain.py

@@ -39,6 +39,7 @@ from dulwich.tests.compat.utils import require_git_version
 from dulwich.tests.utils import (
     build_commit_graph,
     make_object,
+    skipIfPY3,
     )
 
 
@@ -51,6 +52,7 @@ class PorcelainTestCase(TestCase):
         self.repo = Repo.init(repo_dir)
 
 
+@skipIfPY3
 class ArchiveTests(PorcelainTestCase):
     """Tests for the archive command."""
 
@@ -69,6 +71,7 @@ class ArchiveTests(PorcelainTestCase):
         self.assertEqual([], tf.getnames())
 
 
+@skipIfPY3
 class UpdateServerInfoTests(PorcelainTestCase):
 
     def test_simple(self):
@@ -80,6 +83,7 @@ class UpdateServerInfoTests(PorcelainTestCase):
             'info', 'refs')))
 
 
+@skipIfPY3
 class CommitTests(PorcelainTestCase):
 
     def test_custom_author(self):
@@ -92,6 +96,7 @@ class CommitTests(PorcelainTestCase):
         self.assertEqual(len(sha), 40)
 
 
+@skipIfPY3
 class CloneTests(PorcelainTestCase):
 
     def test_simple_local(self):
@@ -168,6 +173,7 @@ class CloneTests(PorcelainTestCase):
             target_path, checkout=True, bare=True, outstream=outstream)
 
 
+@skipIfPY3
 class InitTests(TestCase):
 
     def test_non_bare(self):
@@ -181,6 +187,7 @@ class InitTests(TestCase):
         porcelain.init(repo_dir, bare=True)
 
 
+@skipIfPY3
 class AddTests(PorcelainTestCase):
 
     def test_add_default_paths(self):
@@ -202,7 +209,7 @@ class AddTests(PorcelainTestCase):
 
         # Check that foo was added and nothing in .git was modified
         index = self.repo.open_index()
-        self.assertEqual(list(index), ['blah', 'foo', 'adir/afile'])
+        self.assertEqual(sorted(index), ['adir/afile', 'blah', 'foo'])
 
     def test_add_file(self):
         with open(os.path.join(self.repo.path, 'foo'), 'w') as f:
@@ -210,18 +217,17 @@ class AddTests(PorcelainTestCase):
         porcelain.add(self.repo.path, paths=["foo"])
 
 
+@skipIfPY3
 class RemoveTests(PorcelainTestCase):
 
     def test_remove_file(self):
-        f = open(os.path.join(self.repo.path, 'foo'), 'w')
-        try:
+        with open(os.path.join(self.repo.path, 'foo'), 'w') as f:
             f.write("BAR")
-        finally:
-            f.close()
         porcelain.add(self.repo.path, paths=["foo"])
         porcelain.rm(self.repo.path, paths=["foo"])
 
 
+@skipIfPY3
 class LogTests(PorcelainTestCase):
 
     def test_simple(self):
@@ -241,6 +247,7 @@ class LogTests(PorcelainTestCase):
         self.assertEqual(1, outstream.getvalue().count("-" * 50))
 
 
+@skipIfPY3
 class ShowTests(PorcelainTestCase):
 
     def test_nolist(self):
@@ -267,6 +274,7 @@ class ShowTests(PorcelainTestCase):
         self.assertEqual(outstream.getvalue(), "The Foo\n")
 
 
+@skipIfPY3
 class SymbolicRefTests(PorcelainTestCase):
 
     def test_set_wrong_symbolic_ref(self):
@@ -309,6 +317,7 @@ class SymbolicRefTests(PorcelainTestCase):
         self.assertEqual(new_ref, b'ref: refs/heads/develop\n')
 
 
+@skipIfPY3
 class DiffTreeTests(PorcelainTestCase):
 
     def test_empty(self):
@@ -320,6 +329,7 @@ class DiffTreeTests(PorcelainTestCase):
         self.assertEqual(outstream.getvalue(), "")
 
 
+@skipIfPY3
 class CommitTreeTests(PorcelainTestCase):
 
     def test_simple(self):
@@ -339,6 +349,7 @@ class CommitTreeTests(PorcelainTestCase):
         self.assertEqual(len(sha), 40)
 
 
+@skipIfPY3
 class RevListTests(PorcelainTestCase):
 
     def test_simple(self):
@@ -352,15 +363,16 @@ class RevListTests(PorcelainTestCase):
             outstream.getvalue())
 
 
-class TagTests(PorcelainTestCase):
+@skipIfPY3
+class TagCreateTests(PorcelainTestCase):
 
     def test_annotated(self):
         c1, c2, c3 = build_commit_graph(self.repo.object_store, [[1], [2, 1],
             [3, 1, 2]])
         self.repo.refs["HEAD"] = c3.id
 
-        porcelain.tag(self.repo.path, "tryme", 'foo <foo@bar.com>', 'bar',
-                annotated=True)
+        porcelain.tag_create(self.repo.path, "tryme", 'foo <foo@bar.com>',
+                'bar', annotated=True)
 
         tags = self.repo.refs.as_dict("refs/tags")
         self.assertEqual(tags.keys(), ["tryme"])
@@ -374,7 +386,7 @@ class TagTests(PorcelainTestCase):
             [3, 1, 2]])
         self.repo.refs["HEAD"] = c3.id
 
-        porcelain.tag(self.repo.path, "tryme", annotated=False)
+        porcelain.tag_create(self.repo.path, "tryme", annotated=False)
 
         tags = self.repo.refs.as_dict("refs/tags")
         self.assertEqual(tags.keys(), ["tryme"])
@@ -382,38 +394,46 @@ class TagTests(PorcelainTestCase):
         self.assertEqual(tags.values(), [self.repo.head()])
 
 
-class ListTagsTests(PorcelainTestCase):
+@skipIfPY3
+class TagListTests(PorcelainTestCase):
 
     def test_empty(self):
-        tags = porcelain.list_tags(self.repo.path)
+        tags = porcelain.tag_list(self.repo.path)
         self.assertEqual([], tags)
 
     def test_simple(self):
         self.repo.refs["refs/tags/foo"] = "aa" * 20
         self.repo.refs["refs/tags/bar/bla"] = "bb" * 20
-        tags = porcelain.list_tags(self.repo.path)
+        tags = porcelain.tag_list(self.repo.path)
 
         self.assertEqual(["bar/bla", "foo"], tags)
 
 
+@skipIfPY3
+class TagDeleteTests(PorcelainTestCase):
+
+    def test_simple(self):
+        [c1] = build_commit_graph(self.repo.object_store, [[1]])
+        self.repo["HEAD"] = c1.id
+        porcelain.tag_create(self.repo, 'foo')
+        self.assertTrue("foo" in porcelain.tag_list(self.repo))
+        porcelain.tag_delete(self.repo, 'foo')
+        self.assertFalse("foo" in porcelain.tag_list(self.repo))
+
+
+@skipIfPY3
 class ResetTests(PorcelainTestCase):
 
     def test_hard_head(self):
-        f = open(os.path.join(self.repo.path, 'foo'), 'w')
-        try:
+        with open(os.path.join(self.repo.path, 'foo'), 'w') as f:
             f.write("BAR")
-        finally:
-            f.close()
         porcelain.add(self.repo.path, paths=["foo"])
         porcelain.commit(self.repo.path, message="Some message",
                 committer="Jane <jane@example.com>",
                 author="John <john@example.com>")
 
-        f = open(os.path.join(self.repo.path, 'foo'), 'w')
-        try:
+        with open(os.path.join(self.repo.path, 'foo'), 'w') as f:
             f.write("OOH")
-        finally:
-            f.close()
 
         porcelain.reset(self.repo, "hard", "HEAD")
 
@@ -425,6 +445,7 @@ class ResetTests(PorcelainTestCase):
         self.assertEqual([], changes)
 
 
+@skipIfPY3
 class PushTests(PorcelainTestCase):
 
     def test_simple(self):
@@ -469,6 +490,7 @@ class PushTests(PorcelainTestCase):
         self.assertEqual(os.path.basename(fullpath), change.new.path)
 
 
+@skipIfPY3
 class PullTests(PorcelainTestCase):
 
     def test_simple(self):
@@ -502,6 +524,7 @@ class PullTests(PorcelainTestCase):
         self.assertEqual(r['HEAD'].id, self.repo['HEAD'].id)
 
 
+@skipIfPY3
 class StatusTests(PorcelainTestCase):
 
     def test_status(self):
@@ -597,3 +620,121 @@ class StatusTests(PorcelainTestCase):
 
 
 # TODO(jelmer): Add test for dulwich.porcelain.daemon
+
+
+@skipIfPY3
+class UploadPackTests(PorcelainTestCase):
+    """Tests for upload_pack."""
+
+    def test_upload_pack(self):
+        outf = BytesIO()
+        exitcode = porcelain.upload_pack(self.repo.path, BytesIO("0000"), outf)
+        outlines = outf.getvalue().splitlines()
+        self.assertEqual(["0000"], outlines)
+        self.assertEqual(0, exitcode)
+
+
+@skipIfPY3
+class ReceivePackTests(PorcelainTestCase):
+    """Tests for receive_pack."""
+
+    def test_receive_pack(self):
+        filename = 'foo'
+        with open(os.path.join(self.repo.path, filename), 'w') as f:
+            f.write('stuff')
+        porcelain.add(repo=self.repo.path, paths=filename)
+        self.repo.do_commit(message='test status',
+            author='', committer='', author_timestamp=1402354300,
+            commit_timestamp=1402354300, author_timezone=0, commit_timezone=0)
+        outf = BytesIO()
+        exitcode = porcelain.receive_pack(self.repo.path, BytesIO("0000"), outf)
+        outlines = outf.getvalue().splitlines()
+        self.assertEqual([
+            '005a9e65bdcf4a22cdd4f3700604a275cd2aaf146b23 HEAD\x00report-status '
+            'delete-refs side-band-64k',
+            '003f9e65bdcf4a22cdd4f3700604a275cd2aaf146b23 refs/heads/master',
+            '0000'], outlines)
+        self.assertEqual(0, exitcode)
+
+
+@skipIfPY3
+class BranchListTests(PorcelainTestCase):
+
+    def test_standard(self):
+        self.assertEquals(set([]), set(porcelain.branch_list(self.repo)))
+
+    def test_new_branch(self):
+        [c1] = build_commit_graph(self.repo.object_store, [[1]])
+        self.repo["HEAD"] = c1.id
+        porcelain.branch_create(self.repo, "foo")
+        self.assertEquals(
+            set(["master", "foo"]),
+            set(porcelain.branch_list(self.repo)))
+
+
+@skipIfPY3
+class BranchCreateTests(PorcelainTestCase):
+
+    def test_branch_exists(self):
+        [c1] = build_commit_graph(self.repo.object_store, [[1]])
+        self.repo["HEAD"] = c1.id
+        porcelain.branch_create(self.repo, "foo")
+        self.assertRaises(KeyError, porcelain.branch_create, self.repo, "foo")
+        porcelain.branch_create(self.repo, "foo", force=True)
+
+    def test_new_branch(self):
+        [c1] = build_commit_graph(self.repo.object_store, [[1]])
+        self.repo["HEAD"] = c1.id
+        porcelain.branch_create(self.repo, "foo")
+        self.assertEquals(
+            set(["master", "foo"]),
+            set(porcelain.branch_list(self.repo)))
+
+
+@skipIfPY3
+class BranchDeleteTests(PorcelainTestCase):
+
+    def test_simple(self):
+        [c1] = build_commit_graph(self.repo.object_store, [[1]])
+        self.repo["HEAD"] = c1.id
+        porcelain.branch_create(self.repo, 'foo')
+        self.assertTrue("foo" in porcelain.branch_list(self.repo))
+        porcelain.branch_delete(self.repo, 'foo')
+        self.assertFalse("foo" in porcelain.branch_list(self.repo))
+
+
+@skipIfPY3
+class FetchTests(PorcelainTestCase):
+
+    def test_simple(self):
+        outstream = BytesIO()
+        errstream = BytesIO()
+
+        # create a file for initial commit
+        handle, fullpath = tempfile.mkstemp(dir=self.repo.path)
+        filename = os.path.basename(fullpath)
+        porcelain.add(repo=self.repo.path, paths=filename)
+        porcelain.commit(repo=self.repo.path, message='test',
+                         author='test', committer='test')
+
+        # Setup target repo
+        target_path = tempfile.mkdtemp()
+        target_repo = porcelain.clone(self.repo.path, target=target_path,
+            outstream=outstream)
+
+        # create a second file to be pushed
+        handle, fullpath = tempfile.mkstemp(dir=self.repo.path)
+        filename = os.path.basename(fullpath)
+        porcelain.add(repo=self.repo.path, paths=filename)
+        porcelain.commit(repo=self.repo.path, message='test2',
+            author='test2', committer='test2')
+
+        self.assertFalse(self.repo['HEAD'].id in target_repo)
+
+        # Fetch changes into the cloned repo
+        porcelain.fetch(target_path, self.repo.path, outstream=outstream,
+            errstream=errstream)
+
+        # Check the target repo for pushed changes
+        r = Repo(target_path)
+        self.assertTrue(self.repo['HEAD'].id in r)

+ 6 - 0
dulwich/tests/test_protocol.py

@@ -37,6 +37,7 @@ from dulwich.protocol import (
     BufferedPktLineWriter,
     )
 from dulwich.tests import TestCase
+from dulwich.tests.utils import skipIfPY3
 
 
 class BaseProtocolTests(object):
@@ -106,6 +107,7 @@ class BaseProtocolTests(object):
         self.assertRaises(AssertionError, self.proto.read_cmd)
 
 
+@skipIfPY3
 class ProtocolTests(BaseProtocolTests, TestCase):
 
     def setUp(self):
@@ -133,6 +135,7 @@ class ReceivableBytesIO(BytesIO):
         return self.read(size - 1)
 
 
+@skipIfPY3
 class ReceivableProtocolTests(BaseProtocolTests, TestCase):
 
     def setUp(self):
@@ -204,6 +207,7 @@ class ReceivableProtocolTests(BaseProtocolTests, TestCase):
         self.assertEqual(all_data, data)
 
 
+@skipIfPY3
 class CapabilitiesTestCase(TestCase):
 
     def test_plain(self):
@@ -233,6 +237,7 @@ class CapabilitiesTestCase(TestCase):
                                     'multi_ack_detailed']))
 
 
+@skipIfPY3
 class BufferedPktLineWriterTests(TestCase):
 
     def setUp(self):
@@ -288,6 +293,7 @@ class BufferedPktLineWriterTests(TestCase):
         self.assertOutputEquals('0005z')
 
 
+@skipIfPY3
 class PktLineParserTests(TestCase):
 
     def test_none(self):

+ 5 - 1
dulwich/tests/test_refs.py

@@ -45,6 +45,7 @@ from dulwich.tests import (
 from dulwich.tests.utils import (
     open_repo,
     tear_down_repo,
+    skipIfPY3,
     )
 
 
@@ -79,6 +80,7 @@ TWOS = "2" * 40
 THREES = "3" * 40
 FOURS = "4" * 40
 
+@skipIfPY3
 class PackedRefsFileTests(TestCase):
 
     def test_split_ref_line_errors(self):
@@ -238,7 +240,7 @@ class RefsContainerTests(object):
 
 
 
-
+@skipIfPY3
 class DictRefsContainerTests(RefsContainerTests, TestCase):
 
     def setUp(self):
@@ -254,6 +256,7 @@ class DictRefsContainerTests(RefsContainerTests, TestCase):
         self.assertEqual(expected_refs, self._refs.as_dict())
 
 
+@skipIfPY3
 class DiskRefsContainerTests(RefsContainerTests, TestCase):
 
     def setUp(self):
@@ -425,6 +428,7 @@ _TEST_REFS_SERIALIZED = (
 '3ec9c43c84ff242e3ef4a9fc5bc111fd780a76a8\trefs/tags/refs-0.2\n')
 
 
+@skipIfPY3
 class InfoRefsContainerTests(TestCase):
 
     def test_invalid_refname(self):

+ 14 - 39
dulwich/tests/test_repository.py

@@ -42,11 +42,13 @@ from dulwich.tests.utils import (
     open_repo,
     tear_down_repo,
     setup_warning_catcher,
+    skipIfPY3,
     )
 
 missing_sha = 'b91fa4d900e17e99b433218e988c4eb4a3e9a097'
 
 
+@skipIfPY3
 class CreateRepositoryTests(TestCase):
 
     def assertFileContentsEqual(self, expected, repo, path):
@@ -54,10 +56,8 @@ class CreateRepositoryTests(TestCase):
         if not f:
             self.assertEqual(expected, None)
         else:
-            try:
+            with f:
                 self.assertEqual(expected, f.read())
-            finally:
-                f.close()
 
     def _check_repo_contents(self, repo, expect_bare):
         self.assertEqual(expect_bare, repo.bare)
@@ -87,6 +87,7 @@ class CreateRepositoryTests(TestCase):
         self._check_repo_contents(repo, True)
 
 
+@skipIfPY3
 class RepositoryTests(TestCase):
 
     def setUp(self):
@@ -173,11 +174,8 @@ class RepositoryTests(TestCase):
 
     def test_get_description(self):
         r = self._repo = open_repo('a.git')
-        f = open(os.path.join(r.path, 'description'), 'w')
-        try:
+        with open(os.path.join(r.path, 'description'), 'w') as f:
             f.write("Some description")
-        finally:
-            f.close()
         self.assertEqual("Some description", r.get_description())
 
     def test_set_description(self):
@@ -376,11 +374,8 @@ exit 0
 
         pre_commit = os.path.join(r.controldir(), 'hooks', 'pre-commit')
 
-        f = open(pre_commit, 'wb')
-        try:
+        with open(pre_commit, 'wb') as f:
             f.write(pre_commit_fail)
-        finally:
-            f.close()
         os.chmod(pre_commit, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
 
         self.assertRaises(errors.CommitError, r.do_commit, 'failed commit',
@@ -389,11 +384,8 @@ exit 0
                           commit_timestamp=12345, commit_timezone=0,
                           author_timestamp=12345, author_timezone=0)
 
-        f = open(pre_commit, 'wb')
-        try:
+        with open(pre_commit, 'wb') as f:
             f.write(pre_commit_success)
-        finally:
-            f.close()
         os.chmod(pre_commit, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
 
         commit_sha = r.do_commit(
@@ -422,11 +414,8 @@ exit 0
 
         commit_msg = os.path.join(r.controldir(), 'hooks', 'commit-msg')
 
-        f = open(commit_msg, 'wb')
-        try:
+        with open(commit_msg, 'wb') as f:
             f.write(commit_msg_fail)
-        finally:
-            f.close()
         os.chmod(commit_msg, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
 
         self.assertRaises(errors.CommitError, r.do_commit, 'failed commit',
@@ -435,11 +424,8 @@ exit 0
                           commit_timestamp=12345, commit_timezone=0,
                           author_timestamp=12345, author_timezone=0)
 
-        f = open(commit_msg, 'wb')
-        try:
+        with open(commit_msg, 'wb') as f:
             f.write(commit_msg_success)
-        finally:
-            f.close()
         os.chmod(commit_msg, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
 
         commit_sha = r.do_commit(
@@ -473,11 +459,8 @@ rm %(file)s
 
         post_commit = os.path.join(r.controldir(), 'hooks', 'post-commit')
 
-        f = open(post_commit, 'wb')
-        try:
+        with open(post_commit, 'wb') as f:
             f.write(post_commit_msg)
-        finally:
-            f.close()
         os.chmod(post_commit, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
 
         commit_sha = r.do_commit(
@@ -493,11 +476,8 @@ rm %(file)s
         post_commit_msg_fail = """#!/bin/sh
 exit 1
 """
-        f = open(post_commit, 'wb')
-        try:
+        with open(post_commit, 'wb') as f:
             f.write(post_commit_msg_fail)
-        finally:
-            f.close()
         os.chmod(post_commit, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
 
         warnings.simplefilter("always", UserWarning)
@@ -517,6 +497,7 @@ exit 1
         self.assertEqual([commit_sha], r[commit_sha2].parents)
 
 
+@skipIfPY3
 class BuildRepoTests(TestCase):
     """Tests that build on-disk repos from scratch.
 
@@ -533,11 +514,8 @@ class BuildRepoTests(TestCase):
         self.assertEqual('ref: refs/heads/master', r.refs.read_ref('HEAD'))
         self.assertRaises(KeyError, lambda: r.refs['refs/heads/master'])
 
-        f = open(os.path.join(r.path, 'a'), 'wb')
-        try:
+        with open(os.path.join(r.path, 'a'), 'wb') as f:
             f.write('file contents')
-        finally:
-            f.close()
         r.stage(['a'])
         commit_sha = r.do_commit('msg',
                                  committer='Test Committer <test@nodomain.com>',
@@ -562,11 +540,8 @@ class BuildRepoTests(TestCase):
 
     def test_commit_modified(self):
         r = self._repo
-        f = open(os.path.join(r.path, 'a'), 'wb')
-        try:
+        with open(os.path.join(r.path, 'a'), 'wb') as f:
             f.write('new contents')
-        finally:
-            f.close()
         os.symlink('a', os.path.join(self._repo_dir, 'b'))
         r.stage(['a', 'b'])
         commit_sha = r.do_commit('modified a',

+ 35 - 12
dulwich/tests/test_server.py

@@ -59,6 +59,7 @@ from dulwich.tests import TestCase
 from dulwich.tests.utils import (
     make_commit,
     make_object,
+    skipIfPY3,
     )
 from dulwich.protocol import (
     ZERO_SHA,
@@ -159,6 +160,7 @@ class HandlerTestCase(TestCase):
         self.assertFalse(self._handler.has_capability('capxxx'))
 
 
+@skipIfPY3
 class UploadPackHandlerTestCase(TestCase):
 
     def setUp(self):
@@ -212,6 +214,7 @@ class UploadPackHandlerTestCase(TestCase):
         self.assertEqual({}, self._handler.get_tagged(refs, repo=self._repo))
 
 
+@skipIfPY3
 class FindShallowTests(TestCase):
 
     def setUp(self):
@@ -292,6 +295,7 @@ class TestUploadPackHandler(UploadPackHandler):
     def required_capabilities(self):
         return ()
 
+@skipIfPY3
 class ReceivePackHandlerTestCase(TestCase):
 
     def setUp(self):
@@ -314,6 +318,7 @@ class ReceivePackHandlerTestCase(TestCase):
         self.assertEqual(status[1][1], 'ok')
 
 
+@skipIfPY3
 class ProtocolGraphWalkerEmptyTestCase(TestCase):
     def setUp(self):
         super(ProtocolGraphWalkerEmptyTestCase, self).setUp()
@@ -335,6 +340,7 @@ class ProtocolGraphWalkerEmptyTestCase(TestCase):
 
 
 
+@skipIfPY3
 class ProtocolGraphWalkerTestCase(TestCase):
 
     def setUp(self):
@@ -356,20 +362,28 @@ class ProtocolGraphWalkerTestCase(TestCase):
             TestUploadPackHandler(backend, ['/', 'host=lolcats'], TestProto()),
             self._repo.object_store, self._repo.get_peeled)
 
-    def test_is_satisfied_no_haves(self):
-        self.assertFalse(self._walker._is_satisfied([], ONE, 0))
-        self.assertFalse(self._walker._is_satisfied([], TWO, 0))
-        self.assertFalse(self._walker._is_satisfied([], THREE, 0))
+    def test_all_wants_satisfied_no_haves(self):
+        self._walker.set_wants([ONE])
+        self.assertFalse(self._walker.all_wants_satisfied([]))
+        self._walker.set_wants([TWO])
+        self.assertFalse(self._walker.all_wants_satisfied([]))
+        self._walker.set_wants([THREE])
+        self.assertFalse(self._walker.all_wants_satisfied([]))
 
-    def test_is_satisfied_have_root(self):
-        self.assertTrue(self._walker._is_satisfied([ONE], ONE, 0))
-        self.assertTrue(self._walker._is_satisfied([ONE], TWO, 0))
-        self.assertTrue(self._walker._is_satisfied([ONE], THREE, 0))
+    def test_all_wants_satisfied_have_root(self):
+        self._walker.set_wants([ONE])
+        self.assertTrue(self._walker.all_wants_satisfied([ONE]))
+        self._walker.set_wants([TWO])
+        self.assertTrue(self._walker.all_wants_satisfied([ONE]))
+        self._walker.set_wants([THREE])
+        self.assertTrue(self._walker.all_wants_satisfied([ONE]))
 
-    def test_is_satisfied_have_branch(self):
-        self.assertTrue(self._walker._is_satisfied([TWO], TWO, 0))
+    def test_all_wants_satisfied_have_branch(self):
+        self._walker.set_wants([TWO])
+        self.assertTrue(self._walker.all_wants_satisfied([TWO]))
         # wrong branch
-        self.assertFalse(self._walker._is_satisfied([TWO], THREE, 0))
+        self._walker.set_wants([THREE])
+        self.assertFalse(self._walker.all_wants_satisfied([TWO]))
 
     def test_all_wants_satisfied(self):
         self._walker.set_wants([FOUR, FIVE])
@@ -507,6 +521,7 @@ class ProtocolGraphWalkerTestCase(TestCase):
           ])
 
 
+@skipIfPY3
 class TestProtocolGraphWalker(object):
 
     def __init__(self):
@@ -537,6 +552,7 @@ class TestProtocolGraphWalker(object):
         return self.acks.pop(0)
 
 
+@skipIfPY3
 class AckGraphWalkerImplTestCase(TestCase):
     """Base setup and asserts for AckGraphWalker tests."""
 
@@ -569,6 +585,7 @@ class AckGraphWalkerImplTestCase(TestCase):
         self.assertEqual(sha, next(self._impl))
 
 
+@skipIfPY3
 class SingleAckGraphWalkerImplTestCase(AckGraphWalkerImplTestCase):
 
     impl_cls = SingleAckGraphWalkerImpl
@@ -637,6 +654,7 @@ class SingleAckGraphWalkerImplTestCase(AckGraphWalkerImplTestCase):
         self.assertNak()
 
 
+@skipIfPY3
 class MultiAckGraphWalkerImplTestCase(AckGraphWalkerImplTestCase):
 
     impl_cls = MultiAckGraphWalkerImpl
@@ -711,6 +729,7 @@ class MultiAckGraphWalkerImplTestCase(AckGraphWalkerImplTestCase):
         self.assertNak()
 
 
+@skipIfPY3
 class MultiAckDetailedGraphWalkerImplTestCase(AckGraphWalkerImplTestCase):
 
     impl_cls = MultiAckDetailedGraphWalkerImpl
@@ -824,6 +843,7 @@ class MultiAckDetailedGraphWalkerImplTestCase(AckGraphWalkerImplTestCase):
         self.assertNak()
 
 
+@skipIfPY3
 class FileSystemBackendTests(TestCase):
     """Tests for FileSystemBackend."""
 
@@ -839,7 +859,7 @@ class FileSystemBackendTests(TestCase):
 
     def test_absolute(self):
         repo = self.backend.open_repository(self.path)
-        self.assertEqual(repo.path, self.repo.path)
+        self.assertEqual(os.path.abspath(repo.path), os.path.abspath(self.repo.path))
 
     def test_child(self):
         self.assertRaises(NotGitRepository,
@@ -852,6 +872,7 @@ class FileSystemBackendTests(TestCase):
                           lambda: backend.open_repository('/ups'))
 
 
+@skipIfPY3
 class DictBackendTests(TestCase):
     """Tests for DictBackend."""
 
@@ -869,6 +890,7 @@ class DictBackendTests(TestCase):
                           lambda: backend.open_repository('/ups'))
 
 
+@skipIfPY3
 class ServeCommandTests(TestCase):
     """Tests for serve_command."""
 
@@ -894,6 +916,7 @@ class ServeCommandTests(TestCase):
         self.assertEqual(0, exitcode)
 
 
+@skipIfPY3
 class UpdateServerInfoTests(TestCase):
     """Tests for update_server_info."""
 

+ 2 - 0
dulwich/tests/test_walk.py

@@ -49,6 +49,7 @@ from dulwich.tests.utils import (
     F,
     make_object,
     build_commit_graph,
+    skipIfPY3,
     )
 
 
@@ -70,6 +71,7 @@ class TestWalkEntry(object):
         return self.changes == other.changes()
 
 
+@skipIfPY3
 class WalkerTest(TestCase):
 
     def setUp(self):

+ 5 - 0
dulwich/tests/test_web.py

@@ -61,6 +61,7 @@ from dulwich.web import (
 
 from dulwich.tests.utils import (
     make_object,
+    skipIfPY3,
     )
 
 
@@ -115,6 +116,7 @@ def _test_backend(objects, refs=None, named_files=None):
     return DictBackend({'/': repo})
 
 
+@skipIfPY3
 class DumbHandlersTestCase(WebTestCase):
 
     def test_send_file_not_found(self):
@@ -284,6 +286,7 @@ class DumbHandlersTestCase(WebTestCase):
         self.assertFalse(self._req.cached)
 
 
+@skipIfPY3
 class SmartHandlersTestCase(WebTestCase):
 
     class _TestUploadPackHandler(object):
@@ -361,6 +364,7 @@ class SmartHandlersTestCase(WebTestCase):
         self.assertFalse(self._req.cached)
 
 
+@skipIfPY3
 class LengthLimitedFileTestCase(TestCase):
     def test_no_cutoff(self):
         f = _LengthLimitedFile(BytesIO('foobar'), 1024)
@@ -419,6 +423,7 @@ class HTTPGitRequestTestCase(WebTestCase):
         self.assertEqual(402, self._status)
 
 
+@skipIfPY3
 class HTTPGitApplicationTestCase(TestCase):
 
     def setUp(self):

+ 10 - 4
dulwich/tests/utils.py

@@ -23,12 +23,11 @@
 import datetime
 import os
 import shutil
+import sys
 import tempfile
 import time
 import types
-from unittest import (
-    SkipTest,
-    )
+
 import warnings
 
 from dulwich.index import (
@@ -49,6 +48,11 @@ from dulwich.pack import (
     create_delta,
     )
 from dulwich.repo import Repo
+from dulwich.tests import (
+    SkipTest,
+    skipIf,
+    )
+
 
 # Plain files are very frequently used in tests, so let the mode be very short.
 F = 0o100644  # Shorthand mode for Files.
@@ -98,7 +102,7 @@ def make_object(cls, **attrs):
         pass
 
     obj = TestObject()
-    for name, value in attrs.iteritems():
+    for name, value in attrs.items():
         if name == 'id':
             # id property is read-only, so we overwrite sha instead.
             sha = FixedSha(value)
@@ -328,3 +332,5 @@ 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.")

+ 74 - 69
dulwich/web.py

@@ -27,7 +27,18 @@ import os
 import re
 import sys
 import time
-from urlparse import parse_qs
+from wsgiref.simple_server import (
+    WSGIRequestHandler,
+    ServerHandler,
+    WSGIServer,
+    make_server,
+    )
+
+try:
+    from urlparse import parse_qs
+except ImportError:
+    from urllib.parse import parse_qs
+
 
 from dulwich import log_utils
 from dulwich.protocol import (
@@ -408,89 +419,83 @@ def make_wsgi_chain(*args, **kwargs):
     return wrapped_app
 
 
-# The reference server implementation is based on wsgiref, which is not
-# distributed with python 2.4. If wsgiref is not present, users will not be
-# able to use the HTTP server without a little extra work.
-try:
-    from wsgiref.simple_server import (
-        WSGIRequestHandler,
-        ServerHandler,
-        WSGIServer,
-        make_server,
-    )
-    class ServerHandlerLogger(ServerHandler):
-        """ServerHandler that uses dulwich's logger for logging exceptions."""
+class ServerHandlerLogger(ServerHandler):
+    """ServerHandler that uses dulwich's logger for logging exceptions."""
 
-        def log_exception(self, exc_info):
+    def log_exception(self, exc_info):
+        if sys.version < (2, 7):
+            logger.exception('Exception happened during processing of request')
+        else:
             logger.exception('Exception happened during processing of request',
                              exc_info=exc_info)
 
-        def log_message(self, format, *args):
-            logger.info(format, *args)
+    def log_message(self, format, *args):
+        logger.info(format, *args)
 
-        def log_error(self, *args):
-            logger.error(*args)
+    def log_error(self, *args):
+        logger.error(*args)
 
-    class WSGIRequestHandlerLogger(WSGIRequestHandler):
-        """WSGIRequestHandler that uses dulwich's logger for logging exceptions."""
 
-        def log_exception(self, exc_info):
-            logger.exception('Exception happened during processing of request',
-                             exc_info=exc_info)
+class WSGIRequestHandlerLogger(WSGIRequestHandler):
+    """WSGIRequestHandler that uses dulwich's logger for logging exceptions."""
 
-        def log_message(self, format, *args):
-            logger.info(format, *args)
+    def log_exception(self, exc_info):
+        logger.exception('Exception happened during processing of request',
+                         exc_info=exc_info)
 
-        def log_error(self, *args):
-            logger.error(*args)
+    def log_message(self, format, *args):
+        logger.info(format, *args)
 
-        def handle(self):
-            """Handle a single HTTP request"""
+    def log_error(self, *args):
+        logger.error(*args)
 
-            self.raw_requestline = self.rfile.readline()
-            if not self.parse_request(): # An error code has been sent, just exit
-                return
+    def handle(self):
+        """Handle a single HTTP request"""
 
-            handler = ServerHandlerLogger(
-                self.rfile, self.wfile, self.get_stderr(), self.get_environ()
-            )
-            handler.request_handler = self      # backpointer for logging
-            handler.run(self.server.get_app())
+        self.raw_requestline = self.rfile.readline()
+        if not self.parse_request(): # An error code has been sent, just exit
+            return
 
-    class WSGIServerLogger(WSGIServer):
-        def handle_error(self, request, client_address):
-            """Handle an error. """
-            logger.exception('Exception happened during processing of request from %s' % str(client_address))
+        handler = ServerHandlerLogger(
+            self.rfile, self.wfile, self.get_stderr(), self.get_environ()
+        )
+        handler.request_handler = self      # backpointer for logging
+        handler.run(self.server.get_app())
 
-    def main(argv=sys.argv):
-        """Entry point for starting an HTTP git server."""
-        if len(argv) > 1:
-            gitdir = argv[1]
-        else:
-            gitdir = os.getcwd()
-
-        # TODO: allow serving on other addresses/ports via command-line flag
-        listen_addr = ''
-        port = 8000
-
-        log_utils.default_logging_config()
-        backend = DictBackend({'/': Repo(gitdir)})
-        app = make_wsgi_chain(backend)
-        server = make_server(listen_addr, port, app,
-                             handler_class=WSGIRequestHandlerLogger,
-                             server_class=WSGIServerLogger)
-        logger.info('Listening for HTTP connections on %s:%d', listen_addr,
-                    port)
-        server.serve_forever()
 
-except ImportError:
-    # No wsgiref found; don't provide the reference functionality, but leave
-    # the rest of the WSGI-based implementation.
-    def main(argv=sys.argv):
-        """Stub entry point for failing to start a server without wsgiref."""
-        sys.stderr.write(
-            'Sorry, the wsgiref module is required for dul-web.\n')
-        sys.exit(1)
+class WSGIServerLogger(WSGIServer):
+
+    def handle_error(self, request, client_address):
+        """Handle an error. """
+        logger.exception('Exception happened during processing of request from %s' % str(client_address))
+
+
+def main(argv=sys.argv):
+    """Entry point for starting an HTTP git server."""
+    import optparse
+    parser = optparse.OptionParser()
+    parser.add_option("-l", "--listen_address", dest="listen_address",
+                      default="localhost",
+                      help="Binding IP address.")
+    parser.add_option("-p", "--port", dest="port", type=int,
+                      default=8000,
+                      help="Port to listen on.")
+    options, args = parser.parse_args(argv)
+
+    if len(args) > 1:
+        gitdir = args[1]
+    else:
+        gitdir = os.getcwd()
+
+    log_utils.default_logging_config()
+    backend = DictBackend({'/': Repo(gitdir)})
+    app = make_wsgi_chain(backend)
+    server = make_server(options.listen_address, options.port, app,
+                         handler_class=WSGIRequestHandlerLogger,
+                         server_class=WSGIServerLogger)
+    logger.info('Listening for HTTP connections on %s:%d',
+                options.listen_address, options.port)
+    server.serve_forever()
 
 
 if __name__ == '__main__':

+ 22 - 12
setup.py

@@ -4,13 +4,11 @@
 
 try:
     from setuptools import setup, Extension
-    has_setuptools = True
 except ImportError:
     from distutils.core import setup, Extension
-    has_setuptools = False
 from distutils.core import Distribution
 
-dulwich_version_string = '0.9.7'
+dulwich_version_string = '0.9.8'
 
 include_dirs = []
 # Windows MSVC support
@@ -19,7 +17,6 @@ import sys
 if sys.platform == 'win32':
     include_dirs.append('dulwich')
 
-
 class DulwichDistribution(Distribution):
 
     def is_pure(self):
@@ -49,10 +46,16 @@ if sys.platform == 'darwin' and os.path.exists('/usr/bin/xcodebuild'):
         if l.startswith('Xcode') and int(l.split()[1].split('.')[0]) >= 4:
             os.environ['ARCHFLAGS'] = ''
 
-setup_kwargs = {}
-
-if has_setuptools:
-    setup_kwargs['test_suite'] = 'dulwich.tests.test_suite'
+if sys.version_info[0] == 2:
+    tests_require = ['fastimport', 'mock']
+    if not '__pypy__' in sys.modules:
+        tests_require.extend(['gevent', 'geventhttpclient'])
+else:
+    # fastimport, gevent, geventhttpclient are not available for PY3
+    # mock only used for test_swift, which requires gevent/geventhttpclient
+    tests_require = []
+if sys.version_info < (2, 7):
+    tests_require.append('unittest2')
 
 setup(name='dulwich',
       description='Python Git Library',
@@ -73,7 +76,15 @@ setup(name='dulwich',
       in the particular Monty Python sketch.
       """,
       packages=['dulwich', 'dulwich.tests', 'dulwich.tests.compat', 'dulwich.contrib'],
-      scripts=['bin/dulwich', 'bin/dul-web', 'bin/dul-receive-pack', 'bin/dul-upload-pack'],
+      scripts=['bin/dulwich', 'bin/dul-receive-pack', 'bin/dul-upload-pack'],
+      classifiers=[
+          'Development Status :: 4 - Beta',
+          'License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)',
+          'Programming Language :: Python :: 2.6',
+          'Programming Language :: Python :: 2.7',
+          'Operating System :: POSIX',
+          'Topic :: Software Development :: Version Control',
+      ],
       ext_modules=[
           Extension('dulwich._objects', ['dulwich/_objects.c'],
                     include_dirs=include_dirs),
@@ -82,9 +93,8 @@ setup(name='dulwich',
           Extension('dulwich._diff_tree', ['dulwich/_diff_tree.c'],
               include_dirs=include_dirs),
       ],
+      test_suite='dulwich.tests.test_suite',
+      tests_require=tests_require,
       distclass=DulwichDistribution,
       include_package_data=True,
-      use_2to3=True,
-      convert_2to3_doctests=['../docs/*', '../docs/tutorial/*', ],
-      **setup_kwargs
       )