Forráskód Böngészése

Imported Upstream version 0.9.8

Jelmer Vernooij 10 éve
szülő
commit
6ef7cf24ea
65 módosított fájl, 2360 hozzáadás és 642 törlés
  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>
 Dave Borowitz <dborowitz@google.com>
 Chris Eberle <eberle1080@gmail.com>
 Chris Eberle <eberle1080@gmail.com>
 "milki" <milki@rescomp.berkeley.edu>
 "milki" <milki@rescomp.berkeley.edu>
+Gary van der Merwe <garyvdm@gmail.com>
 
 
 Hervé Cauwelier <herve@itaapy.com> wrote the original tutorial.
 Hervé Cauwelier <herve@itaapy.com> wrote the original tutorial.
 
 

+ 1 - 1
Makefile

@@ -8,7 +8,7 @@ TESTRUNNER ?= unittest
 else
 else
 TESTRUNNER ?= unittest2.__main__
 TESTRUNNER ?= unittest2.__main__
 endif
 endif
-RUNTEST = PYTHONPATH=.:$(PYTHONPATH) $(PYTHON) -m $(TESTRUNNER)
+RUNTEST = PYTHONHASHSEED=random PYTHONPATH=.:$(PYTHONPATH) $(PYTHON) -m $(TESTRUNNER) $(TEST_OPTIONS)
 
 
 DESTDIR=/
 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
 0.9.7	2014-06-08
 
 
  BUG FIXES
  BUG FIXES

+ 8 - 2
PKG-INFO

@@ -1,6 +1,6 @@
-Metadata-Version: 1.0
+Metadata-Version: 1.1
 Name: dulwich
 Name: dulwich
-Version: 0.9.7
+Version: 0.9.8
 Summary: Python Git Library
 Summary: Python Git Library
 Home-page: https://samba.org/~jelmer/dulwich
 Home-page: https://samba.org/~jelmer/dulwich
 Author: Jelmer Vernooij
 Author: Jelmer Vernooij
@@ -18,3 +18,9 @@ Description:
               
               
 Keywords: git
 Keywords: git
 Platform: UNKNOWN
 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.
 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
 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.
 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
 There is a #dulwich IRC channel on Freenode, and a dulwich mailing list at
 https://launchpad.net/~dulwich-users.
 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,
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 # MA  02110-1301, USA.
 # MA  02110-1301, USA.
 
 
-from dulwich.server import serve_command, ReceivePackHandler
+from dulwich.porcelain import receive_pack
 import os
 import os
 import sys
 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.stderr.write("usage: %s <git-dir>\n" % os.path.basename(sys.argv[0]))
     sys.exit(1)
     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,
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 # MA  02110-1301, USA.
 # MA  02110-1301, USA.
 
 
-from dulwich.server import serve_command, UploadPackHandler
+from dulwich.porcelain import upload_pack
 import os
 import os
 import sys
 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.stderr.write("usage: %s <git-dir>\n" % os.path.basename(sys.argv[0]))
     sys.exit(1)
     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
 import sys
 from getopt import getopt
 from getopt import getopt
 import optparse
 import optparse
+import signal
+
+def signal_int(signal, frame):
+    sys.exit(1)
+
+signal.signal(signal.SIGINT, signal_int)
 
 
 from dulwich import porcelain
 from dulwich import porcelain
 from dulwich.client import get_transport_and_path
 from dulwich.client import get_transport_and_path
@@ -259,8 +265,29 @@ def cmd_daemon(args):
     options, args = parser.parse_args(args)
     options, args = parser.parse_args(args)
 
 
     log_utils.default_logging_config()
     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:
     else:
         gitdir = '.'
         gitdir = '.'
     from dulwich import porcelain
     from dulwich import porcelain
@@ -268,6 +295,52 @@ def cmd_daemon(args):
                      port=options.port)
                      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 = {
 commands = {
     "add": cmd_add,
     "add": cmd_add,
     "archive": cmd_archive,
     "archive": cmd_archive,
@@ -283,13 +356,17 @@ commands = {
     "fetch": cmd_fetch,
     "fetch": cmd_fetch,
     "init": cmd_init,
     "init": cmd_init,
     "log": cmd_log,
     "log": cmd_log,
+    "receive-pack": cmd_receive_pack,
     "reset": cmd_reset,
     "reset": cmd_reset,
     "rev-list": cmd_rev_list,
     "rev-list": cmd_rev_list,
     "rm": cmd_rm,
     "rm": cmd_rm,
     "show": cmd_show,
     "show": cmd_show,
+    "status": cmd_status,
     "symbolic-ref": cmd_symbolic_ref,
     "symbolic-ref": cmd_symbolic_ref,
     "tag": cmd_tag,
     "tag": cmd_tag,
     "update-server-info": cmd_update_server_info,
     "update-server-info": cmd_update_server_info,
+    "upload-pack": cmd_upload_pack,
+    "web-daemon": cmd_web_daemon,
     }
     }
 
 
 if len(sys.argv) < 2:
 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.
 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.
 communication.
 
 
 Dulwich is abstracting much of the Git plumbing, so there would be more to
 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
 Name: dulwich
-Version: 0.9.7
+Version: 0.9.8
 Summary: Python Git Library
 Summary: Python Git Library
 Home-page: https://samba.org/~jelmer/dulwich
 Home-page: https://samba.org/~jelmer/dulwich
 Author: Jelmer Vernooij
 Author: Jelmer Vernooij
@@ -18,3 +18,9 @@ Description:
               
               
 Keywords: git
 Keywords: git
 Platform: UNKNOWN
 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
 setup.py
 bin/dul-receive-pack
 bin/dul-receive-pack
 bin/dul-upload-pack
 bin/dul-upload-pack
-bin/dul-web
 bin/dulwich
 bin/dulwich
 docs/Makefile
 docs/Makefile
 docs/conf.py
 docs/conf.py
@@ -26,6 +25,7 @@ docs/tutorial/remote.txt
 docs/tutorial/repo.txt
 docs/tutorial/repo.txt
 docs/tutorial/tag.txt
 docs/tutorial/tag.txt
 dulwich/__init__.py
 dulwich/__init__.py
+dulwich/_compat.py
 dulwich/_diff_tree.c
 dulwich/_diff_tree.c
 dulwich/_objects.c
 dulwich/_objects.c
 dulwich/_pack.c
 dulwich/_pack.c

+ 1 - 1
dulwich/__init__.py

@@ -21,4 +21,4 @@
 
 
 """Python implementation of the Git file formats and protocols."""
 """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)
 	if (!entries2)
 		goto error;
 		goto error;
 
 
-	result = PyList_New(n1 + n2);
+	result = PyList_New(0);
 	if (!result)
 	if (!result)
 		goto error;
 		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) {
 	while (i1 < n1 && i2 < n2) {
 		cmp = entry_path_cmp(entries1[i1], entries2[i2]);
 		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);
 		pair = PyTuple_Pack(2, e1, e2);
 		if (!pair)
 		if (!pair)
 			goto error;
 			goto error;
-		PyList_SET_ITEM(result, Py_SIZE(result)++, pair);
+		PyList_Append(result, pair);
+		Py_DECREF(pair);
 	}
 	}
 
 
 	while (i1 < n1) {
 	while (i1 < n1) {
 		pair = PyTuple_Pack(2, entries1[i1++], null_entry);
 		pair = PyTuple_Pack(2, entries1[i1++], null_entry);
 		if (!pair)
 		if (!pair)
 			goto error;
 			goto error;
-		PyList_SET_ITEM(result, Py_SIZE(result)++, pair);
+		PyList_Append(result, pair);
+		Py_DECREF(pair);
 	}
 	}
 	while (i2 < n2) {
 	while (i2 < n2) {
 		pair = PyTuple_Pack(2, null_entry, entries2[i2++]);
 		pair = PyTuple_Pack(2, null_entry, entries2[i2++]);
 		if (!pair)
 		if (!pair)
 			goto error;
 			goto error;
-		PyList_SET_ITEM(result, Py_SIZE(result)++, pair);
+		PyList_Append(result, pair);
+		Py_DECREF(pair);
 	}
 	}
 	goto done;
 	goto done;
 
 

+ 41 - 31
dulwich/client.py

@@ -43,8 +43,14 @@ import dulwich
 import select
 import select
 import socket
 import socket
 import subprocess
 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 (
 from dulwich.errors import (
     GitProtocolError,
     GitProtocolError,
@@ -177,13 +183,15 @@ class GitClient(object):
             self._fetch_capabilities.remove('thin-pack')
             self._fetch_capabilities.remove('thin-pack')
 
 
     def send_pack(self, path, determine_wants, generate_pack_contents,
     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.
         """Upload a pack to a remote repository.
 
 
         :param path: Repository path
         :param path: Repository path
         :param generate_pack_contents: Function that can return a sequence of
         :param generate_pack_contents: Function that can return a sequence of
             the shas of the objects to upload.
             the shas of the objects to upload.
         :param progress: Optional progress function
         :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 SendPackError: if server rejects the pack data
         :raises UpdateRefsError: if the server supports report-status
         :raises UpdateRefsError: if the server supports report-status
@@ -389,7 +397,7 @@ class GitClient(object):
         while pkt:
         while pkt:
             parts = pkt.rstrip('\n').split(' ')
             parts = pkt.rstrip('\n').split(' ')
             if parts[0] == 'ACK':
             if parts[0] == 'ACK':
-                graph_walker.ack(pkt.split(' ')[1])
+                graph_walker.ack(parts[1])
             if len(parts) < 3 or parts[2] not in (
             if len(parts) < 3 or parts[2] not in (
                     'ready', 'continue', 'common'):
                     'ready', 'continue', 'common'):
                 break
                 break
@@ -425,13 +433,15 @@ class TraditionalGitClient(GitClient):
         raise NotImplementedError()
         raise NotImplementedError()
 
 
     def send_pack(self, path, determine_wants, generate_pack_contents,
     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.
         """Upload a pack to a remote repository.
 
 
         :param path: Repository path
         :param path: Repository path
         :param generate_pack_contents: Function that can return a sequence of
         :param generate_pack_contents: Function that can return a sequence of
             the shas of the objects to upload.
             the shas of the objects to upload.
         :param progress: Optional callback called with progress updates
         :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 SendPackError: if server rejects the pack data
         :raises UpdateRefsError: if the server supports report-status
         :raises UpdateRefsError: if the server supports report-status
@@ -454,21 +464,15 @@ class TraditionalGitClient(GitClient):
 
 
             if not 'delete-refs' in server_capabilities:
             if not 'delete-refs' in server_capabilities:
                 # Server does not support deletions. Fail later.
                 # 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:
                         if 'report-status' in negotiated_capabilities:
                             report_status_parser._ref_statuses.append(
                             report_status_parser._ref_statuses.append(
                                 'ng %s remote does not support deleting refs'
                                 'ng %s remote does not support deleting refs'
-                                % pair[1])
+                                % sha)
                             report_status_parser._ref_status_ok = False
                             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:
             if new_refs is None:
                 proto.write_pkt_line(None)
                 proto.write_pkt_line(None)
@@ -477,8 +481,8 @@ class TraditionalGitClient(GitClient):
             if len(new_refs) == 0 and len(orig_new_refs):
             if len(new_refs) == 0 and len(orig_new_refs):
                 # NOOP - Original new refs filtered out by policy
                 # NOOP - Original new refs filtered out by policy
                 proto.write_pkt_line(None)
                 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
                 return old_refs
 
 
             (have, want) = self._handle_receive_pack_head(
             (have, want) = self._handle_receive_pack_head(
@@ -486,16 +490,13 @@ class TraditionalGitClient(GitClient):
             if not want and old_refs == new_refs:
             if not want and old_refs == new_refs:
                 return new_refs
                 return new_refs
             objects = generate_pack_contents(have, want)
             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(
             self._handle_receive_pack_tail(
                 proto, negotiated_capabilities, progress)
                 proto, negotiated_capabilities, progress)
@@ -664,13 +665,15 @@ class LocalGitClient(GitClient):
         # Ignore the thin_packs argument
         # Ignore the thin_packs argument
 
 
     def send_pack(self, path, determine_wants, generate_pack_contents,
     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.
         """Upload a pack to a remote repository.
 
 
         :param path: Repository path
         :param path: Repository path
         :param generate_pack_contents: Function that can return a sequence of
         :param generate_pack_contents: Function that can return a sequence of
             the shas of the objects to upload.
             the shas of the objects to upload.
         :param progress: Optional progress function
         :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 SendPackError: if server rejects the pack data
         :raises UpdateRefsError: if the server supports report-status
         :raises UpdateRefsError: if the server supports report-status
@@ -979,13 +982,15 @@ class HttpGitClient(GitClient):
         return resp
         return resp
 
 
     def send_pack(self, path, determine_wants, generate_pack_contents,
     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.
         """Upload a pack to a remote repository.
 
 
         :param path: Repository path
         :param path: Repository path
         :param generate_pack_contents: Function that can return a sequence of
         :param generate_pack_contents: Function that can return a sequence of
             the shas of the objects to upload.
             the shas of the objects to upload.
         :param progress: Optional progress function
         :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 SendPackError: if server rejects the pack data
         :raises UpdateRefsError: if the server supports report-status
         :raises UpdateRefsError: if the server supports report-status
@@ -1012,7 +1017,7 @@ class HttpGitClient(GitClient):
             return new_refs
             return new_refs
         objects = generate_pack_contents(have, want)
         objects = generate_pack_contents(have, want)
         if len(objects) > 0:
         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,
         resp = self._smart_request("git-receive-pack", url,
                                    data=req_data.getvalue())
                                    data=req_data.getvalue())
         try:
         try:
@@ -1106,6 +1111,11 @@ def get_transport_and_path(location, **kwargs):
     except ValueError:
     except ValueError:
         pass
         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:
     if ':' in location and not '@' in location:
         # SSH with no user@, zero or one leading slash.
         # SSH with no user@, zero or one leading slash.
         (hostname, path) = location.split(':')
         (hostname, path) = location.split(':')

+ 12 - 12
dulwich/config.py

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

+ 13 - 13
dulwich/contrib/swift.py

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

+ 35 - 32
dulwich/contrib/test_swift.py

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

+ 6 - 1
dulwich/diff_tree.py

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

+ 4 - 10
dulwich/hooks.py

@@ -76,8 +76,8 @@ class ShellHook(Hook):
 
 
         if len(args) != self.numparam:
         if len(args) != self.numparam:
             raise HookError("Hook %s executed with wrong number of args. \
             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):
         if (self.pre_exec_callback is not None):
             args = self.pre_exec_callback(*args)
             args = self.pre_exec_callback(*args)
@@ -127,21 +127,15 @@ class CommitMsgShellHook(ShellHook):
         def prepare_msg(*args):
         def prepare_msg(*args):
             (fd, path) = tempfile.mkstemp()
             (fd, path) = tempfile.mkstemp()
 
 
-            f = os.fdopen(fd, 'wb')
-            try:
+            with os.fdopen(fd, 'wb') as f:
                 f.write(args[0])
                 f.write(args[0])
-            finally:
-                f.close()
 
 
             return (path,)
             return (path,)
 
 
         def clean_msg(success, *args):
         def clean_msg(success, *args):
             if success:
             if success:
-                f = open(args[0], 'rb')
-                try:
+                with open(args[0], 'rb') as f:
                     new_msg = f.read()
                     new_msg = f.read()
-                finally:
-                    f.close()
                 os.unlink(args[0])
                 os.unlink(args[0])
                 return new_msg
                 return new_msg
             os.unlink(args[0])
             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)
             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,
 def build_index_from_tree(prefix, index_path, object_store, tree_id,
                           honor_filemode=True):
                           honor_filemode=True):
     """Generate and materialize index from a tree
     """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))
             os.makedirs(os.path.dirname(full_path))
 
 
         # FIXME: Merge new index into working tree
         # 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
         # Add file to index
         st = os.lstat(full_path)
         st = os.lstat(full_path)
         index[entry.path] = index_entry_from_stat(st, entry.sha, 0)
         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()
     blob = Blob()
     if not stat.S_ISLNK(st.st_mode):
     if not stat.S_ISLNK(st.st_mode):
-        f = open(path, 'rb')
-        try:
+        with open(path, 'rb') as f:
             blob.data = f.read()
             blob.data = f.read()
-        finally:
-            f.close()
     else:
     else:
         blob.data = os.readlink(path)
         blob.data = os.readlink(path)
     return blob
     return blob

+ 9 - 27
dulwich/object_store.py

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

+ 35 - 14
dulwich/objects.py

@@ -48,6 +48,7 @@ _AUTHOR_HEADER = "author"
 _COMMITTER_HEADER = "committer"
 _COMMITTER_HEADER = "committer"
 _ENCODING_HEADER = "encoding"
 _ENCODING_HEADER = "encoding"
 _MERGETAG_HEADER = "mergetag"
 _MERGETAG_HEADER = "mergetag"
+_GPGSIG_HEADER = "gpgsig"
 
 
 # Header fields for objects
 # Header fields for objects
 _OBJECT_HEADER = "object"
 _OBJECT_HEADER = "object"
@@ -359,11 +360,8 @@ class ShaFile(object):
         raise NotImplementedError(self._serialize)
         raise NotImplementedError(self._serialize)
 
 
     def _parse_path(self):
     def _parse_path(self):
-        f = GitFile(self._path, 'rb')
-        try:
+        with GitFile(self._path, 'rb') as f:
             self._parse_file(f)
             self._parse_file(f)
-        finally:
-            f.close()
 
 
     def _parse_file(self, f):
     def _parse_file(self, f):
         magic = self._magic
         magic = self._magic
@@ -378,16 +376,13 @@ class ShaFile(object):
     @classmethod
     @classmethod
     def from_path(cls, path):
     def from_path(cls, path):
         """Open a SHA file from disk."""
         """Open a SHA file from disk."""
-        f = GitFile(path, 'rb')
-        try:
+        with GitFile(path, 'rb') as f:
             obj = cls.from_file(f)
             obj = cls.from_file(f)
             obj._path = path
             obj._path = path
             obj._sha = FixedSha(filename_to_hex(path))
             obj._sha = FixedSha(filename_to_hex(path))
             obj._file = None
             obj._file = None
             obj._magic = None
             obj._magic = None
             return obj
             return obj
-        finally:
-            f.close()
 
 
     @classmethod
     @classmethod
     def from_file(cls, f):
     def from_file(cls, f):
@@ -493,6 +488,14 @@ class ShaFile(object):
             self._sha = new_sha
             self._sha = new_sha
         return self._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
     @property
     def id(self):
     def id(self):
         """The hex SHA of this object."""
         """The hex SHA of this object."""
@@ -1027,7 +1030,7 @@ def parse_commit(chunks):
 
 
     :param chunks: Chunks to parse
     :param chunks: Chunks to parse
     :return: Tuple of (tree, parents, author_info, commit_info,
     :return: Tuple of (tree, parents, author_info, commit_info,
-        encoding, mergetag, message, extra)
+        encoding, mergetag, gpgsig, message, extra)
     """
     """
     parents = []
     parents = []
     extra = []
     extra = []
@@ -1037,8 +1040,10 @@ def parse_commit(chunks):
     encoding = None
     encoding = None
     mergetag = []
     mergetag = []
     message = None
     message = None
+    gpgsig = None
 
 
     for field, value in _parse_message(chunks):
     for field, value in _parse_message(chunks):
+        # TODO(jelmer): Enforce ordering
         if field == _TREE_HEADER:
         if field == _TREE_HEADER:
             tree = value
             tree = value
         elif field == _PARENT_HEADER:
         elif field == _PARENT_HEADER:
@@ -1055,12 +1060,14 @@ def parse_commit(chunks):
             encoding = value
             encoding = value
         elif field == _MERGETAG_HEADER:
         elif field == _MERGETAG_HEADER:
             mergetag.append(Tag.from_string(value + "\n"))
             mergetag.append(Tag.from_string(value + "\n"))
+        elif field == _GPGSIG_HEADER:
+            gpgsig = value
         elif field is None:
         elif field is None:
             message = value
             message = value
         else:
         else:
             extra.append((field, value))
             extra.append((field, value))
     return (tree, parents, author_info, commit_info, encoding, mergetag,
     return (tree, parents, author_info, commit_info, encoding, mergetag,
-            message, extra)
+            gpgsig, message, extra)
 
 
 
 
 class Commit(ShaFile):
 class Commit(ShaFile):
@@ -1073,13 +1080,14 @@ class Commit(ShaFile):
                  '_commit_timezone_neg_utc', '_commit_time',
                  '_commit_timezone_neg_utc', '_commit_time',
                  '_author_time', '_author_timezone', '_commit_timezone',
                  '_author_time', '_author_timezone', '_commit_timezone',
                  '_author', '_committer', '_parents', '_extra',
                  '_author', '_committer', '_parents', '_extra',
-                 '_encoding', '_tree', '_message', '_mergetag')
+                 '_encoding', '_tree', '_message', '_mergetag', '_gpgsig')
 
 
     def __init__(self):
     def __init__(self):
         super(Commit, self).__init__()
         super(Commit, self).__init__()
         self._parents = []
         self._parents = []
         self._encoding = None
         self._encoding = None
         self._mergetag = []
         self._mergetag = []
+        self._gpgsig = None
         self._extra = []
         self._extra = []
         self._author_timezone_neg_utc = False
         self._author_timezone_neg_utc = False
         self._commit_timezone_neg_utc = False
         self._commit_timezone_neg_utc = False
@@ -1093,7 +1101,7 @@ class Commit(ShaFile):
 
 
     def _deserialize(self, chunks):
     def _deserialize(self, chunks):
         (self._tree, self._parents, author_info, commit_info, self._encoding,
         (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))
                         parse_commit(chunks))
         (self._author, self._author_time, (self._author_timezone,
         (self._author, self._author_time, (self._author_timezone,
              self._author_timezone_neg_utc)) = author_info
              self._author_timezone_neg_utc)) = author_info
@@ -1166,6 +1174,11 @@ class Commit(ShaFile):
                 raise AssertionError(
                 raise AssertionError(
                     "newline in extra data: %r -> %r" % (k, v))
                     "newline in extra data: %r -> %r" % (k, v))
             chunks.append("%s %s\n" % (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("\n")  # There must be a new line after the headers
         chunks.append(self._message)
         chunks.append(self._message)
         return chunks
         return chunks
@@ -1184,14 +1197,19 @@ class Commit(ShaFile):
         self._needs_serialization = True
         self._needs_serialization = True
         self._parents = value
         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):
     def _get_extra(self):
         """Return extra settings of this commit."""
         """Return extra settings of this commit."""
         self._ensure_parsed()
         self._ensure_parsed()
         return self._extra
         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",
     author = serializable_property("author",
         "The name of the author of the commit")
         "The name of the author of the commit")
@@ -1221,6 +1239,9 @@ class Commit(ShaFile):
     mergetag = serializable_property(
     mergetag = serializable_property(
         "mergetag", "Associated signed tag.")
         "mergetag", "Associated signed tag.")
 
 
+    gpgsig = serializable_property(
+        "gpgsig", "GPG Signature.")
+
 
 
 OBJECT_CLASSES = (
 OBJECT_CLASSES = (
     Commit,
     Commit,

+ 33 - 36
dulwich/pack.py

@@ -33,13 +33,19 @@ a pointer in to the corresponding packfile.
 from collections import defaultdict
 from collections import defaultdict
 
 
 import binascii
 import binascii
-from io import BytesIO
+from io import BytesIO, UnsupportedOperation
 from collections import (
 from collections import (
     deque,
     deque,
     )
     )
 import difflib
 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:
 try:
     import mmap
     import mmap
@@ -55,7 +61,6 @@ from os import (
     )
     )
 import struct
 import struct
 from struct import unpack_from
 from struct import unpack_from
-import warnings
 import zlib
 import zlib
 
 
 from dulwich.errors import (
 from dulwich.errors import (
@@ -258,18 +263,17 @@ def load_pack_index(path):
     :param filename: Path to the index file
     :param filename: Path to the index file
     :return: A PackIndex loaded from the given path
     :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)
         return load_pack_index_file(path, f)
-    finally:
-        f.close()
 
 
 
 
 def _load_file_contents(f, size=None):
 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()
         fd = f.fileno()
+    except (UnsupportedOperation, AttributeError):
+        fd = None
+    # Attempt to use mmap if possible
+    if fd is not None:
         if size is None:
         if size is None:
             size = os.fstat(fd).st_size
             size = os.fstat(fd).st_size
         if has_mmap:
         if has_mmap:
@@ -919,7 +923,13 @@ def compute_file_sha(f, start_ofs=0, end_ofs=0, buffer_size=1<<16):
     """
     """
     sha = sha1()
     sha = sha1()
     f.seek(0, SEEK_END)
     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)
     f.seek(start_ofs)
     while todo:
     while todo:
         data = f.read(min(todo, buffer_size))
         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
     Pack files can be accessed both sequentially for exploding a pack, and
     directly with the help of an index to retrieve a specific object.
     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
     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.
     indicates that the subsequent byte is still part of the header.
@@ -1128,11 +1138,8 @@ class PackData(object):
         :return: Checksum of index file
         :return: Checksum of index file
         """
         """
         entries = self.sorted_entries(progress=progress)
         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())
             return write_pack_index_v1(f, entries, self.calculate_checksum())
-        finally:
-            f.close()
 
 
     def create_index_v2(self, filename, progress=None):
     def create_index_v2(self, filename, progress=None):
         """Create a version 2 index file for this data file.
         """Create a version 2 index file for this data file.
@@ -1142,11 +1149,8 @@ class PackData(object):
         :return: Checksum of index file
         :return: Checksum of index file
         """
         """
         entries = self.sorted_entries(progress=progress)
         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())
             return write_pack_index_v2(f, entries, self.calculate_checksum())
-        finally:
-            f.close()
 
 
     def create_index(self, filename, progress=None,
     def create_index(self, filename, progress=None,
                      version=2):
                      version=2):
@@ -1457,19 +1461,13 @@ def write_pack(filename, objects, deltify=None, delta_window_size=None):
     :param deltify: Whether to deltify pack objects
     :param deltify: Whether to deltify pack objects
     :return: Tuple with checksum of pack file and index file
     :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,
         entries, data_sum = write_pack_objects(f, objects,
             delta_window_size=delta_window_size, deltify=deltify)
             delta_window_size=delta_window_size, deltify=deltify)
-    finally:
-        f.close()
     entries = [(k, v[0], v[1]) for (k, v) in entries.iteritems()]
     entries = [(k, v[0], v[1]) for (k, v) in entries.iteritems()]
     entries.sort()
     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)
         return data_sum, write_pack_index_v2(f, entries, data_sum)
-    finally:
-        f.close()
 
 
 
 
 def write_pack_header(f, num_objects):
 def write_pack_header(f, num_objects):
@@ -1602,9 +1600,10 @@ def _delta_encode_size(size):
     return ret
     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):
 def _encode_copy_operation(start, length):
     scratch = ''
     scratch = ''
@@ -1613,7 +1612,7 @@ def _encode_copy_operation(start, length):
         if start & 0xff << i*8:
         if start & 0xff << i*8:
             scratch += chr((start >> i*8) & 0xff)
             scratch += chr((start >> i*8) & 0xff)
             op |= 1 << i
             op |= 1 << i
-    for i in range(3):
+    for i in range(2):
         if length & 0xff << i*8:
         if length & 0xff << i*8:
             scratch += chr((length >> i*8) & 0xff)
             scratch += chr((length >> i*8) & 0xff)
             op |= 1 << (4+i)
             op |= 1 << (4+i)
@@ -1701,6 +1700,7 @@ def apply_delta(src_buf, delta):
                     index += 1
                     index += 1
                     cp_off |= x << (i * 8)
                     cp_off |= x << (i * 8)
             cp_size = 0
             cp_size = 0
+            # Version 3 packs can contain copy sizes larger than 64K.
             for i in range(3):
             for i in range(3):
                 if cmd & (1 << (4+i)):
                 if cmd & (1 << (4+i)):
                     x = ord(delta[index])
                     x = ord(delta[index])
@@ -1918,13 +1918,10 @@ class Pack(object):
         :return: The path of the .keep file, as a string.
         :return: The path of the .keep file, as a string.
         """
         """
         keepfile_name = '%s.keep' % self._basename
         keepfile_name = '%s.keep' % self._basename
-        keepfile = GitFile(keepfile_name, 'wb')
-        try:
+        with GitFile(keepfile_name, 'wb') as keepfile:
             if msg:
             if msg:
                 keepfile.write(msg)
                 keepfile.write(msg)
                 keepfile.write('\n')
                 keepfile.write('\n')
-        finally:
-            keepfile.close()
         return keepfile_name
         return keepfile_name
 
 
 
 

+ 176 - 13
dulwich/porcelain.py

@@ -21,19 +21,22 @@
 Currently implemented:
 Currently implemented:
  * archive
  * archive
  * add
  * add
+ * branch{_create,_delete,_list}
  * clone
  * clone
  * commit
  * commit
  * commit-tree
  * commit-tree
  * daemon
  * daemon
  * diff-tree
  * diff-tree
+ * fetch
  * init
  * init
- * list-tags
  * pull
  * pull
  * push
  * push
  * rm
  * rm
+ * receive-pack
  * reset
  * reset
  * rev-list
  * rev-list
- * tag
+ * tag{_create,_delete,_list}
+ * upload-pack
  * update-server-info
  * update-server-info
  * status
  * status
  * symbolic-ref
  * symbolic-ref
@@ -62,8 +65,16 @@ from dulwich.objects import (
     )
     )
 from dulwich.objectspec import parse_object
 from dulwich.objectspec import parse_object
 from dulwich.patch import write_tree_diff
 from dulwich.patch import write_tree_diff
+from dulwich.protocol import Protocol
 from dulwich.repo import (BaseRepo, Repo)
 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
 # Module level tuple definition for status output
 GitStatus = namedtuple('GitStatus', 'staged unstaged untracked')
 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)
         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):
         objectish="HEAD", tag_time=None, tag_timezone=None):
     """Creates a tag in git via dulwich calls:
     """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
     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.
     """List all tags.
 
 
     :param repo: Path to repository
     :param repo: Path to repository
@@ -424,6 +447,23 @@ def list_tags(repo, outstream=sys.stdout):
     return tags
     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"):
 def reset(repo, mode, committish="HEAD"):
     """Reset current HEAD to the specified state.
     """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)
     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.
     """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,
     :return: GitStatus tuple,
         staged -    list of staged paths (diff index/HEAD)
         staged -    list of staged paths (diff index/HEAD)
         unstaged -  list of unstaged paths (diff index/working-tree)
         unstaged -  list of unstaged paths (diff index/working-tree)
         untracked - list of untracked, un-ignored & non-.git paths
         untracked - list of untracked, un-ignored & non-.git paths
     """
     """
+    r = open_repo(repo)
+
     # 1. Get status of staged
     # 1. Get status of staged
-    tracked_changes = get_tree_changes(repo)
+    tracked_changes = get_tree_changes(r)
     # 2. Get status of unstaged
     # 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.
     # TODO - Status of untracked - add untracked changes, need gitignore.
     untracked_changes = []
     untracked_changes = []
     return GitStatus(tracked_changes, unstaged_changes, 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
     # Compares the Index to the HEAD & determines changes
     # Iterate through the changes and report add/delete/modify
     # Iterate through the changes and report add/delete/modify
+    # TODO: call out to dulwich.diff_tree somehow.
     tracked_changes = {
     tracked_changes = {
         'add': [],
         'add': [],
         'delete': [],
         'delete': [],
@@ -547,12 +590,132 @@ def daemon(path=".", address=None, port=None):
     """Run a daemon serving Git requests over TCP/IP.
     """Run a daemon serving Git requests over TCP/IP.
 
 
     :param path: Path to the directory to serve.
     :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.
     # TODO(jelmer): Support git-daemon-export-ok and --export-all.
-    from dulwich.server import (
-        FileSystemBackend,
-        TCPGitServer,
-        )
     backend = FileSystemBackend(path)
     backend = FileSystemBackend(path)
     server = TCPGitServer(backend, address, port)
     server = TCPGitServer(backend, address, port)
     server.serve_forever()
     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')
                 self.report_activity(size, 'read')
             pkt_contents = read(size-4)
             pkt_contents = read(size-4)
             if len(pkt_contents) + 4 != size:
             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))
                                      .format(len(pkt_contents) + 4, size))
             return pkt_contents
             return pkt_contents
         except socket.error as e:
         except socket.error as e:

+ 8 - 15
dulwich/refs.py

@@ -38,6 +38,7 @@ from dulwich.file import (
 
 
 
 
 SYMREF = 'ref: '
 SYMREF = 'ref: '
+LOCAL_BRANCH_PREFIX = 'refs/heads/'
 
 
 
 
 def check_ref_format(refname):
 def check_ref_format(refname):
@@ -446,7 +447,7 @@ class DiskRefsContainer(RefsContainer):
                 if e.errno == errno.ENOENT:
                 if e.errno == errno.ENOENT:
                     return {}
                     return {}
                 raise
                 raise
-            try:
+            with f:
                 first_line = next(iter(f)).rstrip()
                 first_line = next(iter(f)).rstrip()
                 if (first_line.startswith("# pack-refs") and " peeled" in
                 if (first_line.startswith("# pack-refs") and " peeled" in
                         first_line):
                         first_line):
@@ -458,8 +459,6 @@ class DiskRefsContainer(RefsContainer):
                     f.seek(0)
                     f.seek(0)
                     for sha, name in read_packed_refs(f):
                     for sha, name in read_packed_refs(f):
                         self._packed_refs[name] = sha
                         self._packed_refs[name] = sha
-            finally:
-                f.close()
         return self._packed_refs
         return self._packed_refs
 
 
     def get_peeled(self, name):
     def get_peeled(self, name):
@@ -493,8 +492,7 @@ class DiskRefsContainer(RefsContainer):
         """
         """
         filename = self.refpath(name)
         filename = self.refpath(name)
         try:
         try:
-            f = GitFile(filename, 'rb')
-            try:
+            with GitFile(filename, 'rb') as f:
                 header = f.read(len(SYMREF))
                 header = f.read(len(SYMREF))
                 if header == SYMREF:
                 if header == SYMREF:
                     # Read only the first line
                     # Read only the first line
@@ -502,8 +500,6 @@ class DiskRefsContainer(RefsContainer):
                 else:
                 else:
                     # Read only the first 40 bytes
                     # Read only the first 40 bytes
                     return header + f.read(40 - len(SYMREF))
                     return header + f.read(40 - len(SYMREF))
-            finally:
-                f.close()
         except IOError as e:
         except IOError as e:
             if e.errno == errno.ENOENT:
             if e.errno == errno.ENOENT:
                 return None
                 return None
@@ -568,8 +564,7 @@ class DiskRefsContainer(RefsContainer):
             realname = name
             realname = name
         filename = self.refpath(realname)
         filename = self.refpath(realname)
         ensure_dir_exists(os.path.dirname(filename))
         ensure_dir_exists(os.path.dirname(filename))
-        f = GitFile(filename, 'wb')
-        try:
+        with GitFile(filename, 'wb') as f:
             if old_ref is not None:
             if old_ref is not None:
                 try:
                 try:
                     # read again while holding the lock
                     # read again while holding the lock
@@ -587,8 +582,6 @@ class DiskRefsContainer(RefsContainer):
             except (OSError, IOError):
             except (OSError, IOError):
                 f.abort()
                 f.abort()
                 raise
                 raise
-        finally:
-            f.close()
         return True
         return True
 
 
     def add_if_new(self, name, ref):
     def add_if_new(self, name, ref):
@@ -610,8 +603,7 @@ class DiskRefsContainer(RefsContainer):
         self._check_refname(realname)
         self._check_refname(realname)
         filename = self.refpath(realname)
         filename = self.refpath(realname)
         ensure_dir_exists(os.path.dirname(filename))
         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():
             if os.path.exists(filename) or name in self.get_packed_refs():
                 f.abort()
                 f.abort()
                 return False
                 return False
@@ -620,8 +612,6 @@ class DiskRefsContainer(RefsContainer):
             except (OSError, IOError):
             except (OSError, IOError):
                 f.abort()
                 f.abort()
                 raise
                 raise
-        finally:
-            f.close()
         return True
         return True
 
 
     def remove_if_equals(self, name, old_ref):
     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)
         yield '%s\t%s\n' % (o.id, name)
         if o.id != peeled.id:
         if o.id != peeled.id:
             yield '%s\t%s^{}\n' % (peeled.id, name)
             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
             self._controldir = root
         elif (os.path.isfile(os.path.join(root, ".git"))):
         elif (os.path.isfile(os.path.join(root, ".git"))):
             import re
             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()
                 _, path = re.match('(gitdir: )(.+$)', f.read()).groups()
-            finally:
-                f.close()
             self.bare = False
             self.bare = False
             self._controldir = os.path.join(root, path)
             self._controldir = os.path.join(root, path)
         else:
         else:
@@ -689,11 +686,8 @@ class Repo(BaseRepo):
         :param contents: A string to write to the file.
         :param contents: A string to write to the file.
         """
         """
         path = path.lstrip(os.path.sep)
         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)
             f.write(contents)
-        finally:
-            f.close()
 
 
     def get_named_file(self, path):
     def get_named_file(self, path):
         """Get a file from the control dir with a specific name.
         """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')
         path = os.path.join(self._controldir, 'description')
         try:
         try:
-            f = GitFile(path, 'rb')
-            try:
+            with GitFile(path, 'rb') as f:
                 return f.read()
                 return f.read()
-            finally:
-                f.close()
         except (IOError, OSError) as e:
         except (IOError, OSError) as e:
             if e.errno != errno.ENOENT:
             if e.errno != errno.ENOENT:
                 raise
                 raise
@@ -854,11 +845,8 @@ class Repo(BaseRepo):
         """
         """
 
 
         path = os.path.join(self._controldir, 'description')
         path = os.path.join(self._controldir, 'description')
-        f = open(path, 'w')
-        try:
+        with open(path, 'w') as f:
             f.write(description)
             f.write(description)
-        finally:
-            f.close()
 
 
     @classmethod
     @classmethod
     def _init_maybe_bare(cls, path, bare):
     def _init_maybe_bare(cls, path, bare):

+ 60 - 42
dulwich/server.py

@@ -36,18 +36,20 @@ Currently supported capabilities:
  * no-progress
  * no-progress
  * report-status
  * report-status
  * delete-refs
  * delete-refs
-
-Known capabilities that are not supported:
- * shallow (http://pad.lv/909524)
+ * shallow
 """
 """
 
 
 import collections
 import collections
 import os
 import os
 import socket
 import socket
-import SocketServer
 import sys
 import sys
 import zlib
 import zlib
 
 
+try:
+    import SocketServer
+except ImportError:
+    import socketserver as SocketServer
+
 from dulwich.errors import (
 from dulwich.errors import (
     ApplyDeltaError,
     ApplyDeltaError,
     ChecksumMismatch,
     ChecksumMismatch,
@@ -162,9 +164,16 @@ class DictBackend(Backend):
 class FileSystemBackend(Backend):
 class FileSystemBackend(Backend):
     """Simple backend that looks up Git repositories in the local file system."""
     """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):
     def open_repository(self, path):
         logger.debug('opening repository at %s', 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):
 class Handler(object):
@@ -365,6 +374,46 @@ def _find_shallow(store, heads, depth):
     return shallow, not_shallow
     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):
 class ProtocolGraphWalker(object):
     """A graph walker that knows the git protocol.
     """A graph walker that knows the git protocol.
 
 
@@ -464,6 +513,8 @@ class ProtocolGraphWalker(object):
         self.proto.unread_pkt_line('%s %s' % (command, value))
         self.proto.unread_pkt_line('%s %s' % (command, value))
 
 
     def ack(self, have_ref):
     def ack(self, have_ref):
+        if len(have_ref) != 40:
+            raise ValueError("invalid sha %r" % have_ref)
         return self._impl.ack(have_ref)
         return self._impl.ack(have_ref)
 
 
     def reset(self):
     def reset(self):
@@ -526,34 +577,6 @@ class ProtocolGraphWalker(object):
     def set_wants(self, wants):
     def set_wants(self, wants):
         self._wants = 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):
     def all_wants_satisfied(self, haves):
         """Check whether all the current wants are satisfied by a set of 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
         :note: Wants are specified with set_wants rather than passed in since
             in the current interface they are determined outside this class.
             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):
     def set_ack_type(self, ack_type):
         impl_classes = {
         impl_classes = {
@@ -774,9 +792,9 @@ class ReceivePackHandler(Handler):
         flush()
         flush()
 
 
     def handle(self):
     def handle(self):
-        refs = sorted(self.repo.get_refs().iteritems())
-
         if self.advertise_refs or not self.http_req:
         if self.advertise_refs or not self.http_req:
+            refs = sorted(self.repo.get_refs().iteritems())
+
             if refs:
             if refs:
                 self.proto.write_pkt_line(
                 self.proto.write_pkt_line(
                   "%s %s\x00%s\n" % (refs[0][1], refs[0][0],
                   "%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)
     options, args = parser.parse_args(argv)
 
 
     log_utils.default_logging_config()
     log_utils.default_logging_config()
-    if len(argv) > 1:
+    if len(args) > 1:
         gitdir = args[1]
         gitdir = args[1]
     else:
     else:
         gitdir = '.'
         gitdir = '.'

+ 6 - 3
dulwich/tests/__init__.py

@@ -29,7 +29,10 @@ import tempfile
 
 
 # If Python itself provides an exception, use that
 # If Python itself provides an exception, use that
 import unittest
 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):
 def get_safe_env(env=None):
@@ -101,7 +104,6 @@ class BlackboxTestCase(TestCase):
         return subprocess.Popen(argv,
         return subprocess.Popen(argv,
             stdout=subprocess.PIPE,
             stdout=subprocess.PIPE,
             stdin=subprocess.PIPE, stderr=subprocess.PIPE,
             stdin=subprocess.PIPE, stderr=subprocess.PIPE,
-            universal_newlines=True,
             env=env)
             env=env)
 
 
 
 
@@ -177,7 +179,8 @@ def compat_test_suite():
 def test_suite():
 def test_suite():
     result = unittest.TestSuite()
     result = unittest.TestSuite()
     result.addTests(self_test_suite())
     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
     from dulwich.tests.compat import test_suite as compat_test_suite
     result.addTests(compat_test_suite())
     result.addTests(compat_test_suite())
     from dulwich.contrib import test_suite as contrib_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 (
 from dulwich.tests.utils import (
     tear_down_repo,
     tear_down_repo,
+    skipIfPY3,
     )
     )
 from dulwich.tests.compat.utils import (
 from dulwich.tests.compat.utils import (
     import_repo,
     import_repo,
@@ -53,18 +54,17 @@ def _get_shallow(repo):
     if not shallow_file:
     if not shallow_file:
         return []
         return []
     shallows = []
     shallows = []
-    try:
+    with shallow_file:
         for line in shallow_file:
         for line in shallow_file:
             sha = line.strip()
             sha = line.strip()
             if not sha:
             if not sha:
                 continue
                 continue
             hex_to_sha(sha)
             hex_to_sha(sha)
             shallows.append(sha)
             shallows.append(sha)
-    finally:
-        shallow_file.close()
     return shallows
     return shallows
 
 
 
 
+@skipIfPY3
 class ServerTests(object):
 class ServerTests(object):
     """Base tests for testing servers.
     """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."""
 """Compatibilty tests between the Dulwich client and the cgit server."""
 
 
 from io import BytesIO
 from io import BytesIO
-import BaseHTTPServer
-import SimpleHTTPServer
 import copy
 import copy
 import os
 import os
 import select
 import select
 import shutil
 import shutil
 import signal
 import signal
 import subprocess
 import subprocess
+import sys
 import tarfile
 import tarfile
 import tempfile
 import tempfile
 import threading
 import threading
 import urllib
 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 (
 from dulwich import (
     client,
     client,
@@ -45,16 +54,21 @@ from dulwich import (
     )
     )
 from dulwich.tests import (
 from dulwich.tests import (
     get_safe_env,
     get_safe_env,
+    SkipTest,
+    )
+from dulwich.tests.utils import (
+    skipIfPY3,
     )
     )
-
 from dulwich.tests.compat.utils import (
 from dulwich.tests.compat.utils import (
     CompatTestCase,
     CompatTestCase,
     check_for_daemon,
     check_for_daemon,
     import_repo_to_dir,
     import_repo_to_dir,
     run_git_or_fail,
     run_git_or_fail,
+    _DEFAULT_GIT,
     )
     )
 
 
 
 
+@skipIfPY3
 class DulwichClientTestBase(object):
 class DulwichClientTestBase(object):
     """Tests for client/server compatibility."""
     """Tests for client/server compatibility."""
 
 
@@ -235,25 +249,37 @@ class DulwichTCPClientTest(CompatTestCase, DulwichClientTestBase):
         if check_for_daemon(limit=1):
         if check_for_daemon(limit=1):
             raise SkipTest('git-daemon was already running on port %s' %
             raise SkipTest('git-daemon was already running on port %s' %
                               protocol.TCP_GIT_PORT)
                               protocol.TCP_GIT_PORT)
+        env = get_safe_env()
         fd, self.pidfile = tempfile.mkstemp(prefix='dulwich-test-git-client',
         fd, self.pidfile = tempfile.mkstemp(prefix='dulwich-test-git-client',
                                             suffix=".pid")
                                             suffix=".pid")
         os.fdopen(fd).close()
         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():
         if not check_for_daemon():
             raise SkipTest('git-daemon failed to start')
             raise SkipTest('git-daemon failed to start')
 
 
     def tearDown(self):
     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)
         DulwichClientTestBase.tearDown(self)
         CompatTestCase.tearDown(self)
         CompatTestCase.tearDown(self)
 
 

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

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

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

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

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

@@ -19,9 +19,8 @@
 
 
 """Tests for git compatibility utilities."""
 """Tests for git compatibility utilities."""
 
 
-from unittest import SkipTest
-
 from dulwich.tests import (
 from dulwich.tests import (
+    SkipTest,
     TestCase,
     TestCase,
     )
     )
 from dulwich.tests.compat import utils
 from dulwich.tests.compat import utils
@@ -44,19 +43,19 @@ class GitVersionTests(TestCase):
         utils.run_git = self._orig_run_git
         utils.run_git = self._orig_run_git
 
 
     def test_git_version_none(self):
     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())
         self.assertEqual(None, utils.git_version())
 
 
     def test_git_version_3(self):
     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())
         self.assertEqual((1, 6, 6, 0), utils.git_version())
 
 
     def test_git_version_4(self):
     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())
         self.assertEqual((1, 7, 0, 2), utils.git_version())
 
 
     def test_git_version_extra(self):
     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())
         self.assertEqual((1, 7, 0, 3), utils.git_version())
 
 
     def assertRequireSucceeds(self, required_version):
     def assertRequireSucceeds(self, required_version):
@@ -71,7 +70,7 @@ class GitVersionTests(TestCase):
 
 
     def test_require_git_version(self):
     def test_require_git_version(self):
         try:
         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))
             self.assertRequireSucceeds((1, 6, 6, 0))
             self.assertRequireSucceeds((1, 6, 6, 0))
             self.assertRequireSucceeds((1, 6, 5))
             self.assertRequireSucceeds((1, 6, 5))
@@ -81,7 +80,7 @@ class GitVersionTests(TestCase):
             self.assertRaises(ValueError, utils.require_git_version,
             self.assertRaises(ValueError, utils.require_git_version,
                               (1, 6, 6, 0, 0))
                               (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))
             self.assertRequireSucceeds((1, 6, 6, 0))
             self.assertRequireSucceeds((1, 6, 6, 0))
             self.assertRequireSucceeds((1, 7, 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
 import threading
-from unittest import (
-    SkipTest,
-    )
 from wsgiref import simple_server
 from wsgiref import simple_server
 
 
 from dulwich.server import (
 from dulwich.server import (
     DictBackend,
     DictBackend,
     )
     )
+from dulwich.tests import (
+    SkipTest,
+    )
 from dulwich.web import (
 from dulwich.web import (
     make_wsgi_chain,
     make_wsgi_chain,
     HTTPGitApplication,
     HTTPGitApplication,

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

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

+ 7 - 2
dulwich/tests/test_blackbox.py

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

+ 19 - 0
dulwich/tests/test_client.py

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

+ 10 - 0
dulwich/tests/test_config.py

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

+ 4 - 0
dulwich/tests/test_diff_tree.py

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

+ 1 - 1
dulwich/tests/test_fastexport.py

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

+ 6 - 1
dulwich/tests/test_file.py

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

+ 6 - 0
dulwich/tests/test_grafts.py

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

+ 4 - 1
dulwich/tests/test_greenthreads.py

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

+ 18 - 20
dulwich/tests/test_index.py

@@ -48,8 +48,10 @@ from dulwich.objects import (
     )
     )
 from dulwich.repo import Repo
 from dulwich.repo import Repo
 from dulwich.tests import TestCase
 from dulwich.tests import TestCase
+from dulwich.tests.utils import skipIfPY3
 
 
 
 
+@skipIfPY3
 class IndexTestCase(TestCase):
 class IndexTestCase(TestCase):
 
 
     datadir = os.path.join(os.path.dirname(__file__), 'data/indexes')
     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))
         return Index(os.path.join(self.datadir, name))
 
 
 
 
+@skipIfPY3
 class SimpleIndexTestCase(IndexTestCase):
 class SimpleIndexTestCase(IndexTestCase):
 
 
     def test_len(self):
     def test_len(self):
@@ -86,6 +89,7 @@ class SimpleIndexTestCase(IndexTestCase):
         self.assertEqual('e69de29bb2d1d6434b8b29ae775ad8c2e48c5391', newsha)
         self.assertEqual('e69de29bb2d1d6434b8b29ae775ad8c2e48c5391', newsha)
 
 
 
 
+@skipIfPY3
 class SimpleIndexWriterTestCase(IndexTestCase):
 class SimpleIndexWriterTestCase(IndexTestCase):
 
 
     def setUp(self):
     def setUp(self):
@@ -101,18 +105,14 @@ class SimpleIndexWriterTestCase(IndexTestCase):
                     33188, 1000, 1000, 0,
                     33188, 1000, 1000, 0,
                     'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391', 0)]
                     'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391', 0)]
         filename = os.path.join(self.tempdir, 'test-simple-write-index')
         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)
             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)))
             self.assertEqual(entries, list(read_index(x)))
-        finally:
-            x.close()
 
 
 
 
+@skipIfPY3
 class ReadIndexDictTests(IndexTestCase):
 class ReadIndexDictTests(IndexTestCase):
 
 
     def setUp(self):
     def setUp(self):
@@ -128,18 +128,14 @@ class ReadIndexDictTests(IndexTestCase):
                     33188, 1000, 1000, 0,
                     33188, 1000, 1000, 0,
                     'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391', 0)}
                     'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391', 0)}
         filename = os.path.join(self.tempdir, 'test-simple-write-index')
         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)
             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))
             self.assertEqual(entries, read_index_dict(x))
-        finally:
-            x.close()
 
 
 
 
+@skipIfPY3
 class CommitTreeTests(TestCase):
 class CommitTreeTests(TestCase):
 
 
     def setUp(self):
     def setUp(self):
@@ -171,6 +167,7 @@ class CommitTreeTests(TestCase):
                           set(self.store._data.keys()))
                           set(self.store._data.keys()))
 
 
 
 
+@skipIfPY3
 class CleanupModeTests(TestCase):
 class CleanupModeTests(TestCase):
 
 
     def test_file(self):
     def test_file(self):
@@ -189,6 +186,7 @@ class CleanupModeTests(TestCase):
         self.assertEqual(0o160000, cleanup_mode(0o160744))
         self.assertEqual(0o160000, cleanup_mode(0o160744))
 
 
 
 
+@skipIfPY3
 class WriteCacheTimeTests(TestCase):
 class WriteCacheTimeTests(TestCase):
 
 
     def test_write_string(self):
     def test_write_string(self):
@@ -211,6 +209,7 @@ class WriteCacheTimeTests(TestCase):
         self.assertEqual(struct.pack(">LL", 434343, 21), f.getvalue())
         self.assertEqual(struct.pack(">LL", 434343, 21), f.getvalue())
 
 
 
 
+@skipIfPY3
 class IndexEntryFromStatTests(TestCase):
 class IndexEntryFromStatTests(TestCase):
 
 
     def test_simple(self):
     def test_simple(self):
@@ -249,6 +248,7 @@ class IndexEntryFromStatTests(TestCase):
             0))
             0))
 
 
 
 
+@skipIfPY3
 class BuildIndexTests(TestCase):
 class BuildIndexTests(TestCase):
 
 
     def assertReasonableIndexEntry(self, index_entry, mode, filesize, sha):
     def assertReasonableIndexEntry(self, index_entry, mode, filesize, sha):
@@ -260,11 +260,8 @@ class BuildIndexTests(TestCase):
         if symlink:
         if symlink:
             self.assertEqual(os.readlink(path), contents)
             self.assertEqual(os.readlink(path), contents)
         else:
         else:
-            f = open(path, 'rb')
-            try:
+            with open(path, 'rb') as f:
                 self.assertEqual(f.read(), contents)
                 self.assertEqual(f.read(), contents)
-            finally:
-                f.close()
 
 
     def test_empty(self):
     def test_empty(self):
         repo_dir = tempfile.mkdtemp()
         repo_dir = tempfile.mkdtemp()
@@ -349,6 +346,7 @@ class BuildIndexTests(TestCase):
             sorted(os.listdir(os.path.join(repo.path, 'c'))))
             sorted(os.listdir(os.path.join(repo.path, 'c'))))
 
 
 
 
+@skipIfPY3
 class GetUnstagedChangesTests(TestCase):
 class GetUnstagedChangesTests(TestCase):
 
 
     def test_get_unstaged_changes(self):
     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 (
 from dulwich.tests import (
     TestCase,
     TestCase,
     )
     )
+from dulwich.tests.utils import (
+    skipIfPY3,
+    )
 
 
 
 
+@skipIfPY3
 class TestLRUCache(TestCase):
 class TestLRUCache(TestCase):
     """Test that LRU cache properly keeps track of entries."""
     """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()))
         self.assertEqual([6, 7, 8, 9, 10, 11], sorted(cache.keys()))
 
 
 
 
+@skipIfPY3
 class TestLRUSizeCache(TestCase):
 class TestLRUSizeCache(TestCase):
 
 
     def test_basic_init(self):
     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 (
 from dulwich.tests.utils import (
     make_object,
     make_object,
     build_commit_graph,
     build_commit_graph,
+    skipIfPY3,
     )
     )
 
 
 
 
+@skipIfPY3
 class MissingObjectFinderTest(TestCase):
 class MissingObjectFinderTest(TestCase):
 
 
     def setUp(self):
     def setUp(self):
@@ -49,6 +51,7 @@ class MissingObjectFinderTest(TestCase):
             "some objects are not reported as missing: %s" % (expected, ))
             "some objects are not reported as missing: %s" % (expected, ))
 
 
 
 
+@skipIfPY3
 class MOFLinearRepoTest(MissingObjectFinderTest):
 class MOFLinearRepoTest(MissingObjectFinderTest):
 
 
     def setUp(self):
     def setUp(self):
@@ -108,6 +111,7 @@ class MOFLinearRepoTest(MissingObjectFinderTest):
         self.assertMissingMatch([self.cmt(3).id], [self.cmt(3).id], [])
         self.assertMissingMatch([self.cmt(3).id], [self.cmt(3).id], [])
 
 
 
 
+@skipIfPY3
 class MOFMergeForkRepoTest(MissingObjectFinderTest):
 class MOFMergeForkRepoTest(MissingObjectFinderTest):
     # 1 --- 2 --- 4 --- 6 --- 7
     # 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 (
 from dulwich.tests.utils import (
     make_object,
     make_object,
     build_pack,
     build_pack,
+    skipIfPY3,
     )
     )
 
 
 
 
@@ -194,6 +195,7 @@ class ObjectStoreTests(object):
         self.store.close()
         self.store.close()
 
 
 
 
+@skipIfPY3
 class MemoryObjectStoreTests(ObjectStoreTests, TestCase):
 class MemoryObjectStoreTests(ObjectStoreTests, TestCase):
 
 
     def setUp(self):
     def setUp(self):
@@ -227,6 +229,16 @@ class MemoryObjectStoreTests(ObjectStoreTests, TestCase):
                          o.get_raw(packed_blob_sha))
                          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):
 class PackBasedObjectStoreTests(ObjectStoreTests):
 
 
     def tearDown(self):
     def tearDown(self):
@@ -247,6 +259,7 @@ class PackBasedObjectStoreTests(ObjectStoreTests):
         self.assertEqual(0, self.store.pack_loose_objects())
         self.assertEqual(0, self.store.pack_loose_objects())
 
 
 
 
+@skipIfPY3
 class DiskObjectStoreTests(PackBasedObjectStoreTests, TestCase):
 class DiskObjectStoreTests(PackBasedObjectStoreTests, TestCase):
 
 
     def setUp(self):
     def setUp(self):
@@ -312,27 +325,36 @@ class DiskObjectStoreTests(PackBasedObjectStoreTests, TestCase):
 
 
     def test_add_thin_pack(self):
     def test_add_thin_pack(self):
         o = DiskObjectStore(self.store_dir)
         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:
         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:
         finally:
             o.close()
             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):
 class TreeLookupPathTests(TestCase):
 
 
     def setUp(self):
     def setUp(self):
@@ -376,39 +398,46 @@ class TreeLookupPathTests(TestCase):
 
 
 # TODO: MissingObjectFinderTests
 # TODO: MissingObjectFinderTests
 
 
+@skipIfPY3
 class ObjectStoreGraphWalkerTests(TestCase):
 class ObjectStoreGraphWalkerTests(TestCase):
 
 
     def get_walker(self, heads, parent_map):
     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):
     def test_empty(self):
         gw = self.get_walker([], {})
         gw = self.get_walker([], {})
         self.assertIs(None, next(gw))
         self.assertIs(None, next(gw))
-        gw.ack("aa" * 20)
+        gw.ack("a" * 40)
         self.assertIs(None, next(gw))
         self.assertIs(None, next(gw))
 
 
     def test_descends(self):
     def test_descends(self):
         gw = self.get_walker(["a"], {"a": ["b"], "b": []})
         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):
     def test_present(self):
         gw = self.get_walker(["a"], {"a": ["b"], "b": []})
         gw = self.get_walker(["a"], {"a": ["b"], "b": []})
-        gw.ack("a")
+        gw.ack("a" * 40)
         self.assertIs(None, next(gw))
         self.assertIs(None, next(gw))
 
 
     def test_parent_present(self):
     def test_parent_present(self):
         gw = self.get_walker(["a"], {"a": ["b"], "b": []})
         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))
         self.assertIs(None, next(gw))
 
 
     def test_child_ack_later(self):
     def test_child_ack_later(self):
         gw = self.get_walker(["a"], {"a": ["b"], "b": ["c"], "c": []})
         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))
         self.assertIs(None, next(gw))
 
 
     def test_only_once(self):
     def test_only_once(self):
@@ -430,18 +459,18 @@ class ObjectStoreGraphWalkerTests(TestCase):
         walk.append(next(gw))
         walk.append(next(gw))
         # A branch (a, c) or (b, d) may be done after 2 steps or 3 depending on
         # 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.
         # 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])
           gw.ack(walk[0])
           acked = True
           acked = True
 
 
         walk.append(next(gw))
         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))
         walk.append(next(gw))
         self.assertIs(None, 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,
     Commit,
     ShaFile,
     ShaFile,
     Tag,
     Tag,
+    TreeEntry,
     format_timezone,
     format_timezone,
     hex_to_sha,
     hex_to_sha,
     sha_to_hex,
     sha_to_hex,
@@ -47,7 +48,7 @@ from dulwich.objects import (
     check_hexsha,
     check_hexsha,
     check_identity,
     check_identity,
     parse_timezone,
     parse_timezone,
-    TreeEntry,
+    object_class,
     parse_tree,
     parse_tree,
     _parse_tree_py,
     _parse_tree_py,
     sorted_tree_items,
     sorted_tree_items,
@@ -61,6 +62,7 @@ from dulwich.tests.utils import (
     make_object,
     make_object,
     functest_builder,
     functest_builder,
     ext_functest_builder,
     ext_functest_builder,
+    skipIfPY3,
     )
     )
 
 
 a_sha = '6f670c0fb53f9463760b7295fbb814e965fb20c8'
 a_sha = '6f670c0fb53f9463760b7295fbb814e965fb20c8'
@@ -70,6 +72,7 @@ tree_sha = '70c190eb48fa8bbb50ddc692a17b44cb781af7f6'
 tag_sha = '71033db03a03c6a36721efcf1968dd8f8e0cf023'
 tag_sha = '71033db03a03c6a36721efcf1968dd8f8e0cf023'
 
 
 
 
+@skipIfPY3
 class TestHexToSha(TestCase):
 class TestHexToSha(TestCase):
 
 
     def test_simple(self):
     def test_simple(self):
@@ -79,6 +82,7 @@ class TestHexToSha(TestCase):
         self.assertEqual("abcd" * 10, sha_to_hex("\xab\xcd" * 10))
         self.assertEqual("abcd" * 10, sha_to_hex("\xab\xcd" * 10))
 
 
 
 
+@skipIfPY3
 class BlobReadTests(TestCase):
 class BlobReadTests(TestCase):
     """Test decompression of blobs"""
     """Test decompression of blobs"""
 
 
@@ -216,6 +220,7 @@ class BlobReadTests(TestCase):
         self.assertNotEqual(sha, c._make_sha())
         self.assertNotEqual(sha, c._make_sha())
 
 
 
 
+@skipIfPY3
 class ShaFileCheckTests(TestCase):
 class ShaFileCheckTests(TestCase):
 
 
     def assertCheckFails(self, cls, data):
     def assertCheckFails(self, cls, data):
@@ -245,6 +250,7 @@ small_buffer_zlib_object = (
  )
  )
 
 
 
 
+@skipIfPY3
 class ShaFileTests(TestCase):
 class ShaFileTests(TestCase):
 
 
     def test_deflated_smaller_window_buffer(self):
     def test_deflated_smaller_window_buffer(self):
@@ -256,6 +262,7 @@ class ShaFileTests(TestCase):
         self.assertEqual(sf.tagger, " <@localhost>")
         self.assertEqual(sf.tagger, " <@localhost>")
 
 
 
 
+@skipIfPY3
 class CommitSerializationTests(TestCase):
 class CommitSerializationTests(TestCase):
 
 
     def make_commit(self, **kwargs):
     def make_commit(self, **kwargs):
@@ -314,6 +321,52 @@ class CommitSerializationTests(TestCase):
         d._deserialize(c.as_raw_chunks())
         d._deserialize(c.as_raw_chunks())
         self.assertEqual(c, d)
         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):
     def test_serialize_mergetag(self):
         tag = make_object(
         tag = make_object(
             Tag, object=(Commit, "a38d6181ff27824c79fc7df825164a212eff6a3f"),
             Tag, object=(Commit, "a38d6181ff27824c79fc7df825164a212eff6a3f"),
@@ -426,6 +479,7 @@ Merge ../b
 
 
 default_committer = 'James Westby <jw+debian@jameswestby.net> 1174773719 +0000'
 default_committer = 'James Westby <jw+debian@jameswestby.net> 1174773719 +0000'
 
 
+@skipIfPY3
 class CommitParseTests(ShaFileCheckTests):
 class CommitParseTests(ShaFileCheckTests):
 
 
     def make_commit_lines(self,
     def make_commit_lines(self,
@@ -531,6 +585,50 @@ class CommitParseTests(ShaFileCheckTests):
             else:
             else:
                 self.assertCheckFails(Commit, text)
                 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 = {
 _TREE_ITEMS = {
   'a.c': (0o100755, 'd80c186a03f423a81b39df39dc87fd269736ca86'),
   'a.c': (0o100755, 'd80c186a03f423a81b39df39dc87fd269736ca86'),
@@ -545,6 +643,7 @@ _SORTED_TREE_ITEMS = [
   ]
   ]
 
 
 
 
+@skipIfPY3
 class TreeTests(ShaFileCheckTests):
 class TreeTests(ShaFileCheckTests):
 
 
     def test_add(self):
     def test_add(self):
@@ -691,6 +790,7 @@ class TreeTests(ShaFileCheckTests):
         self.assertEqual(set(["foo"]), set(t))
         self.assertEqual(set(["foo"]), set(t))
 
 
 
 
+@skipIfPY3
 class TagSerializeTests(TestCase):
 class TagSerializeTests(TestCase):
 
 
     def test_serialize_simple(self):
     def test_serialize_simple(self):
@@ -723,6 +823,7 @@ OK2XeQOiEeXtT76rV4t2WR4=
 """
 """
 
 
 
 
+@skipIfPY3
 class TagParseTests(ShaFileCheckTests):
 class TagParseTests(ShaFileCheckTests):
 
 
     def make_tag_lines(self,
     def make_tag_lines(self,
@@ -804,6 +905,7 @@ class TagParseTests(ShaFileCheckTests):
                 self.assertCheckFails(Tag, text)
                 self.assertCheckFails(Tag, text)
 
 
 
 
+@skipIfPY3
 class CheckTests(TestCase):
 class CheckTests(TestCase):
 
 
     def test_check_hexsha(self):
     def test_check_hexsha(self):
@@ -835,6 +937,7 @@ class CheckTests(TestCase):
                           "trailing characters")
                           "trailing characters")
 
 
 
 
+@skipIfPY3
 class TimezoneTests(TestCase):
 class TimezoneTests(TestCase):
 
 
     def test_parse_timezone_utc(self):
     def test_parse_timezone_utc(self):
@@ -878,3 +981,47 @@ class TimezoneTests(TestCase):
             (int(((7 * 60)) * 60), False), parse_timezone("+700"))
             (int(((7 * 60)) * 60), False), parse_timezone("+700"))
         self.assertEqual(
         self.assertEqual(
             (int(((7 * 60)) * 60), True), parse_timezone("--700"))
             (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 (
 from dulwich.tests.utils import (
     build_commit_graph,
     build_commit_graph,
+    skipIfPY3,
     )
     )
 
 
 
 
+@skipIfPY3
 class ParseObjectTests(TestCase):
 class ParseObjectTests(TestCase):
     """Test parse_object."""
     """Test parse_object."""
 
 
@@ -52,6 +54,7 @@ class ParseObjectTests(TestCase):
         self.assertEqual(b, parse_object(r, b.id))
         self.assertEqual(b, parse_object(r, b.id))
 
 
 
 
+@skipIfPY3
 class ParseCommitRangeTests(TestCase):
 class ParseCommitRangeTests(TestCase):
     """Test parse_commit_range."""
     """Test parse_commit_range."""
 
 

+ 41 - 29
dulwich/tests/test_pack.py

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

+ 7 - 1
dulwich/tests/test_patch.py

@@ -19,7 +19,6 @@
 """Tests for patch.py."""
 """Tests for patch.py."""
 
 
 from io import BytesIO
 from io import BytesIO
-from unittest import SkipTest
 
 
 from dulwich.objects import (
 from dulwich.objects import (
     Blob,
     Blob,
@@ -38,10 +37,15 @@ from dulwich.patch import (
     write_tree_diff,
     write_tree_diff,
     )
     )
 from dulwich.tests import (
 from dulwich.tests import (
+    SkipTest,
     TestCase,
     TestCase,
     )
     )
+from dulwich.tests.utils import (
+    skipIfPY3,
+    )
 
 
 
 
+@skipIfPY3
 class WriteCommitPatchTests(TestCase):
 class WriteCommitPatchTests(TestCase):
 
 
     def test_simple(self):
     def test_simple(self):
@@ -72,6 +76,7 @@ class WriteCommitPatchTests(TestCase):
             self.assertEqual(lines[8], " 0 files changed\n")
             self.assertEqual(lines[8], " 0 files changed\n")
 
 
 
 
+@skipIfPY3
 class ReadGitAmPatch(TestCase):
 class ReadGitAmPatch(TestCase):
 
 
     def test_extract(self):
     def test_extract(self):
@@ -201,6 +206,7 @@ More help   : https://help.launchpad.net/ListHelp
         self.assertEqual(None, version)
         self.assertEqual(None, version)
 
 
 
 
+@skipIfPY3
 class DiffTests(TestCase):
 class DiffTests(TestCase):
     """Tests for write_blob_diff and write_tree_diff."""
     """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 (
 from dulwich.tests.utils import (
     build_commit_graph,
     build_commit_graph,
     make_object,
     make_object,
+    skipIfPY3,
     )
     )
 
 
 
 
@@ -51,6 +52,7 @@ class PorcelainTestCase(TestCase):
         self.repo = Repo.init(repo_dir)
         self.repo = Repo.init(repo_dir)
 
 
 
 
+@skipIfPY3
 class ArchiveTests(PorcelainTestCase):
 class ArchiveTests(PorcelainTestCase):
     """Tests for the archive command."""
     """Tests for the archive command."""
 
 
@@ -69,6 +71,7 @@ class ArchiveTests(PorcelainTestCase):
         self.assertEqual([], tf.getnames())
         self.assertEqual([], tf.getnames())
 
 
 
 
+@skipIfPY3
 class UpdateServerInfoTests(PorcelainTestCase):
 class UpdateServerInfoTests(PorcelainTestCase):
 
 
     def test_simple(self):
     def test_simple(self):
@@ -80,6 +83,7 @@ class UpdateServerInfoTests(PorcelainTestCase):
             'info', 'refs')))
             'info', 'refs')))
 
 
 
 
+@skipIfPY3
 class CommitTests(PorcelainTestCase):
 class CommitTests(PorcelainTestCase):
 
 
     def test_custom_author(self):
     def test_custom_author(self):
@@ -92,6 +96,7 @@ class CommitTests(PorcelainTestCase):
         self.assertEqual(len(sha), 40)
         self.assertEqual(len(sha), 40)
 
 
 
 
+@skipIfPY3
 class CloneTests(PorcelainTestCase):
 class CloneTests(PorcelainTestCase):
 
 
     def test_simple_local(self):
     def test_simple_local(self):
@@ -168,6 +173,7 @@ class CloneTests(PorcelainTestCase):
             target_path, checkout=True, bare=True, outstream=outstream)
             target_path, checkout=True, bare=True, outstream=outstream)
 
 
 
 
+@skipIfPY3
 class InitTests(TestCase):
 class InitTests(TestCase):
 
 
     def test_non_bare(self):
     def test_non_bare(self):
@@ -181,6 +187,7 @@ class InitTests(TestCase):
         porcelain.init(repo_dir, bare=True)
         porcelain.init(repo_dir, bare=True)
 
 
 
 
+@skipIfPY3
 class AddTests(PorcelainTestCase):
 class AddTests(PorcelainTestCase):
 
 
     def test_add_default_paths(self):
     def test_add_default_paths(self):
@@ -202,7 +209,7 @@ class AddTests(PorcelainTestCase):
 
 
         # Check that foo was added and nothing in .git was modified
         # Check that foo was added and nothing in .git was modified
         index = self.repo.open_index()
         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):
     def test_add_file(self):
         with open(os.path.join(self.repo.path, 'foo'), 'w') as f:
         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"])
         porcelain.add(self.repo.path, paths=["foo"])
 
 
 
 
+@skipIfPY3
 class RemoveTests(PorcelainTestCase):
 class RemoveTests(PorcelainTestCase):
 
 
     def test_remove_file(self):
     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")
             f.write("BAR")
-        finally:
-            f.close()
         porcelain.add(self.repo.path, paths=["foo"])
         porcelain.add(self.repo.path, paths=["foo"])
         porcelain.rm(self.repo.path, paths=["foo"])
         porcelain.rm(self.repo.path, paths=["foo"])
 
 
 
 
+@skipIfPY3
 class LogTests(PorcelainTestCase):
 class LogTests(PorcelainTestCase):
 
 
     def test_simple(self):
     def test_simple(self):
@@ -241,6 +247,7 @@ class LogTests(PorcelainTestCase):
         self.assertEqual(1, outstream.getvalue().count("-" * 50))
         self.assertEqual(1, outstream.getvalue().count("-" * 50))
 
 
 
 
+@skipIfPY3
 class ShowTests(PorcelainTestCase):
 class ShowTests(PorcelainTestCase):
 
 
     def test_nolist(self):
     def test_nolist(self):
@@ -267,6 +274,7 @@ class ShowTests(PorcelainTestCase):
         self.assertEqual(outstream.getvalue(), "The Foo\n")
         self.assertEqual(outstream.getvalue(), "The Foo\n")
 
 
 
 
+@skipIfPY3
 class SymbolicRefTests(PorcelainTestCase):
 class SymbolicRefTests(PorcelainTestCase):
 
 
     def test_set_wrong_symbolic_ref(self):
     def test_set_wrong_symbolic_ref(self):
@@ -309,6 +317,7 @@ class SymbolicRefTests(PorcelainTestCase):
         self.assertEqual(new_ref, b'ref: refs/heads/develop\n')
         self.assertEqual(new_ref, b'ref: refs/heads/develop\n')
 
 
 
 
+@skipIfPY3
 class DiffTreeTests(PorcelainTestCase):
 class DiffTreeTests(PorcelainTestCase):
 
 
     def test_empty(self):
     def test_empty(self):
@@ -320,6 +329,7 @@ class DiffTreeTests(PorcelainTestCase):
         self.assertEqual(outstream.getvalue(), "")
         self.assertEqual(outstream.getvalue(), "")
 
 
 
 
+@skipIfPY3
 class CommitTreeTests(PorcelainTestCase):
 class CommitTreeTests(PorcelainTestCase):
 
 
     def test_simple(self):
     def test_simple(self):
@@ -339,6 +349,7 @@ class CommitTreeTests(PorcelainTestCase):
         self.assertEqual(len(sha), 40)
         self.assertEqual(len(sha), 40)
 
 
 
 
+@skipIfPY3
 class RevListTests(PorcelainTestCase):
 class RevListTests(PorcelainTestCase):
 
 
     def test_simple(self):
     def test_simple(self):
@@ -352,15 +363,16 @@ class RevListTests(PorcelainTestCase):
             outstream.getvalue())
             outstream.getvalue())
 
 
 
 
-class TagTests(PorcelainTestCase):
+@skipIfPY3
+class TagCreateTests(PorcelainTestCase):
 
 
     def test_annotated(self):
     def test_annotated(self):
         c1, c2, c3 = build_commit_graph(self.repo.object_store, [[1], [2, 1],
         c1, c2, c3 = build_commit_graph(self.repo.object_store, [[1], [2, 1],
             [3, 1, 2]])
             [3, 1, 2]])
         self.repo.refs["HEAD"] = c3.id
         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")
         tags = self.repo.refs.as_dict("refs/tags")
         self.assertEqual(tags.keys(), ["tryme"])
         self.assertEqual(tags.keys(), ["tryme"])
@@ -374,7 +386,7 @@ class TagTests(PorcelainTestCase):
             [3, 1, 2]])
             [3, 1, 2]])
         self.repo.refs["HEAD"] = c3.id
         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")
         tags = self.repo.refs.as_dict("refs/tags")
         self.assertEqual(tags.keys(), ["tryme"])
         self.assertEqual(tags.keys(), ["tryme"])
@@ -382,38 +394,46 @@ class TagTests(PorcelainTestCase):
         self.assertEqual(tags.values(), [self.repo.head()])
         self.assertEqual(tags.values(), [self.repo.head()])
 
 
 
 
-class ListTagsTests(PorcelainTestCase):
+@skipIfPY3
+class TagListTests(PorcelainTestCase):
 
 
     def test_empty(self):
     def test_empty(self):
-        tags = porcelain.list_tags(self.repo.path)
+        tags = porcelain.tag_list(self.repo.path)
         self.assertEqual([], tags)
         self.assertEqual([], tags)
 
 
     def test_simple(self):
     def test_simple(self):
         self.repo.refs["refs/tags/foo"] = "aa" * 20
         self.repo.refs["refs/tags/foo"] = "aa" * 20
         self.repo.refs["refs/tags/bar/bla"] = "bb" * 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)
         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):
 class ResetTests(PorcelainTestCase):
 
 
     def test_hard_head(self):
     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")
             f.write("BAR")
-        finally:
-            f.close()
         porcelain.add(self.repo.path, paths=["foo"])
         porcelain.add(self.repo.path, paths=["foo"])
         porcelain.commit(self.repo.path, message="Some message",
         porcelain.commit(self.repo.path, message="Some message",
                 committer="Jane <jane@example.com>",
                 committer="Jane <jane@example.com>",
                 author="John <john@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")
             f.write("OOH")
-        finally:
-            f.close()
 
 
         porcelain.reset(self.repo, "hard", "HEAD")
         porcelain.reset(self.repo, "hard", "HEAD")
 
 
@@ -425,6 +445,7 @@ class ResetTests(PorcelainTestCase):
         self.assertEqual([], changes)
         self.assertEqual([], changes)
 
 
 
 
+@skipIfPY3
 class PushTests(PorcelainTestCase):
 class PushTests(PorcelainTestCase):
 
 
     def test_simple(self):
     def test_simple(self):
@@ -469,6 +490,7 @@ class PushTests(PorcelainTestCase):
         self.assertEqual(os.path.basename(fullpath), change.new.path)
         self.assertEqual(os.path.basename(fullpath), change.new.path)
 
 
 
 
+@skipIfPY3
 class PullTests(PorcelainTestCase):
 class PullTests(PorcelainTestCase):
 
 
     def test_simple(self):
     def test_simple(self):
@@ -502,6 +524,7 @@ class PullTests(PorcelainTestCase):
         self.assertEqual(r['HEAD'].id, self.repo['HEAD'].id)
         self.assertEqual(r['HEAD'].id, self.repo['HEAD'].id)
 
 
 
 
+@skipIfPY3
 class StatusTests(PorcelainTestCase):
 class StatusTests(PorcelainTestCase):
 
 
     def test_status(self):
     def test_status(self):
@@ -597,3 +620,121 @@ class StatusTests(PorcelainTestCase):
 
 
 
 
 # TODO(jelmer): Add test for dulwich.porcelain.daemon
 # 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,
     BufferedPktLineWriter,
     )
     )
 from dulwich.tests import TestCase
 from dulwich.tests import TestCase
+from dulwich.tests.utils import skipIfPY3
 
 
 
 
 class BaseProtocolTests(object):
 class BaseProtocolTests(object):
@@ -106,6 +107,7 @@ class BaseProtocolTests(object):
         self.assertRaises(AssertionError, self.proto.read_cmd)
         self.assertRaises(AssertionError, self.proto.read_cmd)
 
 
 
 
+@skipIfPY3
 class ProtocolTests(BaseProtocolTests, TestCase):
 class ProtocolTests(BaseProtocolTests, TestCase):
 
 
     def setUp(self):
     def setUp(self):
@@ -133,6 +135,7 @@ class ReceivableBytesIO(BytesIO):
         return self.read(size - 1)
         return self.read(size - 1)
 
 
 
 
+@skipIfPY3
 class ReceivableProtocolTests(BaseProtocolTests, TestCase):
 class ReceivableProtocolTests(BaseProtocolTests, TestCase):
 
 
     def setUp(self):
     def setUp(self):
@@ -204,6 +207,7 @@ class ReceivableProtocolTests(BaseProtocolTests, TestCase):
         self.assertEqual(all_data, data)
         self.assertEqual(all_data, data)
 
 
 
 
+@skipIfPY3
 class CapabilitiesTestCase(TestCase):
 class CapabilitiesTestCase(TestCase):
 
 
     def test_plain(self):
     def test_plain(self):
@@ -233,6 +237,7 @@ class CapabilitiesTestCase(TestCase):
                                     'multi_ack_detailed']))
                                     'multi_ack_detailed']))
 
 
 
 
+@skipIfPY3
 class BufferedPktLineWriterTests(TestCase):
 class BufferedPktLineWriterTests(TestCase):
 
 
     def setUp(self):
     def setUp(self):
@@ -288,6 +293,7 @@ class BufferedPktLineWriterTests(TestCase):
         self.assertOutputEquals('0005z')
         self.assertOutputEquals('0005z')
 
 
 
 
+@skipIfPY3
 class PktLineParserTests(TestCase):
 class PktLineParserTests(TestCase):
 
 
     def test_none(self):
     def test_none(self):

+ 5 - 1
dulwich/tests/test_refs.py

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

+ 14 - 39
dulwich/tests/test_repository.py

@@ -42,11 +42,13 @@ from dulwich.tests.utils import (
     open_repo,
     open_repo,
     tear_down_repo,
     tear_down_repo,
     setup_warning_catcher,
     setup_warning_catcher,
+    skipIfPY3,
     )
     )
 
 
 missing_sha = 'b91fa4d900e17e99b433218e988c4eb4a3e9a097'
 missing_sha = 'b91fa4d900e17e99b433218e988c4eb4a3e9a097'
 
 
 
 
+@skipIfPY3
 class CreateRepositoryTests(TestCase):
 class CreateRepositoryTests(TestCase):
 
 
     def assertFileContentsEqual(self, expected, repo, path):
     def assertFileContentsEqual(self, expected, repo, path):
@@ -54,10 +56,8 @@ class CreateRepositoryTests(TestCase):
         if not f:
         if not f:
             self.assertEqual(expected, None)
             self.assertEqual(expected, None)
         else:
         else:
-            try:
+            with f:
                 self.assertEqual(expected, f.read())
                 self.assertEqual(expected, f.read())
-            finally:
-                f.close()
 
 
     def _check_repo_contents(self, repo, expect_bare):
     def _check_repo_contents(self, repo, expect_bare):
         self.assertEqual(expect_bare, repo.bare)
         self.assertEqual(expect_bare, repo.bare)
@@ -87,6 +87,7 @@ class CreateRepositoryTests(TestCase):
         self._check_repo_contents(repo, True)
         self._check_repo_contents(repo, True)
 
 
 
 
+@skipIfPY3
 class RepositoryTests(TestCase):
 class RepositoryTests(TestCase):
 
 
     def setUp(self):
     def setUp(self):
@@ -173,11 +174,8 @@ class RepositoryTests(TestCase):
 
 
     def test_get_description(self):
     def test_get_description(self):
         r = self._repo = open_repo('a.git')
         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")
             f.write("Some description")
-        finally:
-            f.close()
         self.assertEqual("Some description", r.get_description())
         self.assertEqual("Some description", r.get_description())
 
 
     def test_set_description(self):
     def test_set_description(self):
@@ -376,11 +374,8 @@ exit 0
 
 
         pre_commit = os.path.join(r.controldir(), 'hooks', 'pre-commit')
         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)
             f.write(pre_commit_fail)
-        finally:
-            f.close()
         os.chmod(pre_commit, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
         os.chmod(pre_commit, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
 
 
         self.assertRaises(errors.CommitError, r.do_commit, 'failed commit',
         self.assertRaises(errors.CommitError, r.do_commit, 'failed commit',
@@ -389,11 +384,8 @@ exit 0
                           commit_timestamp=12345, commit_timezone=0,
                           commit_timestamp=12345, commit_timezone=0,
                           author_timestamp=12345, author_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)
             f.write(pre_commit_success)
-        finally:
-            f.close()
         os.chmod(pre_commit, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
         os.chmod(pre_commit, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
 
 
         commit_sha = r.do_commit(
         commit_sha = r.do_commit(
@@ -422,11 +414,8 @@ exit 0
 
 
         commit_msg = os.path.join(r.controldir(), 'hooks', 'commit-msg')
         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)
             f.write(commit_msg_fail)
-        finally:
-            f.close()
         os.chmod(commit_msg, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
         os.chmod(commit_msg, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
 
 
         self.assertRaises(errors.CommitError, r.do_commit, 'failed commit',
         self.assertRaises(errors.CommitError, r.do_commit, 'failed commit',
@@ -435,11 +424,8 @@ exit 0
                           commit_timestamp=12345, commit_timezone=0,
                           commit_timestamp=12345, commit_timezone=0,
                           author_timestamp=12345, author_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)
             f.write(commit_msg_success)
-        finally:
-            f.close()
         os.chmod(commit_msg, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
         os.chmod(commit_msg, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
 
 
         commit_sha = r.do_commit(
         commit_sha = r.do_commit(
@@ -473,11 +459,8 @@ rm %(file)s
 
 
         post_commit = os.path.join(r.controldir(), 'hooks', 'post-commit')
         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)
             f.write(post_commit_msg)
-        finally:
-            f.close()
         os.chmod(post_commit, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
         os.chmod(post_commit, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
 
 
         commit_sha = r.do_commit(
         commit_sha = r.do_commit(
@@ -493,11 +476,8 @@ rm %(file)s
         post_commit_msg_fail = """#!/bin/sh
         post_commit_msg_fail = """#!/bin/sh
 exit 1
 exit 1
 """
 """
-        f = open(post_commit, 'wb')
-        try:
+        with open(post_commit, 'wb') as f:
             f.write(post_commit_msg_fail)
             f.write(post_commit_msg_fail)
-        finally:
-            f.close()
         os.chmod(post_commit, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
         os.chmod(post_commit, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
 
 
         warnings.simplefilter("always", UserWarning)
         warnings.simplefilter("always", UserWarning)
@@ -517,6 +497,7 @@ exit 1
         self.assertEqual([commit_sha], r[commit_sha2].parents)
         self.assertEqual([commit_sha], r[commit_sha2].parents)
 
 
 
 
+@skipIfPY3
 class BuildRepoTests(TestCase):
 class BuildRepoTests(TestCase):
     """Tests that build on-disk repos from scratch.
     """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.assertEqual('ref: refs/heads/master', r.refs.read_ref('HEAD'))
         self.assertRaises(KeyError, lambda: r.refs['refs/heads/master'])
         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')
             f.write('file contents')
-        finally:
-            f.close()
         r.stage(['a'])
         r.stage(['a'])
         commit_sha = r.do_commit('msg',
         commit_sha = r.do_commit('msg',
                                  committer='Test Committer <test@nodomain.com>',
                                  committer='Test Committer <test@nodomain.com>',
@@ -562,11 +540,8 @@ class BuildRepoTests(TestCase):
 
 
     def test_commit_modified(self):
     def test_commit_modified(self):
         r = self._repo
         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')
             f.write('new contents')
-        finally:
-            f.close()
         os.symlink('a', os.path.join(self._repo_dir, 'b'))
         os.symlink('a', os.path.join(self._repo_dir, 'b'))
         r.stage(['a', 'b'])
         r.stage(['a', 'b'])
         commit_sha = r.do_commit('modified a',
         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 (
 from dulwich.tests.utils import (
     make_commit,
     make_commit,
     make_object,
     make_object,
+    skipIfPY3,
     )
     )
 from dulwich.protocol import (
 from dulwich.protocol import (
     ZERO_SHA,
     ZERO_SHA,
@@ -159,6 +160,7 @@ class HandlerTestCase(TestCase):
         self.assertFalse(self._handler.has_capability('capxxx'))
         self.assertFalse(self._handler.has_capability('capxxx'))
 
 
 
 
+@skipIfPY3
 class UploadPackHandlerTestCase(TestCase):
 class UploadPackHandlerTestCase(TestCase):
 
 
     def setUp(self):
     def setUp(self):
@@ -212,6 +214,7 @@ class UploadPackHandlerTestCase(TestCase):
         self.assertEqual({}, self._handler.get_tagged(refs, repo=self._repo))
         self.assertEqual({}, self._handler.get_tagged(refs, repo=self._repo))
 
 
 
 
+@skipIfPY3
 class FindShallowTests(TestCase):
 class FindShallowTests(TestCase):
 
 
     def setUp(self):
     def setUp(self):
@@ -292,6 +295,7 @@ class TestUploadPackHandler(UploadPackHandler):
     def required_capabilities(self):
     def required_capabilities(self):
         return ()
         return ()
 
 
+@skipIfPY3
 class ReceivePackHandlerTestCase(TestCase):
 class ReceivePackHandlerTestCase(TestCase):
 
 
     def setUp(self):
     def setUp(self):
@@ -314,6 +318,7 @@ class ReceivePackHandlerTestCase(TestCase):
         self.assertEqual(status[1][1], 'ok')
         self.assertEqual(status[1][1], 'ok')
 
 
 
 
+@skipIfPY3
 class ProtocolGraphWalkerEmptyTestCase(TestCase):
 class ProtocolGraphWalkerEmptyTestCase(TestCase):
     def setUp(self):
     def setUp(self):
         super(ProtocolGraphWalkerEmptyTestCase, self).setUp()
         super(ProtocolGraphWalkerEmptyTestCase, self).setUp()
@@ -335,6 +340,7 @@ class ProtocolGraphWalkerEmptyTestCase(TestCase):
 
 
 
 
 
 
+@skipIfPY3
 class ProtocolGraphWalkerTestCase(TestCase):
 class ProtocolGraphWalkerTestCase(TestCase):
 
 
     def setUp(self):
     def setUp(self):
@@ -356,20 +362,28 @@ class ProtocolGraphWalkerTestCase(TestCase):
             TestUploadPackHandler(backend, ['/', 'host=lolcats'], TestProto()),
             TestUploadPackHandler(backend, ['/', 'host=lolcats'], TestProto()),
             self._repo.object_store, self._repo.get_peeled)
             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
         # 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):
     def test_all_wants_satisfied(self):
         self._walker.set_wants([FOUR, FIVE])
         self._walker.set_wants([FOUR, FIVE])
@@ -507,6 +521,7 @@ class ProtocolGraphWalkerTestCase(TestCase):
           ])
           ])
 
 
 
 
+@skipIfPY3
 class TestProtocolGraphWalker(object):
 class TestProtocolGraphWalker(object):
 
 
     def __init__(self):
     def __init__(self):
@@ -537,6 +552,7 @@ class TestProtocolGraphWalker(object):
         return self.acks.pop(0)
         return self.acks.pop(0)
 
 
 
 
+@skipIfPY3
 class AckGraphWalkerImplTestCase(TestCase):
 class AckGraphWalkerImplTestCase(TestCase):
     """Base setup and asserts for AckGraphWalker tests."""
     """Base setup and asserts for AckGraphWalker tests."""
 
 
@@ -569,6 +585,7 @@ class AckGraphWalkerImplTestCase(TestCase):
         self.assertEqual(sha, next(self._impl))
         self.assertEqual(sha, next(self._impl))
 
 
 
 
+@skipIfPY3
 class SingleAckGraphWalkerImplTestCase(AckGraphWalkerImplTestCase):
 class SingleAckGraphWalkerImplTestCase(AckGraphWalkerImplTestCase):
 
 
     impl_cls = SingleAckGraphWalkerImpl
     impl_cls = SingleAckGraphWalkerImpl
@@ -637,6 +654,7 @@ class SingleAckGraphWalkerImplTestCase(AckGraphWalkerImplTestCase):
         self.assertNak()
         self.assertNak()
 
 
 
 
+@skipIfPY3
 class MultiAckGraphWalkerImplTestCase(AckGraphWalkerImplTestCase):
 class MultiAckGraphWalkerImplTestCase(AckGraphWalkerImplTestCase):
 
 
     impl_cls = MultiAckGraphWalkerImpl
     impl_cls = MultiAckGraphWalkerImpl
@@ -711,6 +729,7 @@ class MultiAckGraphWalkerImplTestCase(AckGraphWalkerImplTestCase):
         self.assertNak()
         self.assertNak()
 
 
 
 
+@skipIfPY3
 class MultiAckDetailedGraphWalkerImplTestCase(AckGraphWalkerImplTestCase):
 class MultiAckDetailedGraphWalkerImplTestCase(AckGraphWalkerImplTestCase):
 
 
     impl_cls = MultiAckDetailedGraphWalkerImpl
     impl_cls = MultiAckDetailedGraphWalkerImpl
@@ -824,6 +843,7 @@ class MultiAckDetailedGraphWalkerImplTestCase(AckGraphWalkerImplTestCase):
         self.assertNak()
         self.assertNak()
 
 
 
 
+@skipIfPY3
 class FileSystemBackendTests(TestCase):
 class FileSystemBackendTests(TestCase):
     """Tests for FileSystemBackend."""
     """Tests for FileSystemBackend."""
 
 
@@ -839,7 +859,7 @@ class FileSystemBackendTests(TestCase):
 
 
     def test_absolute(self):
     def test_absolute(self):
         repo = self.backend.open_repository(self.path)
         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):
     def test_child(self):
         self.assertRaises(NotGitRepository,
         self.assertRaises(NotGitRepository,
@@ -852,6 +872,7 @@ class FileSystemBackendTests(TestCase):
                           lambda: backend.open_repository('/ups'))
                           lambda: backend.open_repository('/ups'))
 
 
 
 
+@skipIfPY3
 class DictBackendTests(TestCase):
 class DictBackendTests(TestCase):
     """Tests for DictBackend."""
     """Tests for DictBackend."""
 
 
@@ -869,6 +890,7 @@ class DictBackendTests(TestCase):
                           lambda: backend.open_repository('/ups'))
                           lambda: backend.open_repository('/ups'))
 
 
 
 
+@skipIfPY3
 class ServeCommandTests(TestCase):
 class ServeCommandTests(TestCase):
     """Tests for serve_command."""
     """Tests for serve_command."""
 
 
@@ -894,6 +916,7 @@ class ServeCommandTests(TestCase):
         self.assertEqual(0, exitcode)
         self.assertEqual(0, exitcode)
 
 
 
 
+@skipIfPY3
 class UpdateServerInfoTests(TestCase):
 class UpdateServerInfoTests(TestCase):
     """Tests for update_server_info."""
     """Tests for update_server_info."""
 
 

+ 2 - 0
dulwich/tests/test_walk.py

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

+ 5 - 0
dulwich/tests/test_web.py

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

+ 10 - 4
dulwich/tests/utils.py

@@ -23,12 +23,11 @@
 import datetime
 import datetime
 import os
 import os
 import shutil
 import shutil
+import sys
 import tempfile
 import tempfile
 import time
 import time
 import types
 import types
-from unittest import (
-    SkipTest,
-    )
+
 import warnings
 import warnings
 
 
 from dulwich.index import (
 from dulwich.index import (
@@ -49,6 +48,11 @@ from dulwich.pack import (
     create_delta,
     create_delta,
     )
     )
 from dulwich.repo import Repo
 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.
 # Plain files are very frequently used in tests, so let the mode be very short.
 F = 0o100644  # Shorthand mode for Files.
 F = 0o100644  # Shorthand mode for Files.
@@ -98,7 +102,7 @@ def make_object(cls, **attrs):
         pass
         pass
 
 
     obj = TestObject()
     obj = TestObject()
-    for name, value in attrs.iteritems():
+    for name, value in attrs.items():
         if name == 'id':
         if name == 'id':
             # id property is read-only, so we overwrite sha instead.
             # id property is read-only, so we overwrite sha instead.
             sha = FixedSha(value)
             sha = FixedSha(value)
@@ -328,3 +332,5 @@ def setup_warning_catcher():
         warnings.showwarning = original_showwarning
         warnings.showwarning = original_showwarning
 
 
     return caught_warnings, restore_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 re
 import sys
 import sys
 import time
 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 import log_utils
 from dulwich.protocol import (
 from dulwich.protocol import (
@@ -408,89 +419,83 @@ def make_wsgi_chain(*args, **kwargs):
     return wrapped_app
     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',
             logger.exception('Exception happened during processing of request',
                              exc_info=exc_info)
                              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__':
 if __name__ == '__main__':

+ 22 - 12
setup.py

@@ -4,13 +4,11 @@
 
 
 try:
 try:
     from setuptools import setup, Extension
     from setuptools import setup, Extension
-    has_setuptools = True
 except ImportError:
 except ImportError:
     from distutils.core import setup, Extension
     from distutils.core import setup, Extension
-    has_setuptools = False
 from distutils.core import Distribution
 from distutils.core import Distribution
 
 
-dulwich_version_string = '0.9.7'
+dulwich_version_string = '0.9.8'
 
 
 include_dirs = []
 include_dirs = []
 # Windows MSVC support
 # Windows MSVC support
@@ -19,7 +17,6 @@ import sys
 if sys.platform == 'win32':
 if sys.platform == 'win32':
     include_dirs.append('dulwich')
     include_dirs.append('dulwich')
 
 
-
 class DulwichDistribution(Distribution):
 class DulwichDistribution(Distribution):
 
 
     def is_pure(self):
     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:
         if l.startswith('Xcode') and int(l.split()[1].split('.')[0]) >= 4:
             os.environ['ARCHFLAGS'] = ''
             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',
 setup(name='dulwich',
       description='Python Git Library',
       description='Python Git Library',
@@ -73,7 +76,15 @@ setup(name='dulwich',
       in the particular Monty Python sketch.
       in the particular Monty Python sketch.
       """,
       """,
       packages=['dulwich', 'dulwich.tests', 'dulwich.tests.compat', 'dulwich.contrib'],
       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=[
       ext_modules=[
           Extension('dulwich._objects', ['dulwich/_objects.c'],
           Extension('dulwich._objects', ['dulwich/_objects.c'],
                     include_dirs=include_dirs),
                     include_dirs=include_dirs),
@@ -82,9 +93,8 @@ setup(name='dulwich',
           Extension('dulwich._diff_tree', ['dulwich/_diff_tree.c'],
           Extension('dulwich._diff_tree', ['dulwich/_diff_tree.c'],
               include_dirs=include_dirs),
               include_dirs=include_dirs),
       ],
       ],
+      test_suite='dulwich.tests.test_suite',
+      tests_require=tests_require,
       distclass=DulwichDistribution,
       distclass=DulwichDistribution,
       include_package_data=True,
       include_package_data=True,
-      use_2to3=True,
-      convert_2to3_doctests=['../docs/*', '../docs/tutorial/*', ],
-      **setup_kwargs
       )
       )