Procházet zdrojové kódy

Merge branch 'master' of git://git.samba.org/jelmer/dulwich into experimental

Conflicts:
	.travis.yml
	setup.py
Jelmer Vernooij před 10 roky
rodič
revize
98d0526f5d
67 změnil soubory, kde provedl 2536 přidání a 2498 odebrání
  1. 16 0
      .travis.yml
  2. 7 0
      NEWS
  3. 0 26
      PKG-INFO
  4. 14 0
      appveyor.yml
  5. 9 8
      bin/dulwich
  6. 26 25
      docs/tutorial/object-store.txt
  7. 12 11
      docs/tutorial/remote.txt
  8. 9 8
      docs/tutorial/repo.txt
  9. 0 26
      dulwich.egg-info/PKG-INFO
  10. 0 180
      dulwich.egg-info/SOURCES.txt
  11. 0 1
      dulwich.egg-info/dependency_links.txt
  12. 0 1
      dulwich.egg-info/top_level.txt
  13. 1 1
      dulwich/__init__.py
  14. 105 84
      dulwich/client.py
  15. 71 54
      dulwich/config.py
  16. 1 1
      dulwich/contrib/swift.py
  17. 3 3
      dulwich/contrib/test_swift.py
  18. 6 14
      dulwich/diff_tree.py
  19. 1 1
      dulwich/file.py
  20. 1 1
      dulwich/greenthreads.py
  21. 3 3
      dulwich/hooks.py
  22. 37 22
      dulwich/index.py
  23. 40 33
      dulwich/object_store.py
  24. 17 15
      dulwich/objects.py
  25. 4 0
      dulwich/objectspec.py
  26. 87 84
      dulwich/pack.py
  27. 48 53
      dulwich/patch.py
  28. 72 48
      dulwich/porcelain.py
  29. 45 19
      dulwich/protocol.py
  30. 16 18
      dulwich/refs.py
  31. 87 71
      dulwich/repo.py
  32. 132 93
      dulwich/server.py
  33. 2 1
      dulwich/tests/__init__.py
  34. 12 0
      dulwich/tests/compat/server_utils.py
  35. 44 35
      dulwich/tests/compat/test_client.py
  36. 11 8
      dulwich/tests/compat/test_pack.py
  37. 6 8
      dulwich/tests/compat/test_repository.py
  38. 4 1
      dulwich/tests/compat/test_server.py
  39. 6 0
      dulwich/tests/compat/test_web.py
  40. 2 2
      dulwich/tests/compat/utils.py
  41. 7 12
      dulwich/tests/test_blackbox.py
  42. 117 130
      dulwich/tests/test_client.py
  43. 98 108
      dulwich/tests/test_config.py
  44. 8 11
      dulwich/tests/test_diff_tree.py
  45. 14 12
      dulwich/tests/test_file.py
  46. 22 28
      dulwich/tests/test_grafts.py
  47. 2 6
      dulwich/tests/test_greenthreads.py
  48. 30 20
      dulwich/tests/test_hooks.py
  49. 65 77
      dulwich/tests/test_index.py
  50. 26 29
      dulwich/tests/test_missing_obj_finder.py
  51. 112 111
      dulwich/tests/test_object_store.py
  52. 73 6
      dulwich/tests/test_objects.py
  53. 1 4
      dulwich/tests/test_objectspec.py
  54. 197 204
      dulwich/tests/test_pack.py
  55. 167 192
      dulwich/tests/test_porcelain.py
  56. 85 87
      dulwich/tests/test_protocol.py
  57. 10 12
      dulwich/tests/test_refs.py
  58. 312 235
      dulwich/tests/test_repository.py
  59. 138 158
      dulwich/tests/test_server.py
  60. 10 10
      dulwich/tests/test_utils.py
  61. 0 2
      dulwich/tests/test_walk.py
  62. 54 60
      dulwich/tests/test_web.py
  63. 8 6
      dulwich/tests/utils.py
  64. 2 0
      dulwich/walk.py
  65. 4 4
      dulwich/web.py
  66. 0 5
      setup.cfg
  67. 17 10
      setup.py

+ 16 - 0
.travis.yml

@@ -0,0 +1,16 @@
+language: python
+# Workaround to make 2.7 use system site packages, and 2.6 and 3.4 not use system
+# site packages.
+# https://github.com/travis-ci/travis-ci/issues/2219#issuecomment-41804942
+python:
+- "2.6"
+- "2.7_with_system_site_packages"
+- "3.4"
+- "pypy"
+script:
+  - PYTHONHASHSEED=random python setup.py test
+  - make check-noextensions
+install:
+  - sudo apt-get update
+  - sudo apt-get install -qq git python-setuptools python-gevent python-fastimport python-mock
+  - if [[ $TRAVIS_PYTHON_VERSION == 2* ]]; then pip install unittest2; fi

+ 7 - 0
NEWS

@@ -1,3 +1,10 @@
+0.10.2  UNRELEASED
+
+ IMPROVEMENTS
+
+  * Extended Python3 support to most of the codebase.
+    (Gary van der Merwe, Jelmer Vernooij)
+
 0.10.1  2015-03-25
 
  BUG FIXES

+ 0 - 26
PKG-INFO

@@ -1,26 +0,0 @@
-Metadata-Version: 1.1
-Name: dulwich
-Version: 0.10.1a
-Summary: Python Git Library
-Home-page: https://samba.org/~jelmer/dulwich
-Author: Jelmer Vernooij
-Author-email: jelmer@samba.org
-License: GPLv2 or later
-Description: 
-              Python implementation of the Git file formats and protocols,
-              without the need to have git installed.
-        
-              All functionality is available in pure Python. Optional
-              C extensions can be built for improved performance.
-        
-              The project is named after the part of London that Mr. and Mrs. Git live in
-              in the particular Monty Python sketch.
-              
-Keywords: git
-Platform: UNKNOWN
-Classifier: Development Status :: 4 - Beta
-Classifier: License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)
-Classifier: Programming Language :: Python :: 2.6
-Classifier: Programming Language :: Python :: 2.7
-Classifier: Operating System :: POSIX
-Classifier: Topic :: Software Development :: Version Control

+ 14 - 0
appveyor.yml

@@ -0,0 +1,14 @@
+environment:
+  matrix:
+    - PYTHON: "C:\\Python27"
+      PYTHON_ARCH: "32"
+
+    - PYTHON: "C:\\Python34"
+      PYTHON_ARCH: "32"
+
+build: off
+
+test_script:
+  - "%WITH_COMPILER% %PYTHON%/python setup.py test"
+
+

+ 9 - 8
bin/dulwich

@@ -88,7 +88,7 @@ def cmd_fetch(args):
         determine_wants = r.object_store.determine_wants_all
     refs = client.fetch(path, r, progress=sys.stdout.write)
     print("Remote refs:")
-    for item in refs.iteritems():
+    for item in refs.items():
         print("%s -> %s" % item)
 
 
@@ -132,9 +132,9 @@ def cmd_dump_pack(args):
     for name in x:
         try:
             print("\t%s" % x[name])
-        except KeyError, k:
+        except KeyError as k:
             print("\t%s: Unable to resolve base %s" % (name, k))
-        except ApplyDeltaError, e:
+        except ApplyDeltaError as e:
             print("\t%s: Unable to apply delta: %r" % (name, e))
 
 
@@ -234,7 +234,7 @@ def cmd_rev_list(args):
 def cmd_tag(args):
     opts, args = getopt(args, '', [])
     if len(args) < 2:
-        print 'Usage: dulwich tag NAME'
+        print('Usage: dulwich tag NAME')
         sys.exit(1)
     porcelain.tag('.', args[0])
 
@@ -291,8 +291,8 @@ def cmd_web_daemon(args):
     else:
         gitdir = '.'
     from dulwich import porcelain
-    porcelain.daemon(gitdir, address=options.listen_address,
-                     port=options.port)
+    porcelain.web_daemon(gitdir, address=options.listen_address,
+                         port=options.port)
 
 
 def cmd_receive_pack(args):
@@ -325,14 +325,15 @@ def cmd_status(args):
     status = porcelain.status(gitdir)
     if status.staged:
         sys.stdout.write("Changes to be committed:\n\n")
-        for kind, names in status.staged.iteritems():
+        for kind, names in status.staged.items():
             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("\t%s\n" %
+                    name.decode(sys.getfilesystemencoding()))
         sys.stdout.write("\n")
     if status.untracked:
         sys.stdout.write("Untracked files:\n\n")

+ 26 - 25
docs/tutorial/object-store.txt

@@ -15,9 +15,9 @@ When you use Git, you generally add or modify content. As our repository is
 empty for now, we'll start by adding a new file::
 
   >>> from dulwich.objects import Blob
-  >>> blob = Blob.from_string("My file content\n")
-  >>> blob.id
-  'c55063a4d5d37aa1af2b2dad3a70aa34dae54dc6'
+  >>> blob = Blob.from_string(b"My file content\n")
+  >>> print(blob.id.decode('ascii'))
+  c55063a4d5d37aa1af2b2dad3a70aa34dae54dc6
 
 Of course you could create a blob from an existing file using ``from_file``
 instead.
@@ -27,9 +27,9 @@ give this content a name::
 
   >>> from dulwich.objects import Tree
   >>> tree = Tree()
-  >>> tree.add("spam", 0100644, blob.id)
+  >>> tree.add(b"spam", 0o100644, blob.id)
 
-Note that "0100644" is the octal form for a regular file with common
+Note that "0o100644" is the octal form for a regular file with common
 permissions. You can hardcode them or you can use the ``stat`` module.
 
 The tree state of our repository still needs to be placed in time. That's the
@@ -39,13 +39,13 @@ job of the commit::
   >>> from time import time
   >>> commit = Commit()
   >>> commit.tree = tree.id
-  >>> author = "Your Name <your.email@example.com>"
+  >>> author = b"Your Name <your.email@example.com>"
   >>> commit.author = commit.committer = author
   >>> commit.commit_time = commit.author_time = int(time())
-  >>> tz = parse_timezone('-0200')[0]
+  >>> tz = parse_timezone(b'-0200')[0]
   >>> commit.commit_timezone = commit.author_timezone = tz
-  >>> commit.encoding = "UTF-8"
-  >>> commit.message = "Initial commit"
+  >>> commit.encoding = b"UTF-8"
+  >>> commit.message = b"Initial commit"
 
 Note that the initial commit has no parents.
 
@@ -64,23 +64,24 @@ saving the changes::
 Now the physical repository contains three objects but still has no branch.
 Let's create the master branch like Git would::
 
-  >>> repo.refs['refs/heads/master'] = commit.id
+  >>> repo.refs[b'refs/heads/master'] = commit.id
 
 The master branch now has a commit where to start. When we commit to master, we
 are also moving HEAD, which is Git's currently checked out branch:
 
-  >>> head = repo.refs['HEAD']
+  >>> head = repo.refs[b'HEAD']
   >>> head == commit.id
   True
-  >>> head == repo.refs['refs/heads/master']
+  >>> head == repo.refs[b'refs/heads/master']
   True
 
 How did that work? As it turns out, HEAD is a special kind of ref called a
 symbolic ref, and it points at master. Most functions on the refs container
 work transparently with symbolic refs, but we can also take a peek inside HEAD:
 
-  >>> repo.refs.read_ref('HEAD')
-  'ref: refs/heads/master'
+  >>> import sys
+  >>> print(repo.refs.read_ref(b'HEAD').decode(sys.getfilesystemencoding()))
+  ref: refs/heads/master
 
 Normally, you won't need to use read_ref. If you want to change what ref HEAD
 points to, in order to check out another branch, just use set_symbolic_ref.
@@ -122,20 +123,20 @@ and the new commit'task is to point to this new version.
 Let's first build the blob::
 
   >>> from dulwich.objects import Blob
-  >>> spam = Blob.from_string("My new file content\n")
-  >>> spam.id
-  '16ee2682887a962f854ebd25a61db16ef4efe49f'
+  >>> spam = Blob.from_string(b"My new file content\n")
+  >>> print(spam.id.decode('ascii'))
+  16ee2682887a962f854ebd25a61db16ef4efe49f
 
 An alternative is to alter the previously constructed blob object::
 
-  >>> blob.data = "My new file content\n"
-  >>> blob.id
-  '16ee2682887a962f854ebd25a61db16ef4efe49f'
+  >>> blob.data = b"My new file content\n"
+  >>> print(blob.id.decode('ascii'))
+  16ee2682887a962f854ebd25a61db16ef4efe49f
 
 In any case, update the blob id known as "spam". You also have the
 opportunity of changing its mode::
 
-  >>> tree["spam"] = (0100644, spam.id)
+  >>> tree[b"spam"] = (0o100644, spam.id)
 
 Now let's record the change::
 
@@ -144,11 +145,11 @@ Now let's record the change::
   >>> c2 = Commit()
   >>> c2.tree = tree.id
   >>> c2.parents = [commit.id]
-  >>> c2.author = c2.committer = "John Doe <john@example.com>"
+  >>> c2.author = c2.committer = b"John Doe <john@example.com>"
   >>> c2.commit_time = c2.author_time = int(time())
   >>> c2.commit_timezone = c2.author_timezone = 0
-  >>> c2.encoding = "UTF-8"
-  >>> c2.message = 'Changing "spam"'
+  >>> c2.encoding = b"UTF-8"
+  >>> c2.message = b'Changing "spam"'
 
 In this new commit we record the changed tree id, and most important, the
 previous commit as the parent. Parents are actually a list because a commit
@@ -181,6 +182,6 @@ write_tree_diff::
 You won't see it using git log because the head is still the previous
 commit. It's easy to remedy::
 
-  >>> repo.refs['refs/heads/master'] = c2.id
+  >>> repo.refs[b'refs/heads/master'] = c2.id
 
 Now all git tools will work as expected.

+ 12 - 11
docs/tutorial/remote.txt

@@ -5,12 +5,12 @@ Most of the tests in this file require a Dulwich server, so let's start one:
     >>> from dulwich.repo import Repo
     >>> from dulwich.server import DictBackend, TCPGitServer
     >>> import threading
-    >>> repo = Repo.init("remote", mkdir=True)
-    >>> cid = repo.do_commit("message", committer="Jelmer <jelmer@samba.org>")
-    >>> backend = DictBackend({'/': repo})
-    >>> dul_server = TCPGitServer(backend, 'localhost', 0)
+    >>> repo = Repo.init(b"remote", mkdir=True)
+    >>> cid = repo.do_commit(b"message", committer=b"Jelmer <jelmer@samba.org>")
+    >>> backend = DictBackend({b'/': repo})
+    >>> dul_server = TCPGitServer(backend, b'localhost', 0)
     >>> threading.Thread(target=dul_server.serve).start()
-    >>> server_address, server_port = dul_server.socket.getsockname()
+    >>> server_address, server_port=dul_server.socket.getsockname()
 
 Remote repositories
 ===================
@@ -32,7 +32,7 @@ Dulwich provides support for accessing remote repositories in
 one manually::
 
    >>> from dulwich.client import TCPGitClient
-   >>> client = TCPGitClient(server_address, server_port)
+   >>> client = TCPGitClient(server_address.encode('ascii'), server_port)
 
 Retrieving raw pack files
 -------------------------
@@ -53,19 +53,20 @@ which claims that the client doesn't have any objects::
    >>> class DummyGraphWalker(object):
    ...     def ack(self, sha): pass
    ...     def next(self): pass
+   ...     def __next__(self): pass
 
 With the ``determine_wants`` function in place, we can now fetch a pack,
 which we will write to a ``BytesIO`` object::
 
    >>> from io import BytesIO
    >>> f = BytesIO()
-   >>> remote_refs = client.fetch_pack("/", determine_wants,
+   >>> remote_refs = client.fetch_pack(b"/", determine_wants,
    ...    DummyGraphWalker(), pack_data=f.write)
 
 ``f`` will now contain a full pack file::
 
-   >>> f.getvalue()[:4]
-   'PACK'
+   >>> print(f.getvalue()[:4].decode('ascii'))
+   PACK
 
 Fetching objects into a local repository
 ----------------------------------------
@@ -75,8 +76,8 @@ in which case Dulwich takes care of providing the right graph walker, and
 importing the received pack file into the local repository::
 
    >>> from dulwich.repo import Repo
-   >>> local = Repo.init("local", mkdir=True)
-   >>> remote_refs = client.fetch("/", local)
+   >>> local = Repo.init(b"local", mkdir=True)
+   >>> remote_refs = client.fetch(b"/", local)
 
 Let's shut down the server now that all tests have been run::
 

+ 9 - 8
docs/tutorial/repo.txt

@@ -24,6 +24,7 @@ Creating a repository
 Let's create a folder and turn it into a repository, like ``git init`` would::
 
   >>> from os import mkdir
+  >>> import sys
   >>> mkdir("myrepo")
   >>> repo = Repo.init("myrepo")
   >>> repo
@@ -52,8 +53,8 @@ so only non-bare repositories will have an index, too. To open the index, simply
 call::
 
     >>> index = repo.open_index()
-    >>> repr(index).replace('\\\\', '/')
-    "Index('myrepo/.git/index')"
+    >>> print(index.path.decode(sys.getfilesystemencoding()))
+    myrepo/.git/index
 
 Since the repository was just created, the index will be empty::
 
@@ -66,16 +67,16 @@ Staging new files
 The repository allows "staging" files. Only files can be staged - directories
 aren't tracked explicitly by git. Let's create a simple text file and stage it::
 
-    >>> f = open('myrepo/foo', 'w')
-    >>> f.write("monty")
+    >>> f = open('myrepo/foo', 'wb')
+    >>> _ = f.write(b"monty")
     >>> f.close()
 
-    >>> repo.stage(["foo"])
+    >>> repo.stage([b"foo"])
 
 It will now show up in the index::
 
-    >>> list(repo.open_index())
-    ['foo']
+    >>> print(",".join([f.decode(sys.getfilesystemencoding()) for f in repo.open_index()]))
+    foo
 
 
 Creating new commits
@@ -91,7 +92,7 @@ to specify the message. The committer and author will be retrieved from the
 repository configuration or global configuration if they are not specified::
 
     >>> commit_id = repo.do_commit(
-    ...     "The first commit", committer="Jelmer Vernooij <jelmer@samba.org>")
+    ...     b"The first commit", committer=b"Jelmer Vernooij <jelmer@samba.org>")
 
 ``do_commit`` returns the SHA1 of the commit. Since the commit was to the 
 default branch, the repository's head will now be set to that commit::

+ 0 - 26
dulwich.egg-info/PKG-INFO

@@ -1,26 +0,0 @@
-Metadata-Version: 1.1
-Name: dulwich
-Version: 0.10.1a
-Summary: Python Git Library
-Home-page: https://samba.org/~jelmer/dulwich
-Author: Jelmer Vernooij
-Author-email: jelmer@samba.org
-License: GPLv2 or later
-Description: 
-              Python implementation of the Git file formats and protocols,
-              without the need to have git installed.
-        
-              All functionality is available in pure Python. Optional
-              C extensions can be built for improved performance.
-        
-              The project is named after the part of London that Mr. and Mrs. Git live in
-              in the particular Monty Python sketch.
-              
-Keywords: git
-Platform: UNKNOWN
-Classifier: Development Status :: 4 - Beta
-Classifier: License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)
-Classifier: Programming Language :: Python :: 2.6
-Classifier: Programming Language :: Python :: 2.7
-Classifier: Operating System :: POSIX
-Classifier: Topic :: Software Development :: Version Control

+ 0 - 180
dulwich.egg-info/SOURCES.txt

@@ -1,180 +0,0 @@
-AUTHORS
-COPYING
-HACKING
-MANIFEST.in
-Makefile
-NEWS
-README.md
-setup.cfg
-setup.py
-bin/dul-receive-pack
-bin/dul-upload-pack
-bin/dulwich
-docs/Makefile
-docs/conf.py
-docs/index.txt
-docs/make.bat
-docs/performance.txt
-docs/protocol.txt
-docs/tutorial/Makefile
-docs/tutorial/conclusion.txt
-docs/tutorial/file-format.txt
-docs/tutorial/index.txt
-docs/tutorial/introduction.txt
-docs/tutorial/object-store.txt
-docs/tutorial/remote.txt
-docs/tutorial/repo.txt
-docs/tutorial/tag.txt
-dulwich/__init__.py
-dulwich/_compat.py
-dulwich/_diff_tree.c
-dulwich/_objects.c
-dulwich/_pack.c
-dulwich/client.py
-dulwich/config.py
-dulwich/diff_tree.py
-dulwich/errors.py
-dulwich/fastexport.py
-dulwich/file.py
-dulwich/greenthreads.py
-dulwich/hooks.py
-dulwich/index.py
-dulwich/log_utils.py
-dulwich/lru_cache.py
-dulwich/object_store.py
-dulwich/objects.py
-dulwich/objectspec.py
-dulwich/pack.py
-dulwich/patch.py
-dulwich/porcelain.py
-dulwich/protocol.py
-dulwich/refs.py
-dulwich/repo.py
-dulwich/server.py
-dulwich/stdint.h
-dulwich/walk.py
-dulwich/web.py
-dulwich.egg-info/PKG-INFO
-dulwich.egg-info/SOURCES.txt
-dulwich.egg-info/dependency_links.txt
-dulwich.egg-info/top_level.txt
-dulwich/contrib/__init__.py
-dulwich/contrib/swift.py
-dulwich/contrib/test_swift.py
-dulwich/contrib/test_swift_smoke.py
-dulwich/tests/__init__.py
-dulwich/tests/test_blackbox.py
-dulwich/tests/test_client.py
-dulwich/tests/test_config.py
-dulwich/tests/test_diff_tree.py
-dulwich/tests/test_fastexport.py
-dulwich/tests/test_file.py
-dulwich/tests/test_grafts.py
-dulwich/tests/test_greenthreads.py
-dulwich/tests/test_hooks.py
-dulwich/tests/test_index.py
-dulwich/tests/test_lru_cache.py
-dulwich/tests/test_missing_obj_finder.py
-dulwich/tests/test_object_store.py
-dulwich/tests/test_objects.py
-dulwich/tests/test_objectspec.py
-dulwich/tests/test_pack.py
-dulwich/tests/test_patch.py
-dulwich/tests/test_porcelain.py
-dulwich/tests/test_protocol.py
-dulwich/tests/test_refs.py
-dulwich/tests/test_repository.py
-dulwich/tests/test_server.py
-dulwich/tests/test_utils.py
-dulwich/tests/test_walk.py
-dulwich/tests/test_web.py
-dulwich/tests/utils.py
-dulwich/tests/compat/__init__.py
-dulwich/tests/compat/server_utils.py
-dulwich/tests/compat/test_client.py
-dulwich/tests/compat/test_pack.py
-dulwich/tests/compat/test_repository.py
-dulwich/tests/compat/test_server.py
-dulwich/tests/compat/test_utils.py
-dulwich/tests/compat/test_web.py
-dulwich/tests/compat/utils.py
-dulwich/tests/data/blobs/11/11111111111111111111111111111111111111
-dulwich/tests/data/blobs/6f/670c0fb53f9463760b7295fbb814e965fb20c8
-dulwich/tests/data/blobs/95/4a536f7819d40e6f637f849ee187dd10066349
-dulwich/tests/data/blobs/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391
-dulwich/tests/data/commits/0d/89f20333fbb1d2f3a94da77f4981373d8f4310
-dulwich/tests/data/commits/5d/ac377bdded4c9aeb8dff595f0faeebcc8498cc
-dulwich/tests/data/commits/60/dacdc733de308bb77bb76ce0fb0f9b44c9769e
-dulwich/tests/data/indexes/index
-dulwich/tests/data/packs/pack-bc63ddad95e7321ee734ea11a7a62d314e0d7481.idx
-dulwich/tests/data/packs/pack-bc63ddad95e7321ee734ea11a7a62d314e0d7481.pack
-dulwich/tests/data/repos/.gitattributes
-dulwich/tests/data/repos/server_new.export
-dulwich/tests/data/repos/server_old.export
-dulwich/tests/data/repos/a.git/HEAD
-dulwich/tests/data/repos/a.git/packed-refs
-dulwich/tests/data/repos/a.git/objects/28/237f4dc30d0d462658d6b937b08a0f0b6ef55a
-dulwich/tests/data/repos/a.git/objects/2a/72d929692c41d8554c07f6301757ba18a65d91
-dulwich/tests/data/repos/a.git/objects/4e/f30bbfe26431a69c3820d3a683df54d688f2ec
-dulwich/tests/data/repos/a.git/objects/4f/2e6529203aa6d44b5af6e3292c837ceda003f9
-dulwich/tests/data/repos/a.git/objects/7d/9a07d797595ef11344549b8d08198e48c15364
-dulwich/tests/data/repos/a.git/objects/a2/96d0bb611188cabb256919f36bc30117cca005
-dulwich/tests/data/repos/a.git/objects/a9/0fa2d900a17e99b433217e988c4eb4a2e9a097
-dulwich/tests/data/repos/a.git/objects/b0/931cadc54336e78a1d980420e3268903b57a50
-dulwich/tests/data/repos/a.git/objects/ff/d47d45845a8f6576491e1edb97e3fe6a850e7f
-dulwich/tests/data/repos/a.git/refs/heads/master
-dulwich/tests/data/repos/a.git/refs/tags/mytag
-dulwich/tests/data/repos/empty.git/HEAD
-dulwich/tests/data/repos/empty.git/config
-dulwich/tests/data/repos/empty.git/objects/info/.gitignore
-dulwich/tests/data/repos/empty.git/objects/pack/.gitignore
-dulwich/tests/data/repos/empty.git/refs/heads/.gitignore
-dulwich/tests/data/repos/empty.git/refs/tags/.gitignore
-dulwich/tests/data/repos/ooo_merge.git/HEAD
-dulwich/tests/data/repos/ooo_merge.git/objects/29/69be3e8ee1c0222396a5611407e4769f14e54b
-dulwich/tests/data/repos/ooo_merge.git/objects/38/74e9c60a6d149c44c928140f250d81e6381520
-dulwich/tests/data/repos/ooo_merge.git/objects/6f/670c0fb53f9463760b7295fbb814e965fb20c8
-dulwich/tests/data/repos/ooo_merge.git/objects/70/c190eb48fa8bbb50ddc692a17b44cb781af7f6
-dulwich/tests/data/repos/ooo_merge.git/objects/76/01d7f6231db6a57f7bbb79ee52e4d462fd44d1
-dulwich/tests/data/repos/ooo_merge.git/objects/90/182552c4a85a45ec2a835cadc3451bebdfe870
-dulwich/tests/data/repos/ooo_merge.git/objects/95/4a536f7819d40e6f637f849ee187dd10066349
-dulwich/tests/data/repos/ooo_merge.git/objects/b2/a2766a2879c209ab1176e7e778b81ae422eeaa
-dulwich/tests/data/repos/ooo_merge.git/objects/f5/07291b64138b875c28e03469025b1ea20bc614
-dulwich/tests/data/repos/ooo_merge.git/objects/f9/e39b120c68182a4ba35349f832d0e4e61f485c
-dulwich/tests/data/repos/ooo_merge.git/objects/fb/5b0425c7ce46959bec94d54b9a157645e114f5
-dulwich/tests/data/repos/ooo_merge.git/refs/heads/master
-dulwich/tests/data/repos/refs.git/HEAD
-dulwich/tests/data/repos/refs.git/packed-refs
-dulwich/tests/data/repos/refs.git/objects/3b/9e5457140e738c2dcd39bf6d7acf88379b90d1
-dulwich/tests/data/repos/refs.git/objects/3e/c9c43c84ff242e3ef4a9fc5bc111fd780a76a8
-dulwich/tests/data/repos/refs.git/objects/42/d06bd4b77fed026b154d16493e5deab78f02ec
-dulwich/tests/data/repos/refs.git/objects/a1/8114c31713746a33a2e70d9914d1ef3e781425
-dulwich/tests/data/repos/refs.git/objects/cd/a609072918d7b70057b6bef9f4c2537843fcfe
-dulwich/tests/data/repos/refs.git/objects/df/6800012397fb85c56e7418dd4eb9405dee075c
-dulwich/tests/data/repos/refs.git/refs/heads/40-char-ref-aaaaaaaaaaaaaaaaaa
-dulwich/tests/data/repos/refs.git/refs/heads/loop
-dulwich/tests/data/repos/refs.git/refs/heads/master
-dulwich/tests/data/repos/refs.git/refs/tags/refs-0.2
-dulwich/tests/data/repos/simple_merge.git/HEAD
-dulwich/tests/data/repos/simple_merge.git/objects/0d/89f20333fbb1d2f3a94da77f4981373d8f4310
-dulwich/tests/data/repos/simple_merge.git/objects/1b/6318f651a534b38f9c7aedeebbd56c1e896853
-dulwich/tests/data/repos/simple_merge.git/objects/29/69be3e8ee1c0222396a5611407e4769f14e54b
-dulwich/tests/data/repos/simple_merge.git/objects/4c/ffe90e0a41ad3f5190079d7c8f036bde29cbe6
-dulwich/tests/data/repos/simple_merge.git/objects/5d/ac377bdded4c9aeb8dff595f0faeebcc8498cc
-dulwich/tests/data/repos/simple_merge.git/objects/60/dacdc733de308bb77bb76ce0fb0f9b44c9769e
-dulwich/tests/data/repos/simple_merge.git/objects/6f/670c0fb53f9463760b7295fbb814e965fb20c8
-dulwich/tests/data/repos/simple_merge.git/objects/70/c190eb48fa8bbb50ddc692a17b44cb781af7f6
-dulwich/tests/data/repos/simple_merge.git/objects/90/182552c4a85a45ec2a835cadc3451bebdfe870
-dulwich/tests/data/repos/simple_merge.git/objects/95/4a536f7819d40e6f637f849ee187dd10066349
-dulwich/tests/data/repos/simple_merge.git/objects/ab/64bbdcc51b170d21588e5c5d391ee5c0c96dfd
-dulwich/tests/data/repos/simple_merge.git/objects/d4/bdad6549dfedf25d3b89d21f506aff575b28a7
-dulwich/tests/data/repos/simple_merge.git/objects/d8/0c186a03f423a81b39df39dc87fd269736ca86
-dulwich/tests/data/repos/simple_merge.git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391
-dulwich/tests/data/repos/simple_merge.git/refs/heads/master
-dulwich/tests/data/repos/submodule/dotgit
-dulwich/tests/data/tags/71/033db03a03c6a36721efcf1968dd8f8e0cf023
-dulwich/tests/data/trees/70/c190eb48fa8bbb50ddc692a17b44cb781af7f6
-examples/clone.py
-examples/config.py
-examples/diff.py
-examples/latest_change.py

+ 0 - 1
dulwich.egg-info/dependency_links.txt

@@ -1 +0,0 @@
-

+ 0 - 1
dulwich.egg-info/top_level.txt

@@ -1 +0,0 @@
-dulwich

+ 1 - 1
dulwich/__init__.py

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

+ 105 - 84
dulwich/client.py

@@ -38,7 +38,7 @@ Known capabilities that are not supported:
 
 __docformat__ = 'restructuredText'
 
-from io import BytesIO
+from io import BytesIO, BufferedReader
 import dulwich
 import select
 import socket
@@ -60,6 +60,19 @@ from dulwich.errors import (
     )
 from dulwich.protocol import (
     _RBUFSIZE,
+    CAPABILITY_DELETE_REFS,
+    CAPABILITY_MULTI_ACK,
+    CAPABILITY_MULTI_ACK_DETAILED,
+    CAPABILITY_OFS_DELTA,
+    CAPABILITY_REPORT_STATUS,
+    CAPABILITY_SIDE_BAND_64K,
+    CAPABILITY_THIN_PACK,
+    COMMAND_DONE,
+    COMMAND_HAVE,
+    COMMAND_WANT,
+    SIDE_BAND_CHANNEL_DATA,
+    SIDE_BAND_CHANNEL_PROGRESS,
+    SIDE_BAND_CHANNEL_FATAL,
     PktLineParser,
     Protocol,
     ProtocolFile,
@@ -79,10 +92,11 @@ def _fileno_can_read(fileno):
     """Check if a file descriptor is readable."""
     return len(select.select([fileno], [], [], 0)[0]) > 0
 
-COMMON_CAPABILITIES = ['ofs-delta', 'side-band-64k']
-FETCH_CAPABILITIES = (['thin-pack', 'multi_ack', 'multi_ack_detailed'] +
+COMMON_CAPABILITIES = [CAPABILITY_OFS_DELTA, CAPABILITY_SIDE_BAND_64K]
+FETCH_CAPABILITIES = ([CAPABILITY_THIN_PACK, CAPABILITY_MULTI_ACK,
+                       CAPABILITY_MULTI_ACK_DETAILED] +
                       COMMON_CAPABILITIES)
-SEND_CAPABILITIES = ['report-status'] + COMMON_CAPABILITIES
+SEND_CAPABILITIES = [CAPABILITY_REPORT_STATUS] + COMMON_CAPABILITIES
 
 
 class ReportStatusParser(object):
@@ -101,27 +115,27 @@ class ReportStatusParser(object):
         :raise SendPackError: Raised when the server could not unpack
         :raise UpdateRefsError: Raised when refs could not be updated
         """
-        if self._pack_status not in ('unpack ok', None):
+        if self._pack_status not in (b'unpack ok', None):
             raise SendPackError(self._pack_status)
         if not self._ref_status_ok:
             ref_status = {}
             ok = set()
             for status in self._ref_statuses:
-                if ' ' not in status:
+                if b' ' not in status:
                     # malformed response, move on to the next one
                     continue
-                status, ref = status.split(' ', 1)
+                status, ref = status.split(b' ', 1)
 
-                if status == 'ng':
-                    if ' ' in ref:
-                        ref, status = ref.split(' ', 1)
+                if status == b'ng':
+                    if b' ' in ref:
+                        ref, status = ref.split(b' ', 1)
                 else:
                     ok.add(ref)
                 ref_status[ref] = status
-            raise UpdateRefsError('%s failed to update' %
-                                  ', '.join([ref for ref in ref_status
-                                             if ref not in ok]),
-                                  ref_status=ref_status)
+            # TODO(jelmer): don't assume encoding of refs is ascii.
+            raise UpdateRefsError(', '.join([
+                ref.decode('ascii') for ref in ref_status if ref not in ok]) +
+                ' failed to update', ref_status=ref_status)
 
     def handle_packet(self, pkt):
         """Handle a packet.
@@ -139,7 +153,7 @@ class ReportStatusParser(object):
         else:
             ref_status = pkt.strip()
             self._ref_statuses.append(ref_status)
-            if not ref_status.startswith('ok '):
+            if not ref_status.startswith(b'ok '):
                 self._ref_status_ok = False
 
 
@@ -148,8 +162,8 @@ def read_pkt_refs(proto):
     refs = {}
     # Receive refs from server
     for pkt in proto.read_pkt_seq():
-        (sha, ref) = pkt.rstrip('\n').split(None, 1)
-        if sha == 'ERR':
+        (sha, ref) = pkt.rstrip(b'\n').split(None, 1)
+        if sha == b'ERR':
             raise GitProtocolError(ref)
         if server_capabilities is None:
             (ref, server_capabilities) = extract_capabilities(ref)
@@ -180,7 +194,7 @@ class GitClient(object):
         self._fetch_capabilities = set(FETCH_CAPABILITIES)
         self._send_capabilities = set(SEND_CAPABILITIES)
         if not thin_packs:
-            self._fetch_capabilities.remove('thin-pack')
+            self._fetch_capabilities.remove(CAPABILITY_THIN_PACK)
 
     def send_pack(self, path, determine_wants, generate_pack_contents,
                   progress=None, write_pack=write_pack_objects):
@@ -236,7 +250,7 @@ class GitClient(object):
 
     def _parse_status_report(self, proto):
         unpack = proto.read_pkt_line().strip()
-        if unpack != 'unpack ok':
+        if unpack != b'unpack ok':
             st = True
             # flush remaining error data
             while st is not None:
@@ -248,7 +262,7 @@ class GitClient(object):
         while ref_status:
             ref_status = ref_status.strip()
             statuses.append(ref_status)
-            if not ref_status.startswith('ok '):
+            if not ref_status.startswith(b'ok '):
                 errs = True
             ref_status = proto.read_pkt_line()
 
@@ -256,20 +270,20 @@ class GitClient(object):
             ref_status = {}
             ok = set()
             for status in statuses:
-                if ' ' not in status:
+                if b' ' not in status:
                     # malformed response, move on to the next one
                     continue
-                status, ref = status.split(' ', 1)
+                status, ref = status.split(b' ', 1)
 
-                if status == 'ng':
-                    if ' ' in ref:
-                        ref, status = ref.split(' ', 1)
+                if status == b'ng':
+                    if b' ' in ref:
+                        ref, status = ref.split(b' ', 1)
                 else:
                     ok.add(ref)
                 ref_status[ref] = status
-            raise UpdateRefsError('%s failed to update' %
-                                  ', '.join([ref for ref in ref_status
-                                             if ref not in ok]),
+            raise UpdateRefsError(', '.join([ref for ref in ref_status
+                                             if ref not in ok]) +
+                                             b' failed to update',
                                   ref_status=ref_status)
 
     def _read_side_band64k_data(self, proto, channel_callbacks):
@@ -282,7 +296,7 @@ class GitClient(object):
             handlers to use. None for a callback discards channel data.
         """
         for pkt in proto.read_pkt_seq():
-            channel = ord(pkt[0])
+            channel = ord(pkt[:1])
             pkt = pkt[1:]
             try:
                 cb = channel_callbacks[channel]
@@ -306,18 +320,18 @@ class GitClient(object):
         have = [x for x in old_refs.values() if not x == ZERO_SHA]
         sent_capabilities = False
 
-        for refname in set(new_refs.keys() + old_refs.keys()):
+        all_refs = set(new_refs.keys()).union(set(old_refs.keys()))
+        for refname in all_refs:
             old_sha1 = old_refs.get(refname, ZERO_SHA)
             new_sha1 = new_refs.get(refname, ZERO_SHA)
 
             if old_sha1 != new_sha1:
                 if sent_capabilities:
-                    proto.write_pkt_line('%s %s %s' % (
-                        old_sha1, new_sha1, refname))
+                    proto.write_pkt_line(old_sha1 + b' ' + new_sha1 + b' ' + refname)
                 else:
                     proto.write_pkt_line(
-                        '%s %s %s\0%s' % (old_sha1, new_sha1, refname,
-                                          ' '.join(capabilities)))
+                        old_sha1 + b' ' + new_sha1 + b' ' + refname + b'\0' +
+                        b' '.join(capabilities))
                     sent_capabilities = True
             if new_sha1 not in have and new_sha1 != ZERO_SHA:
                 want.append(new_sha1)
@@ -331,16 +345,16 @@ class GitClient(object):
         :param capabilities: List of negotiated capabilities
         :param progress: Optional progress reporting function
         """
-        if "side-band-64k" in capabilities:
+        if b"side-band-64k" in capabilities:
             if progress is None:
                 progress = lambda x: None
             channel_callbacks = {2: progress}
-            if 'report-status' in capabilities:
+            if CAPABILITY_REPORT_STATUS in capabilities:
                 channel_callbacks[1] = PktLineParser(
                     self._report_status_parser.handle_packet).parse
             self._read_side_band64k_data(proto, channel_callbacks)
         else:
-            if 'report-status' in capabilities:
+            if CAPABILITY_REPORT_STATUS in capabilities:
                 for pkt in proto.read_pkt_seq():
                     self._report_status_parser.handle_packet(pkt)
         if self._report_status_parser is not None:
@@ -357,30 +371,29 @@ class GitClient(object):
         :param can_read: function that returns a boolean that indicates
             whether there is extra graph data to read on proto
         """
-        assert isinstance(wants, list) and isinstance(wants[0], str)
-        proto.write_pkt_line('want %s %s\n' % (
-            wants[0], ' '.join(capabilities)))
+        assert isinstance(wants, list) and isinstance(wants[0], bytes)
+        proto.write_pkt_line(COMMAND_WANT + b' ' + wants[0] + b' ' + b' '.join(capabilities) + b'\n')
         for want in wants[1:]:
-            proto.write_pkt_line('want %s\n' % want)
+            proto.write_pkt_line(COMMAND_WANT + b' ' + want + b'\n')
         proto.write_pkt_line(None)
         have = next(graph_walker)
         while have:
-            proto.write_pkt_line('have %s\n' % have)
+            proto.write_pkt_line(COMMAND_HAVE + b' ' + have + b'\n')
             if can_read():
                 pkt = proto.read_pkt_line()
-                parts = pkt.rstrip('\n').split(' ')
-                if parts[0] == 'ACK':
+                parts = pkt.rstrip(b'\n').split(b' ')
+                if parts[0] == b'ACK':
                     graph_walker.ack(parts[1])
-                    if parts[2] in ('continue', 'common'):
+                    if parts[2] in (b'continue', b'common'):
                         pass
-                    elif parts[2] == 'ready':
+                    elif parts[2] == b'ready':
                         break
                     else:
                         raise AssertionError(
                             "%s not in ('continue', 'ready', 'common)" %
                             parts[2])
             have = next(graph_walker)
-        proto.write_pkt_line('done\n')
+        proto.write_pkt_line(COMMAND_DONE + b'\n')
 
     def _handle_upload_pack_tail(self, proto, capabilities, graph_walker,
                                  pack_data, progress=None, rbufsize=_RBUFSIZE):
@@ -395,22 +408,25 @@ class GitClient(object):
         """
         pkt = proto.read_pkt_line()
         while pkt:
-            parts = pkt.rstrip('\n').split(' ')
-            if parts[0] == 'ACK':
+            parts = pkt.rstrip(b'\n').split(b' ')
+            if parts[0] == b'ACK':
                 graph_walker.ack(parts[1])
             if len(parts) < 3 or parts[2] not in (
-                    'ready', 'continue', 'common'):
+                    b'ready', b'continue', b'common'):
                 break
             pkt = proto.read_pkt_line()
-        if "side-band-64k" in capabilities:
+        if CAPABILITY_SIDE_BAND_64K in capabilities:
             if progress is None:
                 # Just ignore progress data
                 progress = lambda x: None
-            self._read_side_band64k_data(proto, {1: pack_data, 2: progress})
+            self._read_side_band64k_data(proto, {
+                SIDE_BAND_CHANNEL_DATA: pack_data,
+                SIDE_BAND_CHANNEL_PROGRESS: progress}
+            )
         else:
             while True:
                 data = proto.read(rbufsize)
-                if data == "":
+                if data == b"":
                     break
                 pack_data(data)
 
@@ -447,12 +463,12 @@ class TraditionalGitClient(GitClient):
         :raises UpdateRefsError: if the server supports report-status
                                  and rejects ref updates
         """
-        proto, unused_can_read = self._connect('receive-pack', path)
+        proto, unused_can_read = self._connect(b'receive-pack', path)
         with proto:
             old_refs, server_capabilities = read_pkt_refs(proto)
             negotiated_capabilities = self._send_capabilities & server_capabilities
 
-            if 'report-status' in negotiated_capabilities:
+            if CAPABILITY_REPORT_STATUS in negotiated_capabilities:
                 self._report_status_parser = ReportStatusParser()
             report_status_parser = self._report_status_parser
 
@@ -462,15 +478,14 @@ class TraditionalGitClient(GitClient):
                 proto.write_pkt_line(None)
                 raise
 
-            if not 'delete-refs' in server_capabilities:
+            if not CAPABILITY_DELETE_REFS in server_capabilities:
                 # Server does not support deletions. Fail later.
                 new_refs = dict(orig_new_refs)
-                for ref, sha in orig_new_refs.iteritems():
+                for ref, sha in orig_new_refs.items():
                     if sha == ZERO_SHA:
-                        if 'report-status' in negotiated_capabilities:
+                        if CAPABILITY_REPORT_STATUS in negotiated_capabilities:
                             report_status_parser._ref_statuses.append(
-                                'ng %s remote does not support deleting refs'
-                                % sha)
+                                b'ng ' + sha + b' remote does not support deleting refs')
                             report_status_parser._ref_status_ok = False
                         del new_refs[ref]
 
@@ -493,7 +508,7 @@ class TraditionalGitClient(GitClient):
 
             dowrite = len(objects) > 0
             dowrite = dowrite or any(old_refs.get(ref) != sha
-                                     for (ref, sha) in new_refs.iteritems()
+                                     for (ref, sha) in new_refs.items()
                                      if sha != ZERO_SHA)
             if dowrite:
                 write_pack(proto.write_file(), objects)
@@ -511,7 +526,7 @@ class TraditionalGitClient(GitClient):
         :param pack_data: Callback called for each bit of data in the pack
         :param progress: Callback for progress reports (strings)
         """
-        proto, can_read = self._connect('upload-pack', path)
+        proto, can_read = self._connect(b'upload-pack', path)
         with proto:
             refs, server_capabilities = read_pkt_refs(proto)
             negotiated_capabilities = (
@@ -541,22 +556,24 @@ class TraditionalGitClient(GitClient):
                 write_error=None):
         proto, can_read = self._connect(b'upload-archive', path)
         with proto:
-            proto.write_pkt_line("argument %s" % committish)
+            proto.write_pkt_line(b"argument " + committish)
             proto.write_pkt_line(None)
             pkt = proto.read_pkt_line()
-            if pkt == "NACK\n":
+            if pkt == b"NACK\n":
                 return
-            elif pkt == "ACK\n":
+            elif pkt == b"ACK\n":
                 pass
-            elif pkt.startswith("ERR "):
-                raise GitProtocolError(pkt[4:].rstrip("\n"))
+            elif pkt.startswith(b"ERR "):
+                raise GitProtocolError(pkt[4:].rstrip(b"\n"))
             else:
                 raise AssertionError("invalid response %r" % pkt)
             ret = proto.read_pkt_line()
             if ret is not None:
                 raise AssertionError("expected pkt tail")
             self._read_side_band64k_data(proto, {
-                1: write_data, 2: progress, 3: write_error})
+                SIDE_BAND_CHANNEL_DATA: write_data,
+                SIDE_BAND_CHANNEL_PROGRESS: progress,
+                SIDE_BAND_CHANNEL_FATAL: write_error})
 
 
 class TCPGitClient(TraditionalGitClient):
@@ -597,9 +614,9 @@ class TCPGitClient(TraditionalGitClient):
 
         proto = Protocol(rfile.read, wfile.write, close,
                          report_activity=self._report_activity)
-        if path.startswith("/~"):
+        if path.startswith(b"/~"):
             path = path[1:]
-        proto.send_cmd('git-%s' % cmd, path, 'host=%s' % self._host)
+        proto.send_cmd(b'git-' + cmd, path, b'host=' + self._host)
         return proto, lambda: _fileno_can_read(s)
 
 
@@ -608,7 +625,10 @@ class SubprocessWrapper(object):
 
     def __init__(self, proc):
         self.proc = proc
-        self.read = proc.stdout.read
+        if sys.version_info[0] == 2:
+            self.read = proc.stdout.read
+        else:
+            self.read = BufferedReader(proc.stdout).read
         self.write = proc.stdin.write
 
     def can_read(self):
@@ -688,7 +708,8 @@ class LocalGitClient(GitClient):
 
         have = [sha1 for sha1 in old_refs.values() if sha1 != ZERO_SHA]
         want = []
-        for refname in set(new_refs.keys() + old_refs.keys()):
+        all_refs = set(new_refs.keys()).union(set(old_refs.keys()))
+        for refname in all_refs:
             old_sha1 = old_refs.get(refname, ZERO_SHA)
             new_sha1 = new_refs.get(refname, ZERO_SHA)
             if new_sha1 not in have and new_sha1 != ZERO_SHA:
@@ -699,7 +720,7 @@ class LocalGitClient(GitClient):
 
         target.object_store.add_objects(generate_pack_contents(have, want))
 
-        for name, sha in new_refs.iteritems():
+        for name, sha in new_refs.items():
             target.refs[name] = sha
 
         return new_refs
@@ -909,16 +930,16 @@ class SSHGitClient(TraditionalGitClient):
         self.alternative_paths = {}
 
     def _get_cmd_path(self, cmd):
-        return self.alternative_paths.get(cmd, 'git-%s' % cmd)
+        return self.alternative_paths.get(cmd, b'git-' + cmd)
 
     def _connect(self, cmd, path):
-        if path.startswith("/~"):
+        if path.startswith(b"/~"):
             path = path[1:]
         con = get_ssh_vendor().run_command(
-            self.host, ["%s '%s'" % (self._get_cmd_path(cmd), path)],
+            self.host, [self._get_cmd_path(cmd) + b" '" + path + b"'"],
             port=self.port, username=self.username)
-        return (Protocol(con.read, con.write, con.close, 
-                         report_activity=self._report_activity), 
+        return (Protocol(con.read, con.write, con.close,
+                         report_activity=self._report_activity),
                 con.can_read)
 
 
@@ -1021,10 +1042,10 @@ class HttpGitClient(GitClient):
         """
         url = self._get_url(path)
         old_refs, server_capabilities = self._discover_references(
-            "git-receive-pack", url)
+            b"git-receive-pack", url)
         negotiated_capabilities = self._send_capabilities & server_capabilities
 
-        if 'report-status' in negotiated_capabilities:
+        if CAPABILITY_REPORT_STATUS in negotiated_capabilities:
             self._report_status_parser = ReportStatusParser()
 
         new_refs = determine_wants(dict(old_refs))
@@ -1041,7 +1062,7 @@ class HttpGitClient(GitClient):
         objects = generate_pack_contents(have, want)
         if len(objects) > 0:
             write_pack(req_proto.write_file(), objects)
-        resp = self._smart_request("git-receive-pack", url,
+        resp = self._smart_request(b"git-receive-pack", url,
                                    data=req_data.getvalue())
         try:
             resp_proto = Protocol(resp.read, None)
@@ -1064,7 +1085,7 @@ class HttpGitClient(GitClient):
         """
         url = self._get_url(path)
         refs, server_capabilities = self._discover_references(
-            "git-upload-pack", url)
+            b"git-upload-pack", url)
         negotiated_capabilities = self._fetch_capabilities & server_capabilities
         wants = determine_wants(refs)
         if wants is not None:
@@ -1079,7 +1100,7 @@ class HttpGitClient(GitClient):
             req_proto, negotiated_capabilities, graph_walker, wants,
             lambda: False)
         resp = self._smart_request(
-            "git-upload-pack", url, data=req_data.getvalue())
+            b"git-upload-pack", url, data=req_data.getvalue())
         try:
             resp_proto = Protocol(resp.read, None)
             self._handle_upload_pack_tail(resp_proto, negotiated_capabilities,
@@ -1135,7 +1156,7 @@ def get_transport_and_path(location, **kwargs):
         pass
 
     if (sys.platform == 'win32' and
-            location[0].isalpha() and location[1:2] == ':\\'):
+            location[0].isalpha() and location[1:3] == ':\\'):
         # Windows local path
         return default_local_git_client_cls(**kwargs), location
 

+ 71 - 54
dulwich/config.py

@@ -26,7 +26,6 @@ TODO:
 
 import errno
 import os
-import re
 
 try:
     from collections import (
@@ -69,9 +68,9 @@ class Config(object):
             value = self.get(section, name)
         except KeyError:
             return default
-        if value.lower() == "true":
+        if value.lower() == b"true":
             return True
-        elif value.lower() == "false":
+        elif value.lower() == b"false":
             return False
         raise ValueError("not a valid boolean string: %r" % value)
 
@@ -142,7 +141,7 @@ class ConfigDict(Config, MutableMapping):
             return (parts[0], None, parts[1])
 
     def get(self, section, name):
-        if isinstance(section, basestring):
+        if not isinstance(section, tuple):
             section = (section, )
         if len(section) > 1:
             try:
@@ -152,37 +151,37 @@ class ConfigDict(Config, MutableMapping):
         return self._values[(section[0],)][name]
 
     def set(self, section, name, value):
-        if isinstance(section, basestring):
+        if not isinstance(section, tuple):
             section = (section, )
         self._values.setdefault(section, OrderedDict())[name] = value
 
     def iteritems(self, section):
-        return self._values.get(section, OrderedDict()).iteritems()
+        return self._values.get(section, OrderedDict()).items()
 
     def itersections(self):
         return self._values.keys()
 
 
 def _format_string(value):
-    if (value.startswith(" ") or
-        value.startswith("\t") or
-        value.endswith(" ") or
-        value.endswith("\t")):
-        return '"%s"' % _escape_value(value)
+    if (value.startswith(b" ") or
+        value.startswith(b"\t") or
+        value.endswith(b" ") or
+        value.endswith(b"\t")):
+        return b'"' + _escape_value(value) + b'"'
     return _escape_value(value)
 
 
 def _parse_string(value):
-    value = value.strip()
-    ret = []
-    block = []
+    value = bytearray(value.strip())
+    ret = bytearray()
+    block = bytearray()
     in_quotes = False
     for c in value:
-        if c == "\"":
+        if c == ord(b"\""):
             in_quotes = (not in_quotes)
-            ret.append(_unescape_value("".join(block)))
-            block = []
-        elif c in ("#", ";") and not in_quotes:
+            ret.extend(_unescape_value(block))
+            block = bytearray()
+        elif c in (ord(b"#"), ord(b";")) and not in_quotes:
             # the rest of the line is a comment
             break
         else:
@@ -191,46 +190,58 @@ def _parse_string(value):
     if in_quotes:
         raise ValueError("value starts with quote but lacks end quote")
 
-    ret.append(_unescape_value("".join(block)).rstrip())
+    ret.extend(_unescape_value(block).rstrip())
 
-    return "".join(ret)
+    return bytes(ret)
 
 
 def _unescape_value(value):
     """Unescape a value."""
-    def unescape(c):
-        return {
-            "\\\\": "\\",
-            "\\\"": "\"",
-            "\\n": "\n",
-            "\\t": "\t",
-            "\\b": "\b",
-            }[c.group(0)]
-    return re.sub(r"(\\.)", unescape, value)
+    if type(value) != bytearray:
+        raise TypeError("expected: bytearray")
+    table = {
+        ord(b"\\"): ord(b"\\"),
+        ord(b"\""): ord(b"\""),
+        ord(b"n"): ord(b"\n"),
+        ord(b"t"): ord(b"\t"),
+        ord(b"b"): ord(b"\b"),
+        }
+    ret = bytearray()
+    i = 0
+    while i < len(value):
+        if value[i] == ord(b"\\"):
+            i += 1
+            ret.append(table[value[i]])
+        else:
+            ret.append(value[i])
+        i += 1
+    return ret
 
 
 def _escape_value(value):
     """Escape a value."""
-    return value.replace("\\", "\\\\").replace("\n", "\\n").replace("\t", "\\t").replace("\"", "\\\"")
+    return value.replace(b"\\", b"\\\\").replace(b"\n", b"\\n").replace(b"\t", b"\\t").replace(b"\"", b"\\\"")
 
 
 def _check_variable_name(name):
-    for c in name:
-        if not c.isalnum() and c != '-':
+    for i in range(len(name)):
+        c = name[i:i+1]
+        if not c.isalnum() and c != b'-':
             return False
     return True
 
 
 def _check_section_name(name):
-    for c in name:
-        if not c.isalnum() and c not in ('-', '.'):
+    for i in range(len(name)):
+        c = name[i:i+1]
+        if not c.isalnum() and c not in (b'-', b'.'):
             return False
     return True
 
 
 def _strip_comments(line):
-    line = line.split("#")[0]
-    line = line.split(";")[0]
+    line = line.split(b"#")[0]
+    line = line.split(b";")[0]
     return line
 
 
@@ -247,47 +258,47 @@ class ConfigFile(ConfigDict):
         for lineno, line in enumerate(f.readlines()):
             line = line.lstrip()
             if setting is None:
-                if len(line) > 0 and line[0] == "[":
+                if len(line) > 0 and line[:1] == b"[":
                     line = _strip_comments(line).rstrip()
-                    last = line.index("]")
+                    last = line.index(b"]")
                     if last == -1:
                         raise ValueError("expected trailing ]")
-                    pts = line[1:last].split(" ", 1)
+                    pts = line[1:last].split(b" ", 1)
                     line = line[last+1:]
                     pts[0] = pts[0].lower()
                     if len(pts) == 2:
-                        if pts[1][0] != "\"" or pts[1][-1] != "\"":
+                        if pts[1][:1] != b"\"" or pts[1][-1:] != b"\"":
                             raise ValueError(
-                                "Invalid subsection " + pts[1])
+                                "Invalid subsection %r" % pts[1])
                         else:
                             pts[1] = pts[1][1:-1]
                         if not _check_section_name(pts[0]):
-                            raise ValueError("invalid section name %s" %
+                            raise ValueError("invalid section name %r" %
                                              pts[0])
                         section = (pts[0], pts[1])
                     else:
                         if not _check_section_name(pts[0]):
-                            raise ValueError("invalid section name %s" %
+                            raise ValueError("invalid section name %r" %
                                     pts[0])
-                        pts = pts[0].split(".", 1)
+                        pts = pts[0].split(b".", 1)
                         if len(pts) == 2:
                             section = (pts[0], pts[1])
                         else:
                             section = (pts[0], )
                     ret._values[section] = OrderedDict()
-                if _strip_comments(line).strip() == "":
+                if _strip_comments(line).strip() == b"":
                     continue
                 if section is None:
                     raise ValueError("setting %r without section" % line)
                 try:
-                    setting, value = line.split("=", 1)
+                    setting, value = line.split(b"=", 1)
                 except ValueError:
                     setting = line
-                    value = "true"
+                    value = b"true"
                 setting = setting.strip().lower()
                 if not _check_variable_name(setting):
                     raise ValueError("invalid variable name %s" % setting)
-                if value.endswith("\\\n"):
+                if value.endswith(b"\\\n"):
                     value = value[:-2]
                     continuation = True
                 else:
@@ -297,7 +308,7 @@ class ConfigFile(ConfigDict):
                 if not continuation:
                     setting = None
             else:  # continuation line
-                if line.endswith("\\\n"):
+                if line.endswith(b"\\\n"):
                     line = line[:-2]
                     continuation = True
                 else:
@@ -325,18 +336,24 @@ class ConfigFile(ConfigDict):
 
     def write_to_file(self, f):
         """Write configuration to a file-like object."""
-        for section, values in self._values.iteritems():
+        for section, values in self._values.items():
             try:
                 section_name, subsection_name = section
             except ValueError:
                 (section_name, ) = section
                 subsection_name = None
             if subsection_name is None:
-                f.write("[%s]\n" % section_name)
+                f.write(b"[" + section_name + b"]\n")
             else:
-                f.write("[%s \"%s\"]\n" % (section_name, subsection_name))
-            for key, value in values.iteritems():
-                f.write("\t%s = %s\n" % (key, _escape_value(value)))
+                f.write(b"[" + section_name + b" \"" + subsection_name + b"\"]\n")
+            for key, value in values.items():
+                if value is True:
+                    value = b"true"
+                elif value is False:
+                    value = b"false"
+                else:
+                    value = _escape_value(value)
+                f.write(b"\t" + key + b" = " + value + b"\n")
 
 
 class StackedConfig(Config):

+ 1 - 1
dulwich/contrib/swift.py

@@ -126,7 +126,7 @@ class PackInfoObjectStoreIterator(GreenThreadsObjectStoreIterator):
 
     def __len__(self):
         while len(self.finder.objects_to_send):
-            for _ in xrange(0, len(self.finder.objects_to_send)):
+            for _ in range(0, len(self.finder.objects_to_send)):
                 sha = self.finder.next()
                 self._shas.append(sha)
         return len(self._shas)

+ 3 - 3
dulwich/contrib/test_swift.py

@@ -193,7 +193,7 @@ def create_commit(data, marker='Default', blob=None):
 
 def create_commits(length=1, marker='Default'):
     data = []
-    for i in xrange(0, length):
+    for i in range(0, length):
         _marker = "%s_%s" % (marker, i)
         blob, tree, tag, cmt = create_commit(data, _marker)
         data.extend([blob, tree, tag, cmt])
@@ -449,11 +449,11 @@ class TestPackInfoLoadDump(TestCase):
 #    def test_pack_info_perf(self):
 #        dump_time = []
 #        load_time = []
-#        for i in xrange(0, 100):
+#        for i in range(0, 100):
 #            start = time()
 #            dumps = swift.pack_info_create(self.pack_data, self.pack_index)
 #            dump_time.append(time() - start)
-#        for i in xrange(0, 100):
+#        for i in range(0, 100):
 #            start = time()
 #            pack_infos = swift.load_pack_info('', file=BytesIO(dumps))
 #            load_time.append(time() - start)

+ 6 - 14
dulwich/diff_tree.py

@@ -27,14 +27,6 @@ from io import BytesIO
 from itertools import chain
 import stat
 
-if sys.version_info[0] == 3:
-    xrange = range
-    izip = zip
-    iteritems = lambda d: d.items()
-else:
-    from itertools import izip
-    iteritems = lambda d: d.iteritems()
-
 from dulwich.objects import (
     S_ISGITLINK,
     TreeEntry,
@@ -111,9 +103,9 @@ def _merge_entries(path, tree1, tree2):
             result.append((entry1, entry2))
             i1 += 1
             i2 += 1
-    for i in xrange(i1, len1):
+    for i in range(i1, len1):
         result.append((entries1[i], _NULL_ENTRY))
-    for i in xrange(i2, len2):
+    for i in range(i2, len2):
         result.append((_NULL_ENTRY, entries2[i]))
     return result
 
@@ -265,7 +257,7 @@ def tree_changes_for_merge(store, parent_tree_ids, tree_id,
     change_type = lambda c: c.type
 
     # Yield only conflicting changes.
-    for _, changes in sorted(iteritems(changes_by_path)):
+    for _, changes in sorted(changes_by_path.items()):
         assert len(changes) == num_parents
         have = [c for c in changes if c is not None]
         if _all_eq(have, change_type, CHANGE_DELETE):
@@ -330,7 +322,7 @@ def _common_bytes(blocks1, blocks2):
     if len(blocks1) > len(blocks2):
         blocks1, blocks2 = blocks2, blocks1
     score = 0
-    for block, count1 in iteritems(blocks1):
+    for block, count1 in blocks1.items():
         count2 = blocks2.get(block)
         if count2:
             score += min(count1, count2)
@@ -458,9 +450,9 @@ class RenameDetector(object):
 
         add_paths = set()
         delete_paths = set()
-        for sha, sha_deletes in iteritems(delete_map):
+        for sha, sha_deletes in delete_map.items():
             sha_adds = add_map[sha]
-            for (old, is_delete), new in izip(sha_deletes, sha_adds):
+            for (old, is_delete), new in zip(sha_deletes, sha_adds):
                 if stat.S_IFMT(old.mode) != stat.S_IFMT(new.mode):
                     continue
                 if is_delete:

+ 1 - 1
dulwich/file.py

@@ -104,7 +104,7 @@ class _GitFile(object):
                      'truncate', 'write', 'writelines')
     def __init__(self, filename, mode, bufsize):
         self._filename = filename
-        self._lockfilename = '%s.lock' % self._filename
+        self._lockfilename = self._filename + b'.lock'
         fd = os.open(self._lockfilename,
             os.O_RDWR | os.O_CREAT | os.O_EXCL | getattr(os, "O_BINARY", 0))
         self._file = os.fdopen(fd, mode, bufsize)

+ 1 - 1
dulwich/greenthreads.py

@@ -132,7 +132,7 @@ class GreenThreadsObjectStoreIterator(ObjectStoreIterator):
             return len(self._shas)
         while len(self.finder.objects_to_send):
             jobs = []
-            for _ in xrange(0, len(self.finder.objects_to_send)):
+            for _ in range(0, len(self.finder.objects_to_send)):
                 jobs.append(self.p.spawn(self.finder.next))
             gevent.joinall(jobs)
             for j in jobs:

+ 3 - 3
dulwich/hooks.py

@@ -100,7 +100,7 @@ class PreCommitShellHook(ShellHook):
     """pre-commit shell hook"""
 
     def __init__(self, controldir):
-        filepath = os.path.join(controldir, 'hooks', 'pre-commit')
+        filepath = os.path.join(controldir, b'hooks', b'pre-commit')
 
         ShellHook.__init__(self, 'pre-commit', filepath, 0)
 
@@ -109,7 +109,7 @@ class PostCommitShellHook(ShellHook):
     """post-commit shell hook"""
 
     def __init__(self, controldir):
-        filepath = os.path.join(controldir, 'hooks', 'post-commit')
+        filepath = os.path.join(controldir, b'hooks', b'post-commit')
 
         ShellHook.__init__(self, 'post-commit', filepath, 0)
 
@@ -122,7 +122,7 @@ class CommitMsgShellHook(ShellHook):
     """
 
     def __init__(self, controldir):
-        filepath = os.path.join(controldir, 'hooks', 'commit-msg')
+        filepath = os.path.join(controldir, b'hooks', b'commit-msg')
 
         def prepare_msg(*args):
             (fd, path) = tempfile.mkstemp()

+ 37 - 22
dulwich/index.py

@@ -23,6 +23,7 @@ import errno
 import os
 import stat
 import struct
+import sys
 
 from dulwich.file import GitFile
 from dulwich.objects import (
@@ -52,9 +53,9 @@ def pathsplit(path):
     :return: Tuple with directory name and basename
     """
     try:
-        (dirname, basename) = path.rsplit("/", 1)
+        (dirname, basename) = path.rsplit(b"/", 1)
     except ValueError:
-        return ("", path)
+        return (b"", path)
     else:
         return (dirname, basename)
 
@@ -63,7 +64,7 @@ def pathjoin(*args):
     """Join a /-delimited path.
 
     """
-    return "/".join([p for p in args if p])
+    return b"/".join([p for p in args if p])
 
 
 def read_cache_time(f):
@@ -122,18 +123,18 @@ def write_cache_entry(f, entry):
     write_cache_time(f, ctime)
     write_cache_time(f, mtime)
     flags = len(name) | (flags &~ 0x0fff)
-    f.write(struct.pack(">LLLLLL20sH", dev & 0xFFFFFFFF, ino & 0xFFFFFFFF, mode, uid, gid, size, hex_to_sha(sha), flags))
+    f.write(struct.pack(b'>LLLLLL20sH', dev & 0xFFFFFFFF, ino & 0xFFFFFFFF, mode, uid, gid, size, hex_to_sha(sha), flags))
     f.write(name)
     real_size = ((f.tell() - beginoffset + 8) & ~7)
-    f.write("\0" * ((beginoffset + real_size) - f.tell()))
+    f.write(b'\0' * ((beginoffset + real_size) - f.tell()))
 
 
 def read_index(f):
     """Read an index file, yielding the individual entries."""
     header = f.read(4)
-    if header != "DIRC":
+    if header != b'DIRC':
         raise AssertionError("Invalid index file header: %r" % header)
-    (version, num_entries) = struct.unpack(">LL", f.read(4 * 2))
+    (version, num_entries) = struct.unpack(b'>LL', f.read(4 * 2))
     assert version in (1, 2)
     for i in range(num_entries):
         yield read_cache_entry(f)
@@ -156,8 +157,8 @@ def write_index(f, entries):
     :param f: File-like object to write to
     :param entries: Iterable over the entries to write
     """
-    f.write("DIRC")
-    f.write(struct.pack(">LL", 2, len(entries)))
+    f.write(b'DIRC')
+    f.write(struct.pack(b'>LL', 2, len(entries)))
     for x in entries:
         write_cache_entry(f, x)
 
@@ -202,6 +203,10 @@ class Index(object):
         self.clear()
         self.read()
 
+    @property
+    def path(self):
+        return self._filename
+
     def __repr__(self):
         return "%s(%r)" % (self.__class__.__name__, self._filename)
 
@@ -263,20 +268,20 @@ class Index(object):
         self._byname = {}
 
     def __setitem__(self, name, x):
-        assert isinstance(name, str)
+        assert isinstance(name, bytes)
         assert len(x) == 10
         # Remove the old entry if any
         self._byname[name] = x
 
     def __delitem__(self, name):
-        assert isinstance(name, str)
+        assert isinstance(name, bytes)
         del self._byname[name]
 
     def iteritems(self):
-        return self._byname.iteritems()
+        return self._byname.items()
 
     def update(self, entries):
-        for name, value in entries.iteritems():
+        for name, value in entries.items():
             self[name] = value
 
     def changes_from_tree(self, object_store, tree, want_unchanged=False):
@@ -312,14 +317,14 @@ def commit_tree(object_store, blobs):
     :return: SHA1 of the created tree.
     """
 
-    trees = {"": {}}
+    trees = {b'': {}}
 
     def add_tree(path):
         if path in trees:
             return trees[path]
         dirname, basename = pathsplit(path)
         t = add_tree(dirname)
-        assert isinstance(basename, str)
+        assert isinstance(basename, bytes)
         newtree = {}
         t[basename] = newtree
         trees[path] = newtree
@@ -332,7 +337,7 @@ def commit_tree(object_store, blobs):
 
     def build_tree(path):
         tree = Tree()
-        for basename, entry in trees[path].iteritems():
+        for basename, entry in trees[path].items():
             if isinstance(entry, dict):
                 mode = stat.S_IFDIR
                 sha = build_tree(pathjoin(path, basename))
@@ -341,7 +346,7 @@ def commit_tree(object_store, blobs):
             tree.add(basename, mode, sha)
         object_store.add_object(tree)
         return tree.id
-    return build_tree("")
+    return build_tree(b'')
 
 
 def commit_index(object_store, index):
@@ -431,7 +436,7 @@ def build_file_from_blob(blob, mode, target_path, honor_filemode=True):
             os.chmod(target_path, mode)
 
 
-INVALID_DOTNAMES = (".git", ".", "..", "")
+INVALID_DOTNAMES = (b".git", b".", b"..", b"")
 
 
 def validate_path_element_default(element):
@@ -439,17 +444,17 @@ def validate_path_element_default(element):
 
 
 def validate_path_element_ntfs(element):
-    stripped = element.rstrip(". ").lower()
+    stripped = element.rstrip(b". ").lower()
     if stripped in INVALID_DOTNAMES:
         return False
-    if stripped == "git~1":
+    if stripped == b"git~1":
         return False
     return True
 
 
 def validate_path(path, element_validator=validate_path_element_default):
     """Default path validator that just checks for .git/."""
-    parts = path.split("/")
+    parts = path.split(b"/")
     for p in parts:
         if not element_validator(p):
             return False
@@ -475,6 +480,9 @@ def build_index_from_tree(prefix, index_path, object_store, tree_id,
         in a working dir. Suiteable only for fresh clones.
     """
 
+    if not isinstance(prefix, bytes):
+        prefix = prefix.encode(sys.getfilesystemencoding())
+
     index = Index(index_path)
 
     for entry in object_store.iter_tree_contents(tree_id):
@@ -508,7 +516,11 @@ def blob_from_path_and_stat(path, st):
         with open(path, 'rb') as f:
             blob.data = f.read()
     else:
-        blob.data = os.readlink(path)
+        if not isinstance(path, bytes):
+            blob.data = os.readlink(path.encode(sys.getfilesystemencoding()))
+        else:
+            blob.data = os.readlink(path)
+
     return blob
 
 
@@ -520,6 +532,9 @@ def get_unstaged_changes(index, path):
     :return: iterator over paths with unstaged changes
     """
     # For each entry in the index check the sha1 & ensure not staged
+    if not isinstance(path, bytes):
+        path = path.encode(sys.getfilesystemencoding())
+
     for name, entry in index.iteritems():
         fp = os.path.join(path, name)
         blob = blob_from_path_and_stat(fp, os.lstat(fp))

+ 40 - 33
dulwich/object_store.py

@@ -26,6 +26,7 @@ import errno
 from itertools import chain
 import os
 import stat
+import sys
 import tempfile
 
 from dulwich.diff_tree import (
@@ -62,16 +63,16 @@ from dulwich.pack import (
     PackStreamCopier,
     )
 
-INFODIR = 'info'
-PACKDIR = 'pack'
+INFODIR = b'info'
+PACKDIR = b'pack'
 
 
 class BaseObjectStore(object):
     """Object store interface."""
 
     def determine_wants_all(self, refs):
-        return [sha for (ref, sha) in refs.iteritems()
-                if not sha in self and not ref.endswith("^{}") and
+        return [sha for (ref, sha) in refs.items()
+                if not sha in self and not ref.endswith(b"^{}") and
                    not sha == ZERO_SHA]
 
     def iter_shas(self, shas):
@@ -335,7 +336,7 @@ class PackBasedObjectStore(BaseObjectStore):
 
     def __iter__(self):
         """Iterate over the SHAs that are present in this store."""
-        iterables = self.packs + [self._iter_loose_objects()] + [self._iter_alternate_objects()]
+        iterables = list(self.packs) + [self._iter_loose_objects()] + [self._iter_alternate_objects()]
         return chain(*iterables)
 
     def contains_loose(self, sha):
@@ -424,33 +425,31 @@ class DiskObjectStore(PackBasedObjectStore):
 
     def _read_alternate_paths(self):
         try:
-            f = GitFile(os.path.join(self.path, "info", "alternates"),
+            f = GitFile(os.path.join(self.path, INFODIR, b'alternates'),
                     'rb')
         except (OSError, IOError) as e:
             if e.errno == errno.ENOENT:
-                return []
+                return
             raise
-        ret = []
         with f:
             for l in f.readlines():
-                l = l.rstrip("\n")
-                if l[0] == "#":
+                l = l.rstrip(b"\n")
+                if l[0] == b"#":
                     continue
                 if os.path.isabs(l):
-                    ret.append(l)
+                    yield l
                 else:
-                    ret.append(os.path.join(self.path, l))
-            return ret
+                    yield os.path.join(self.path, l)
 
     def add_alternate_path(self, path):
         """Add an alternate path to this object store.
         """
         try:
-            os.mkdir(os.path.join(self.path, "info"))
+            os.mkdir(os.path.join(self.path, INFODIR))
         except OSError as e:
             if e.errno != errno.EEXIST:
                 raise
-        alternates_path = os.path.join(self.path, "info/alternates")
+        alternates_path = os.path.join(self.path, INFODIR, b'alternates')
         with GitFile(alternates_path, 'wb') as f:
             try:
                 orig_f = open(alternates_path, 'rb')
@@ -460,7 +459,7 @@ class DiskObjectStore(PackBasedObjectStore):
             else:
                 with orig_f:
                     f.write(orig_f.read())
-            f.write("%s\n" % path)
+            f.write(path + b"\n")
 
         if not os.path.isabs(path):
             path = os.path.join(self.path, path)
@@ -478,9 +477,10 @@ class DiskObjectStore(PackBasedObjectStore):
         self._pack_cache_time = os.stat(self.pack_dir).st_mtime
         pack_files = set()
         for name in pack_dir_contents:
+            assert type(name) is bytes
             # TODO: verify that idx exists first
-            if name.startswith("pack-") and name.endswith(".pack"):
-                pack_files.add(name[:-len(".pack")])
+            if name.startswith(b'pack-') and name.endswith(b'.pack'):
+                pack_files.add(name[:-len(b'.pack')])
 
         # Open newly appeared pack files
         for f in pack_files:
@@ -507,7 +507,7 @@ class DiskObjectStore(PackBasedObjectStore):
             if len(base) != 2:
                 continue
             for rest in os.listdir(os.path.join(self.path, base)):
-                yield base+rest
+                yield (base+rest)
 
     def _get_loose_object(self, sha):
         path = self._get_shafile_path(sha)
@@ -521,6 +521,11 @@ class DiskObjectStore(PackBasedObjectStore):
     def _remove_loose_object(self, sha):
         os.remove(self._get_shafile_path(sha))
 
+    def _get_pack_basepath(self, entries):
+        suffix = iter_sha1(entry[0] for entry in entries)
+        # TODO: Handle self.pack_dir being bytes
+        return os.path.join(self.pack_dir, b"pack-" + suffix)
+
     def _complete_thin_pack(self, f, path, copier, indexer):
         """Move a specific file containing a pack into the pack directory.
 
@@ -560,12 +565,11 @@ class DiskObjectStore(PackBasedObjectStore):
 
         # Move the pack in.
         entries.sort()
-        pack_base_name = os.path.join(
-          self.pack_dir, 'pack-' + iter_sha1(e[0] for e in entries))
-        os.rename(path, pack_base_name + '.pack')
+        pack_base_name = self._get_pack_basepath(entries)
+        os.rename(path, pack_base_name + b'.pack')
 
         # Write the index.
-        index_file = GitFile(pack_base_name + '.idx', 'wb')
+        index_file = GitFile(pack_base_name + b'.idx', 'wb')
         try:
             write_pack_index_v2(index_file, entries, pack_sha)
             index_file.close()
@@ -592,7 +596,10 @@ class DiskObjectStore(PackBasedObjectStore):
         :return: A Pack object pointing at the now-completed thin pack in the
             objects/pack directory.
         """
-        fd, path = tempfile.mkstemp(dir=self.path, prefix='tmp_pack_')
+        fd, path = tempfile.mkstemp(
+            dir=self.path.decode(sys.getfilesystemencoding()),
+            prefix='tmp_pack_')
+        path = path.encode(sys.getfilesystemencoding())
         with os.fdopen(fd, 'w+b') as f:
             indexer = PackIndexer(f, resolve_ext_ref=self.get_raw)
             copier = PackStreamCopier(read_all, read_some, f,
@@ -610,11 +617,10 @@ class DiskObjectStore(PackBasedObjectStore):
         """
         with PackData(path) as p:
             entries = p.sorted_entries()
-            basename = os.path.join(self.pack_dir,
-                "pack-%s" % iter_sha1(entry[0] for entry in entries))
-            with GitFile(basename+".idx", "wb") as f:
+            basename = self._get_pack_basepath(entries)
+            with GitFile(basename+b'.idx', "wb") as f:
                 write_pack_index_v2(f, entries, p.get_stored_checksum())
-        os.rename(path, basename + ".pack")
+        os.rename(path, basename + b'.pack')
         final_pack = Pack(basename)
         self._add_known_pack(basename, final_pack)
         return final_pack
@@ -626,7 +632,8 @@ class DiskObjectStore(PackBasedObjectStore):
             call when the pack is finished and an abort
             function.
         """
-        fd, path = tempfile.mkstemp(dir=self.pack_dir, suffix=".pack")
+        pack_dir_str = self.pack_dir.decode(sys.getfilesystemencoding())
+        fd, path = tempfile.mkstemp(dir=pack_dir_str, suffix='.pack')
         f = os.fdopen(fd, 'wb')
         def commit():
             os.fsync(fd)
@@ -646,13 +653,13 @@ class DiskObjectStore(PackBasedObjectStore):
 
         :param obj: Object to add
         """
-        dir = os.path.join(self.path, obj.id[:2])
+        path = self._get_shafile_path(obj.id)
+        dir = os.path.dirname(path)
         try:
             os.mkdir(dir)
         except OSError as e:
             if e.errno != errno.EEXIST:
                 raise
-        path = os.path.join(dir, obj.id[2:])
         if os.path.exists(path):
             return # Already there, no need to write again
         with GitFile(path, 'wb') as f:
@@ -665,7 +672,7 @@ class DiskObjectStore(PackBasedObjectStore):
         except OSError as e:
             if e.errno != errno.EEXIST:
                 raise
-        os.mkdir(os.path.join(path, "info"))
+        os.mkdir(os.path.join(path, INFODIR))
         os.mkdir(os.path.join(path, PACKDIR))
         return cls(path)
 
@@ -695,7 +702,7 @@ class MemoryObjectStore(BaseObjectStore):
 
     def __iter__(self):
         """Iterate over the SHAs that are present in this store."""
-        return self._data.iterkeys()
+        return iter(self._data.keys())
 
     @property
     def packs(self):

+ 17 - 15
dulwich/objects.py

@@ -40,11 +40,6 @@ from dulwich.errors import (
     )
 from dulwich.file import GitFile
 
-if sys.version_info[0] == 2:
-    iteritems = lambda d: d.iteritems()
-else:
-    iteritems = lambda d: d.items()
-
 
 ZERO_SHA = b'0' * 40
 
@@ -92,7 +87,7 @@ def sha_to_hex(sha):
 
 def hex_to_sha(hex):
     """Takes a hex sha and returns a binary sha"""
-    assert len(hex) == 40, "Incorrent length of hexsha: %s" % hex
+    assert len(hex) == 40, "Incorrect length of hexsha: %s" % hex
     try:
         return binascii.unhexlify(hex)
     except TypeError as exc:
@@ -101,17 +96,26 @@ def hex_to_sha(hex):
         raise ValueError(exc.args[0])
 
 
+def valid_hexsha(hex):
+    if len(hex) != 40:
+        return False
+    try:
+        binascii.unhexlify(hex)
+    except (TypeError, binascii.Error):
+        return False
+    else:
+        return True
+
+
 def hex_to_filename(path, hex):
     """Takes a hex sha and returns its filename relative to the given path."""
     # os.path.join accepts bytes or unicode, but all args must be of the same
     # type. Make sure that hex which is expected to be bytes, is the same type
     # as path.
-    if getattr(path, 'encode', None) is not None:
-        hex = hex.decode('ascii')
-    dir = hex[:2]
+    directory = hex[:2]
     file = hex[2:]
     # Check from object dir
-    return os.path.join(path, dir, file)
+    return os.path.join(path, directory, file)
 
 
 def filename_to_hex(filename):
@@ -162,9 +166,7 @@ def check_hexsha(hex, error_msg):
     :param error_msg: Error message to use in exception
     :raise ObjectFormatException: Raised when the string is not valid
     """
-    try:
-        hex_to_sha(hex)
-    except (TypeError, AssertionError, ValueError):
+    if not valid_hexsha(hex):
         raise ObjectFormatException("%s %s" % (error_msg, hex))
 
 
@@ -273,6 +275,7 @@ class ShaFile(object):
             self._ensure_parsed()
         elif self._needs_serialization:
             self._chunked_text = self._serialize()
+            self._needs_serialization = False
         return self._chunked_text
 
     def as_raw_string(self):
@@ -549,7 +552,6 @@ class Blob(ShaFile):
     def _serialize(self):
         if not self._chunked_text:
             self._ensure_parsed()
-        self._needs_serialization = False
         return self._chunked_text
 
     def _deserialize(self, chunks):
@@ -785,7 +787,7 @@ def sorted_tree_items(entries, name_order):
     :return: Iterator over (name, mode, hexsha)
     """
     key_func = name_order and key_entry_name_order or key_entry
-    for name, entry in sorted(iteritems(entries), key=key_func):
+    for name, entry in sorted(entries.items(), key=key_func):
         mode, hexsha = entry
         # Stricter type checks than normal to mirror checks in the C version.
         mode = int(mode)

+ 4 - 0
dulwich/objectspec.py

@@ -27,6 +27,8 @@ def parse_object(repo, objectish):
     :return: A git object
     :raise KeyError: If the object can not be found
     """
+    if getattr(objectish, "encode", None) is not None:
+        objectish = objectish.encode('ascii')
     return repo[objectish]
 
 
@@ -39,4 +41,6 @@ def parse_commit_range(repo, committishs):
     :raise KeyError: When the reference commits can not be found
     :raise ValueError: If the range can not be parsed
     """
+    if getattr(committishs, "encode", None) is not None:
+        committishs = committishs.encode('ascii')
     return iter([repo[committishs]])

+ 87 - 84
dulwich/pack.py

@@ -38,6 +38,7 @@ from collections import (
     deque,
     )
 import difflib
+import struct
 
 from itertools import chain
 try:
@@ -48,6 +49,7 @@ except ImportError:
     izip = zip
 
 import os
+import sys
 
 try:
     import mmap
@@ -56,8 +58,8 @@ except ImportError:
 else:
     has_mmap = True
 
-# For some reason the above try, except fails to set has_mmap = False
-if os.uname()[0] == 'Plan9':
+# For some reason the above try, except fails to set has_mmap = False for plan9
+if sys.platform == 'Plan9':
     has_mmap = False
 
 from hashlib import sha1
@@ -65,7 +67,6 @@ from os import (
     SEEK_CUR,
     SEEK_END,
     )
-import struct
 from struct import unpack_from
 import zlib
 
@@ -104,7 +105,7 @@ def take_msb_bytes(read, crc32=None):
         b = read(1)
         if crc32 is not None:
             crc32 = binascii.crc32(b, crc32)
-        ret.append(ord(b))
+        ret.append(ord(b[:1]))
     return ret, crc32
 
 
@@ -260,7 +261,7 @@ def iter_sha1(iter):
     sha = sha1()
     for name in iter:
         sha.update(name)
-    return sha.hexdigest()
+    return sha.hexdigest().encode('ascii')
 
 
 def load_pack_index(path):
@@ -303,8 +304,8 @@ def load_pack_index_file(path, f):
     :return: A PackIndex loaded from the given file
     """
     contents, size = _load_file_contents(f)
-    if contents[:4] == '\377tOc':
-        version = struct.unpack('>L', contents[4:8])[0]
+    if contents[:4] == b'\377tOc':
+        version = struct.unpack(b'>L', contents[4:8])[0]
         if version == 2:
             return PackIndex2(path, file=f, contents=contents,
                 size=size)
@@ -327,10 +328,9 @@ def bisect_find_sha(start, end, sha, unpack_name):
     while start <= end:
         i = (start + end) // 2
         file_sha = unpack_name(i)
-        x = cmp(file_sha, sha)
-        if x < 0:
+        if file_sha < sha:
             start = i + 1
-        elif x > 0:
+        elif file_sha > sha:
             end = i - 1
         else:
             return i
@@ -546,14 +546,14 @@ class FilePackIndex(PackIndex):
 
         :return: 20-byte binary digest
         """
-        return str(self._contents[-40:-20])
+        return bytes(self._contents[-40:-20])
 
     def get_stored_checksum(self):
         """Return the SHA1 checksum stored for this index.
 
         :return: 20-byte binary digest
         """
-        return str(self._contents[-20:])
+        return bytes(self._contents[-20:])
 
     def _object_index(self, sha):
         """See object_index.
@@ -561,7 +561,7 @@ class FilePackIndex(PackIndex):
         :param sha: A *binary* SHA string. (20 characters long)_
         """
         assert len(sha) == 20
-        idx = ord(sha[0])
+        idx = ord(sha[:1])
         if idx == 0:
             start = 0
         else:
@@ -604,9 +604,9 @@ class PackIndex2(FilePackIndex):
 
     def __init__(self, filename, file=None, contents=None, size=None):
         super(PackIndex2, self).__init__(filename, file, contents, size)
-        if self._contents[:4] != '\377tOc':
+        if self._contents[:4] != b'\377tOc':
             raise AssertionError('Not a v2 pack index file')
-        (self.version, ) = unpack_from('>L', self._contents, 4)
+        (self.version, ) = unpack_from(b'>L', self._contents, 4)
         if self.version != 2:
             raise AssertionError('Version was %d' % self.version)
         self._fan_out_table = self._read_fan_out_table(8)
@@ -648,17 +648,20 @@ def read_pack_header(read):
     header = read(12)
     if not header:
         return None, None
-    if header[:4] != 'PACK':
+    if header[:4] != b'PACK':
         raise AssertionError('Invalid pack header %r' % header)
-    (version,) = unpack_from('>L', header, 4)
+    (version,) = unpack_from(b'>L', header, 4)
     if version not in (2, 3):
         raise AssertionError('Version was %d' % version)
-    (num_objects,) = unpack_from('>L', header, 8)
+    (num_objects,) = unpack_from(b'>L', header, 8)
     return (version, num_objects)
 
 
 def chunks_length(chunks):
-    return sum(imap(len, chunks))
+    if isinstance(chunks, bytes):
+        return len(chunks)
+    else:
+        return sum(imap(len, chunks))
 
 
 def unpack_object(read_all, read_some=None, compute_crc32=False,
@@ -774,8 +777,7 @@ class PackStreamReader(object):
         else:
             to_pop = max(n + tn - 20, 0)
             to_add = n
-        for _ in xrange(to_pop):
-            self.sha.update(self._trailer.popleft())
+        self.sha.update(bytes(bytearray([self._trailer.popleft() for _ in range(to_pop)])))
         self._trailer.extend(data[-to_add:])
 
         # hash everything but the trailer
@@ -838,7 +840,7 @@ class PackStreamReader(object):
         if pack_version is None:
             return
 
-        for i in xrange(self._num_objects):
+        for i in range(self._num_objects):
             offset = self.offset
             unpacked, unused = unpack_object(
               self.read, read_some=self.recv, compute_crc32=compute_crc32,
@@ -861,7 +863,7 @@ class PackStreamReader(object):
             # read buffer and (20 - N) come from the wire.
             self.read(20)
 
-        pack_sha = ''.join(self._trailer)
+        pack_sha = bytearray(self._trailer)
         if pack_sha != self.sha.digest():
             raise ChecksumMismatch(sha_to_hex(pack_sha), self.sha.hexdigest())
 
@@ -912,8 +914,11 @@ def obj_sha(type, chunks):
     """Compute the SHA for a numeric type and object chunks."""
     sha = sha1()
     sha.update(object_header(type, chunks_length(chunks)))
-    for chunk in chunks:
-        sha.update(chunk)
+    if isinstance(chunks, bytes):
+        sha.update(chunks)
+    else:
+        for chunk in chunks:
+            sha.update(chunk)
     return sha.digest()
 
 
@@ -1071,7 +1076,7 @@ class PackData(object):
             assert isinstance(type, int)
         elif type == REF_DELTA:
             (basename, delta) = obj
-            assert isinstance(basename, str) and len(basename) == 20
+            assert isinstance(basename, bytes) and len(basename) == 20
             base_offset, type, base_obj = get_ref(basename)
             assert isinstance(type, int)
         type, base_chunks = self.resolve_object(base_offset, type, base_obj)
@@ -1086,7 +1091,7 @@ class PackData(object):
 
     def iterobjects(self, progress=None, compute_crc32=True):
         self._file.seek(self._header_size)
-        for i in xrange(1, self._num_objects + 1):
+        for i in range(1, self._num_objects + 1):
             offset = self._file.tell()
             unpacked, unused = unpack_object(
               self._file.read, compute_crc32=compute_crc32)
@@ -1104,7 +1109,7 @@ class PackData(object):
         if self._num_objects is None:
             return
 
-        for _ in xrange(self._num_objects):
+        for _ in range(self._num_objects):
             offset = self._file.tell()
             unpacked, unused = unpack_object(
               self._file.read, compute_crc32=False)
@@ -1200,8 +1205,6 @@ class PackData(object):
             return self._offset_cache[offset]
         except KeyError:
             pass
-        assert isinstance(offset, long) or isinstance(offset, int),\
-                'offset was %r' % offset
         assert offset >= self._header_size
         self._file.seek(offset)
         unpacked, _ = unpack_object(self._file.read)
@@ -1280,7 +1283,7 @@ class DeltaChainIterator(object):
             self._ensure_no_pending()
             return
 
-        for base_sha, pending in sorted(self._pending_ref.iteritems()):
+        for base_sha, pending in sorted(self._pending_ref.items()):
             if base_sha not in self._pending_ref:
                 continue
             try:
@@ -1357,7 +1360,7 @@ class SHA1Reader(object):
 
     def __init__(self, f):
         self.f = f
-        self.sha1 = sha1('')
+        self.sha1 = sha1(b'')
 
     def read(self, num=None):
         data = self.f.read(num)
@@ -1382,7 +1385,7 @@ class SHA1Writer(object):
     def __init__(self, f):
         self.f = f
         self.length = 0
-        self.sha1 = sha1('')
+        self.sha1 = sha1(b'')
 
     def write(self, data):
         self.sha1.update(data)
@@ -1416,14 +1419,14 @@ def pack_object_header(type_num, delta_base, size):
     :param size: Uncompressed object size.
     :return: A header for a packed object.
     """
-    header = ''
+    header = []
     c = (type_num << 4) | (size & 15)
     size >>= 4
     while size:
-        header += (chr(c | 0x80))
+        header.append(c | 0x80)
         c = size & 0x7f
         size >>= 7
-    header += chr(c)
+    header.append(c)
     if type_num == OFS_DELTA:
         ret = [delta_base & 0x7f]
         delta_base >>= 7
@@ -1431,11 +1434,11 @@ def pack_object_header(type_num, delta_base, size):
             delta_base -= 1
             ret.insert(0, 0x80 | (delta_base & 0x7f))
             delta_base >>= 7
-        header += ''.join([chr(x) for x in ret])
+        header.extend(ret)
     elif type_num == REF_DELTA:
         assert len(delta_base) == 20
         header += delta_base
-    return header
+    return bytearray(header)
 
 
 def write_pack_object(f, type, object, sha=None):
@@ -1450,7 +1453,7 @@ def write_pack_object(f, type, object, sha=None):
         delta_base, object = object
     else:
         delta_base = None
-    header = pack_object_header(type, delta_base, len(object))
+    header = bytes(pack_object_header(type, delta_base, len(object)))
     comp_data = zlib.compress(object)
     crc32 = 0
     for data in (header, comp_data):
@@ -1471,20 +1474,20 @@ def write_pack(filename, objects, deltify=None, delta_window_size=None):
     :param deltify: Whether to deltify pack objects
     :return: Tuple with checksum of pack file and index file
     """
-    with GitFile(filename + '.pack', 'wb') as f:
+    with GitFile(filename + b'.pack', 'wb') as f:
         entries, data_sum = write_pack_objects(f, objects,
             delta_window_size=delta_window_size, deltify=deltify)
-    entries = [(k, v[0], v[1]) for (k, v) in entries.iteritems()]
+    entries = [(k, v[0], v[1]) for (k, v) in entries.items()]
     entries.sort()
-    with GitFile(filename + '.idx', 'wb') as f:
+    with GitFile(filename + b'.idx', 'wb') as f:
         return data_sum, write_pack_index_v2(f, entries, data_sum)
 
 
 def write_pack_header(f, num_objects):
     """Write a pack header for the given number of objects."""
-    f.write('PACK')                          # Pack header
-    f.write(struct.pack('>L', 2))            # Pack version
-    f.write(struct.pack('>L', num_objects))  # Number of objects in pack
+    f.write(b'PACK')                          # Pack header
+    f.write(struct.pack(b'>L', 2))            # Pack version
+    f.write(struct.pack(b'>L', num_objects))  # Number of objects in pack
 
 
 def deltify_pack_objects(objects, window_size=None):
@@ -1584,7 +1587,7 @@ def write_pack_index_v1(f, entries, pack_checksum):
     f = SHA1Writer(f)
     fan_out_table = defaultdict(lambda: 0)
     for (name, offset, entry_checksum) in entries:
-        fan_out_table[ord(name[0])] += 1
+        fan_out_table[ord(name[:1])] += 1
     # Fan-out table
     for i in range(0x100):
         f.write(struct.pack('>L', fan_out_table[i]))
@@ -1599,14 +1602,14 @@ def write_pack_index_v1(f, entries, pack_checksum):
 
 
 def _delta_encode_size(size):
-    ret = ''
+    ret = bytearray()
     c = size & 0x7f
     size >>= 7
     while size:
-        ret += chr(c | 0x80)
+        ret.append(c | 0x80)
         c = size & 0x7f
         size >>= 7
-    ret += chr(c)
+    ret.append(c)
     return ret
 
 
@@ -1616,17 +1619,17 @@ def _delta_encode_size(size):
 _MAX_COPY_LEN = 0xffff
 
 def _encode_copy_operation(start, length):
-    scratch = ''
+    scratch = []
     op = 0x80
     for i in range(4):
         if start & 0xff << i*8:
-            scratch += chr((start >> i*8) & 0xff)
+            scratch.append((start >> i*8) & 0xff)
             op |= 1 << i
     for i in range(2):
         if length & 0xff << i*8:
-            scratch += chr((length >> i*8) & 0xff)
+            scratch.append((length >> i*8) & 0xff)
             op |= 1 << (4+i)
-    return chr(op) + scratch
+    return bytearray([op] + scratch)
 
 
 def create_delta(base_buf, target_buf):
@@ -1635,10 +1638,10 @@ def create_delta(base_buf, target_buf):
     :param base_buf: Base buffer
     :param target_buf: Target buffer
     """
-    assert isinstance(base_buf, str)
-    assert isinstance(target_buf, str)
-    out_buf = ''
-     # write delta header
+    assert isinstance(base_buf, bytes)
+    assert isinstance(target_buf, bytes)
+    out_buf = bytearray()
+    # write delta header
     out_buf += _delta_encode_size(len(base_buf))
     out_buf += _delta_encode_size(len(target_buf))
     # write out delta opcodes
@@ -1663,13 +1666,13 @@ def create_delta(base_buf, target_buf):
             s = j2 - j1
             o = j1
             while s > 127:
-                out_buf += chr(127)
-                out_buf += target_buf[o:o+127]
+                out_buf.append(127)
+                out_buf += bytearray(target_buf[o:o+127])
                 s -= 127
                 o += 127
-            out_buf += chr(s)
-            out_buf += target_buf[o:o+s]
-    return out_buf
+            out_buf.append(s)
+            out_buf += bytearray(target_buf[o:o+s])
+    return bytes(out_buf)
 
 
 def apply_delta(src_buf, delta):
@@ -1678,10 +1681,10 @@ def apply_delta(src_buf, delta):
     :param src_buf: Source buffer
     :param delta: Delta instructions
     """
-    if not isinstance(src_buf, str):
-        src_buf = ''.join(src_buf)
-    if not isinstance(delta, str):
-        delta = ''.join(delta)
+    if not isinstance(src_buf, bytes):
+        src_buf = b''.join(src_buf)
+    if not isinstance(delta, bytes):
+        delta = b''.join(delta)
     out = []
     index = 0
     delta_length = len(delta)
@@ -1689,7 +1692,7 @@ def apply_delta(src_buf, delta):
         size = 0
         i = 0
         while delta:
-            cmd = ord(delta[index])
+            cmd = ord(delta[index:index+1])
             index += 1
             size |= (cmd & ~0x80) << i
             i += 7
@@ -1700,20 +1703,20 @@ def apply_delta(src_buf, delta):
     dest_size, index = get_delta_header_size(delta, index)
     assert src_size == len(src_buf), '%d vs %d' % (src_size, len(src_buf))
     while index < delta_length:
-        cmd = ord(delta[index])
+        cmd = ord(delta[index:index+1])
         index += 1
         if cmd & 0x80:
             cp_off = 0
             for i in range(4):
                 if cmd & (1 << i):
-                    x = ord(delta[index])
+                    x = ord(delta[index:index+1])
                     index += 1
                     cp_off |= x << (i * 8)
             cp_size = 0
             # Version 3 packs can contain copy sizes larger than 64K.
             for i in range(3):
                 if cmd & (1 << (4+i)):
-                    x = ord(delta[index])
+                    x = ord(delta[index:index+1])
                     index += 1
                     cp_size |= x << (i * 8)
             if cp_size == 0:
@@ -1748,28 +1751,28 @@ def write_pack_index_v2(f, entries, pack_checksum):
     :return: The SHA of the index file written
     """
     f = SHA1Writer(f)
-    f.write('\377tOc') # Magic!
+    f.write(b'\377tOc')  # Magic!
     f.write(struct.pack('>L', 2))
     fan_out_table = defaultdict(lambda: 0)
     for (name, offset, entry_checksum) in entries:
-        fan_out_table[ord(name[0])] += 1
+        fan_out_table[ord(name[:1])] += 1
     # Fan-out table
     largetable = []
     for i in range(0x100):
-        f.write(struct.pack('>L', fan_out_table[i]))
+        f.write(struct.pack(b'>L', fan_out_table[i]))
         fan_out_table[i+1] += fan_out_table[i]
     for (name, offset, entry_checksum) in entries:
         f.write(name)
     for (name, offset, entry_checksum) in entries:
-        f.write(struct.pack('>L', entry_checksum))
+        f.write(struct.pack(b'>L', entry_checksum))
     for (name, offset, entry_checksum) in entries:
         if offset < 2**31:
-            f.write(struct.pack('>L', offset))
+            f.write(struct.pack(b'>L', offset))
         else:
-            f.write(struct.pack('>L', 2**31 + len(largetable)))
+            f.write(struct.pack(b'>L', 2**31 + len(largetable)))
             largetable.append(offset)
     for offset in largetable:
-        f.write(struct.pack('>Q', offset))
+        f.write(struct.pack(b'>Q', offset))
     assert len(pack_checksum) == 20
     f.write(pack_checksum)
     return f.write_sha()
@@ -1782,8 +1785,8 @@ class Pack(object):
         self._basename = basename
         self._data = None
         self._idx = None
-        self._idx_path = self._basename + '.idx'
-        self._data_path = self._basename + '.pack'
+        self._idx_path = self._basename + b'.idx'
+        self._data_path = self._basename + b'.pack'
         self._data_load = lambda: PackData(self._data_path)
         self._idx_load = lambda: load_pack_index(self._idx_path)
         self.resolve_ext_ref = resolve_ext_ref
@@ -1792,7 +1795,7 @@ class Pack(object):
     def from_lazy_objects(self, data_fn, idx_fn):
         """Create a new pack object from callables to load pack data and
         index objects."""
-        ret = Pack('')
+        ret = Pack(b'')
         ret._data_load = data_fn
         ret._idx_load = idx_fn
         return ret
@@ -1800,7 +1803,7 @@ class Pack(object):
     @classmethod
     def from_objects(self, data, idx):
         """Create a new pack object from pack data and index objects."""
-        ret = Pack('')
+        ret = Pack(b'')
         ret._data_load = lambda: data
         ret._idx_load = lambda: idx
         return ret
@@ -1889,7 +1892,7 @@ class Pack(object):
         offset = self.index.object_index(sha1)
         obj_type, obj = self.data.get_object_at(offset)
         type_num, chunks = self.data.resolve_object(offset, obj_type, obj)
-        return type_num, ''.join(chunks)
+        return type_num, b''.join(chunks)
 
     def __getitem__(self, sha1):
         """Retrieve the specified SHA1."""
@@ -1927,11 +1930,11 @@ class Pack(object):
                     determine whether or not a .keep file is obsolete.
         :return: The path of the .keep file, as a string.
         """
-        keepfile_name = '%s.keep' % self._basename
+        keepfile_name = self._basename + b'.keep'
         with GitFile(keepfile_name, 'wb') as keepfile:
             if msg:
                 keepfile.write(msg)
-                keepfile.write('\n')
+                keepfile.write(b'\n')
         return keepfile_name
 
 

+ 48 - 53
dulwich/patch.py

@@ -115,6 +115,20 @@ def is_binary(content):
     return '\0' in content[:FIRST_FEW_BYTES]
 
 
+def shortid(hexsha):
+    if hexsha is None:
+        return "0" * 7
+    else:
+        return hexsha[:7]
+
+
+def patch_filename(p, root):
+    if p is None:
+        return "/dev/null"
+    else:
+        return root + "/" + p
+
+
 def write_object_diff(f, store, old_file, new_file, diff_binary=False):
     """Write the diff for an object.
 
@@ -129,12 +143,8 @@ def write_object_diff(f, store, old_file, new_file, diff_binary=False):
     """
     (old_path, old_mode, old_id) = old_file
     (new_path, new_mode, new_id) = new_file
-    def shortid(hexsha):
-        if hexsha is None:
-            return "0" * 7
-        else:
-            return hexsha[:7]
-
+    old_path = patch_filename(old_path, "a")
+    new_path = patch_filename(new_path, "b")
     def content(mode, hexsha):
         if hexsha is None:
             return ''
@@ -148,27 +158,8 @@ def write_object_diff(f, store, old_file, new_file, diff_binary=False):
             return []
         else:
             return content.splitlines(True)
-
-    if old_path is None:
-        old_path = "/dev/null"
-    else:
-        old_path = "a/%s" % old_path
-    if new_path is None:
-        new_path = "/dev/null"
-    else:
-        new_path = "b/%s" % new_path
-    f.write("diff --git %s %s\n" % (old_path, new_path))
-    if old_mode != new_mode:
-        if new_mode is not None:
-            if old_mode is not None:
-                f.write("old mode %o\n" % old_mode)
-            f.write("new mode %o\n" % new_mode)
-        else:
-            f.write("deleted mode %o\n" % old_mode)
-    f.write("index %s..%s" % (shortid(old_id), shortid(new_id)))
-    if new_mode is not None:
-        f.write(" %o" % new_mode)
-    f.write("\n")
+    f.writelines(gen_diff_header(
+        (old_path, new_path), (old_mode, new_mode), (old_id, new_id)))
     old_content = content(old_mode, old_id)
     new_content = content(new_mode, new_id)
     if not diff_binary and (is_binary(old_content) or is_binary(new_content)):
@@ -178,8 +169,32 @@ def write_object_diff(f, store, old_file, new_file, diff_binary=False):
             old_path, new_path))
 
 
+def gen_diff_header(paths, modes, shas):
+    """Write a blob diff header.
+
+    :param paths: Tuple with old and new path
+    :param modes: Tuple with old and new modes
+    :param shas: Tuple with old and new shas
+    """
+    (old_path, new_path) = paths
+    (old_mode, new_mode) = modes
+    (old_sha, new_sha) = shas
+    yield "diff --git %s %s\n" % (old_path, new_path)
+    if old_mode != new_mode:
+        if new_mode is not None:
+            if old_mode is not None:
+                yield "old mode %o\n" % old_mode
+            yield "new mode %o\n" % new_mode
+        else:
+            yield "deleted mode %o\n" % old_mode
+    yield "index " + shortid(old_sha) + ".." + shortid(new_sha)
+    if new_mode is not None:
+        yield " %o" % new_mode
+    yield "\n"
+
+
 def write_blob_diff(f, old_file, new_file):
-    """Write diff file header.
+    """Write blob diff.
 
     :param f: File-like object to write to
     :param old_file: (path, mode, hexsha) tuple (None if nonexisting)
@@ -189,36 +204,16 @@ def write_blob_diff(f, old_file, new_file):
     """
     (old_path, old_mode, old_blob) = old_file
     (new_path, new_mode, new_blob) = new_file
-    def blob_id(blob):
-        if blob is None:
-            return "0" * 7
-        else:
-            return blob.id[:7]
+    old_path = patch_filename(old_path, "a")
+    new_path = patch_filename(new_path, "b")
     def lines(blob):
         if blob is not None:
             return blob.data.splitlines(True)
         else:
             return []
-    if old_path is None:
-        old_path = "/dev/null"
-    else:
-        old_path = "a/%s" % old_path
-    if new_path is None:
-        new_path = "/dev/null"
-    else:
-        new_path = "b/%s" % new_path
-    f.write("diff --git %s %s\n" % (old_path, new_path))
-    if old_mode != new_mode:
-        if new_mode is not None:
-            if old_mode is not None:
-                f.write("old mode %o\n" % old_mode)
-            f.write("new mode %o\n" % new_mode)
-        else:
-            f.write("deleted mode %o\n" % old_mode)
-    f.write("index %s..%s" % (blob_id(old_blob), blob_id(new_blob)))
-    if new_mode is not None:
-        f.write(" %o" % new_mode)
-    f.write("\n")
+    f.writelines(gen_diff_header(
+        (old_path, new_path), (old_mode, new_mode),
+        (getattr(old_blob, "id", None), getattr(new_blob, "id", None))))
     old_contents = lines(old_blob)
     new_contents = lines(new_blob)
     f.writelines(unified_diff(old_contents, new_contents,

+ 72 - 48
dulwich/porcelain.py

@@ -52,7 +52,6 @@ import os
 import sys
 import time
 
-from dulwich import index
 from dulwich.client import get_transport_and_path
 from dulwich.errors import (
     SendPackError,
@@ -123,10 +122,10 @@ def symbolic_ref(repo, ref_name, force=False):
     :param force: force settings without checking if it exists in refs/heads
     """
     repo_obj = open_repo(repo)
-    ref_path = 'refs/heads/%s' % ref_name
+    ref_path = b'refs/heads/' + ref_name
     if not force and ref_path not in repo_obj.refs.keys():
         raise ValueError('fatal: ref `%s` is not a ref' % ref_name)
-    repo_obj.refs.set_symbolic_ref('HEAD', ref_path)
+    repo_obj.refs.set_symbolic_ref(b'HEAD', ref_path)
 
 
 def commit(repo=".", message=None, author=None, committer=None):
@@ -174,15 +173,22 @@ def init(path=".", bare=False):
         return Repo.init(path)
 
 
-def clone(source, target=None, bare=False, checkout=None, outstream=sys.stdout):
+def clone(source, target=None, bare=False, checkout=None, errstream=sys.stdout, outstream=None):
     """Clone a local or remote git repository.
 
     :param source: Path or URL for source repository
     :param target: Path to target repository (optional)
     :param bare: Whether or not to create a bare repository
-    :param outstream: Optional stream to write progress to
+    :param errstream: Optional stream to write progress to
+    :param outstream: Optional stream to write progress to (deprecated)
     :return: The new repository
     """
+    if outstream is not None:
+        import warnings
+        warnings.warn("outstream= has been deprecated in favour of errstream=.", DeprecationWarning,
+                stacklevel=3)
+        errstream = outstream
+
     if checkout is None:
         checkout = (not bare)
     if checkout and bare:
@@ -200,10 +206,10 @@ def clone(source, target=None, bare=False, checkout=None, outstream=sys.stdout):
         r = Repo.init(target)
     remote_refs = client.fetch(host_path, r,
         determine_wants=r.object_store.determine_wants_all,
-        progress=outstream.write)
-    r["HEAD"] = remote_refs["HEAD"]
+        progress=errstream.write)
+    r[b"HEAD"] = remote_refs[b"HEAD"]
     if checkout:
-        outstream.write('Checking out HEAD')
+        errstream.write(b'Checking out HEAD')
         r.reset_index()
 
     return r
@@ -238,25 +244,31 @@ def rm(repo=".", paths=None):
     r = open_repo(repo)
     index = r.open_index()
     for p in paths:
-        del index[p]
+        del index[p.encode(sys.getfilesystemencoding())]
     index.write()
 
 
+def commit_decode(commit, contents):
+    if commit.encoding is not None:
+        return contents.decode(commit.encoding, "replace")
+    return contents.decode("utf-8", "replace")
+
+
 def print_commit(commit, outstream=sys.stdout):
     """Write a human-readable commit log entry.
 
     :param commit: A `Commit` object
     :param outstream: A stream file to write to
     """
-    outstream.write("-" * 50 + "\n")
-    outstream.write("commit: %s\n" % commit.id)
+    outstream.write(b"-" * 50 + b"\n")
+    outstream.write(b"commit: " + commit.id + b"\n")
     if len(commit.parents) > 1:
-        outstream.write("merge: %s\n" % "...".join(commit.parents[1:]))
-    outstream.write("author: %s\n" % commit.author)
-    outstream.write("committer: %s\n" % commit.committer)
-    outstream.write("\n")
-    outstream.write(commit.message + "\n")
-    outstream.write("\n")
+        outstream.write(b"merge: " + b"...".join(commit.parents[1:]) + b"\n")
+    outstream.write(b"author: " + commit.author + b"\n")
+    outstream.write(b"committer: " + commit.committer + b"\n")
+    outstream.write(b"\n")
+    outstream.write(commit.message + b"\n")
+    outstream.write(b"\n")
 
 
 def print_tag(tag, outstream=sys.stdout):
@@ -265,11 +277,11 @@ def print_tag(tag, outstream=sys.stdout):
     :param tag: A `Tag` object
     :param outstream: A stream to write to
     """
-    outstream.write("Tagger: %s\n" % tag.tagger)
-    outstream.write("Date:   %s\n" % tag.tag_time)
-    outstream.write("\n")
-    outstream.write("%s\n" % tag.message)
-    outstream.write("\n")
+    outstream.write(b"Tagger: " + tag.tagger + b"\n")
+    outstream.write(b"Date:   " + tag.tag_time + b"\n")
+    outstream.write(b"\n")
+    outstream.write(tag.message + b"\n")
+    outstream.write(b"\n")
 
 
 def show_blob(repo, blob, outstream=sys.stdout):
@@ -318,10 +330,10 @@ def show_tag(repo, tag, outstream=sys.stdout):
 
 def show_object(repo, obj, outstream):
     return {
-        "tree": show_tree,
-        "blob": show_blob,
-        "commit": show_commit,
-        "tag": show_tag,
+        b"tree": show_tree,
+        b"blob": show_blob,
+        b"commit": show_commit,
+        b"tag": show_tag,
             }[obj.type_name](repo, obj, outstream)
 
 
@@ -375,12 +387,12 @@ def rev_list(repo, commits, outstream=sys.stdout):
     """
     r = open_repo(repo)
     for entry in r.get_walker(include=[r[c].id for c in commits]):
-        outstream.write("%s\n" % entry.commit.id)
+        outstream.write(entry.commit.id + b"\n")
 
 
 def tag(*args, **kwargs):
     import warnings
-    warnings.warn(DeprecationWarning, "tag has been deprecated in favour of tag_create.")
+    warnings.warn("tag has been deprecated in favour of tag_create.", DeprecationWarning)
     return tag_create(*args, **kwargs)
 
 
@@ -425,12 +437,12 @@ def tag_create(repo, tag, author=None, message=None, annotated=False,
     else:
         tag_id = object.id
 
-    r.refs['refs/tags/' + tag] = tag_id
+    r.refs[b'refs/tags/' + tag] = tag_id
 
 
 def list_tags(*args, **kwargs):
     import warnings
-    warnings.warn(DeprecationWarning, "list_tags has been deprecated in favour of tag_list.")
+    warnings.warn("list_tags has been deprecated in favour of tag_list.", DeprecationWarning)
     return tag_list(*args, **kwargs)
 
 
@@ -441,7 +453,7 @@ def tag_list(repo, outstream=sys.stdout):
     :param outstream: Stream to write tags to
     """
     r = open_repo(repo)
-    tags = list(r.refs.as_dict("refs/tags"))
+    tags = list(r.refs.as_dict(b"refs/tags"))
     tags.sort()
     return tags
 
@@ -453,14 +465,14 @@ def tag_delete(repo, name):
     :param name: Name of tag to remove
     """
     r = open_repo(repo)
-    if isinstance(name, str):
+    if isinstance(name, bytes):
         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]
+        del r.refs[b"refs/tags/" + name]
 
 
 def reset(repo, mode, committish="HEAD"):
@@ -498,17 +510,21 @@ def push(repo, remote_location, refs_path,
 
     def update_refs(refs):
         new_refs = r.get_refs()
-        refs[refs_path] = new_refs['HEAD']
-        del new_refs['HEAD']
+        refs[refs_path] = new_refs[b'HEAD']
+        del new_refs[b'HEAD']
         return refs
 
+    err_encoding = getattr(errstream, 'encoding', 'utf-8')
+    if not isinstance(remote_location, bytes):
+        remote_location_bytes = remote_location.encode(err_encoding)
+    else:
+        remote_location_bytes = remote_location
     try:
         client.send_pack(path, update_refs,
             r.object_store.generate_pack_contents, progress=errstream.write)
-        outstream.write("Push to %s successful.\n" % remote_location)
+        errstream.write(b"Push to " + remote_location_bytes + b" successful.\n")
     except (UpdateRefsError, SendPackError) as e:
-        outstream.write("Push to %s failed.\n" % remote_location)
-        errstream.write("Push to %s failed -> '%s'\n" % e.message)
+        errstream.write(b"Push to " + remote_location_bytes + b" failed -> " + e.message.encode(err_encoding) + b"\n")
 
 
 def pull(repo, remote_location, refs_path,
@@ -527,10 +543,10 @@ def pull(repo, remote_location, refs_path,
 
     client, path = get_transport_and_path(remote_location)
     remote_refs = client.fetch(path, r, progress=errstream.write)
-    r['HEAD'] = remote_refs[refs_path]
+    r[b'HEAD'] = remote_refs[refs_path]
 
     # Perform 'git checkout .' - syncs staged changes
-    tree = r["HEAD"].tree
+    tree = r[b"HEAD"].tree
     r.reset_index()
 
 
@@ -571,7 +587,7 @@ def get_tree_changes(repo):
         'delete': [],
         'modify': [],
     }
-    for change in index.changes_from_tree(r.object_store, r['HEAD'].tree):
+    for change in index.changes_from_tree(r.object_store, r[b'HEAD'].tree):
         if not change[0][0]:
             tracked_changes['add'].append(change[0][1])
         elif not change[0][1]:
@@ -617,13 +633,17 @@ def web_daemon(path=".", address=None, port=None):
     server.serve_forever()
 
 
-def upload_pack(path=".", inf=sys.stdin, outf=sys.stdout):
+def upload_pack(path=".", inf=None, outf=None):
     """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
     """
+    if outf is None:
+        outf = getattr(sys.stdout, 'buffer', sys.stdout)
+    if inf is None:
+        inf = getattr(sys.stdin, 'buffer', sys.stdin)
     backend = FileSystemBackend()
     def send_fn(data):
         outf.write(data)
@@ -635,13 +655,17 @@ def upload_pack(path=".", inf=sys.stdin, outf=sys.stdout):
     return 0
 
 
-def receive_pack(path=".", inf=sys.stdin, outf=sys.stdout):
+def receive_pack(path=".", inf=None, outf=None):
     """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
     """
+    if outf is None:
+        outf = getattr(sys.stdout, 'buffer', sys.stdout)
+    if inf is None:
+        inf = getattr(sys.stdin, 'buffer', sys.stdin)
     backend = FileSystemBackend()
     def send_fn(data):
         outf.write(data)
@@ -660,14 +684,14 @@ def branch_delete(repo, name):
     :param name: Name of the branch
     """
     r = open_repo(repo)
-    if isinstance(name, str):
+    if isinstance(name, bytes):
         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]
+        del r.refs[b"refs/heads/" + name]
 
 
 def branch_create(repo, name, objectish=None, force=False):
@@ -679,7 +703,7 @@ def branch_create(repo, name, objectish=None, force=False):
     :param force: Force creation of branch, even if it already exists
     """
     r = open_repo(repo)
-    if isinstance(name, str):
+    if isinstance(name, bytes):
         names = [name]
     elif isinstance(name, list):
         names = name
@@ -688,7 +712,7 @@ def branch_create(repo, name, objectish=None, force=False):
     if objectish is None:
         objectish = "HEAD"
     object = parse_object(r, objectish)
-    refname = "refs/heads/" + name
+    refname = b"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
@@ -700,7 +724,7 @@ def branch_list(repo):
     :param repo: Path to the repository
     """
     r = open_repo(repo)
-    return r.refs.keys(base="refs/heads/")
+    return r.refs.keys(base=b"refs/heads/")
 
 
 def fetch(repo, remote_location, outstream=sys.stdout, errstream=sys.stderr):

+ 45 - 19
dulwich/protocol.py

@@ -32,12 +32,37 @@ from dulwich.errors import (
 
 TCP_GIT_PORT = 9418
 
-ZERO_SHA = "0" * 40
+ZERO_SHA = b"0" * 40
 
 SINGLE_ACK = 0
 MULTI_ACK = 1
 MULTI_ACK_DETAILED = 2
 
+# pack data
+SIDE_BAND_CHANNEL_DATA = 1
+# progress messages
+SIDE_BAND_CHANNEL_PROGRESS = 2
+# fatal error message just before stream aborts
+SIDE_BAND_CHANNEL_FATAL = 3
+
+CAPABILITY_DELETE_REFS = b'delete-refs'
+CAPABILITY_INCLUDE_TAG = b'include-tag'
+CAPABILITY_MULTI_ACK = b'multi_ack'
+CAPABILITY_MULTI_ACK_DETAILED = b'multi_ack_detailed'
+CAPABILITY_NO_PROGRESS = b'no-progress'
+CAPABILITY_OFS_DELTA = b'ofs-delta'
+CAPABILITY_REPORT_STATUS = b'report-status'
+CAPABILITY_SHALLOW = b'shallow'
+CAPABILITY_SIDE_BAND_64K = b'side-band-64k'
+CAPABILITY_THIN_PACK = b'thin-pack'
+
+COMMAND_DEEPEN = b'deepen'
+COMMAND_SHALLOW = b'shallow'
+COMMAND_UNSHALLOW = b'unshallow'
+COMMAND_DONE = b'done'
+COMMAND_WANT = b'want'
+COMMAND_HAVE = b'have'
+
 
 class ProtocolFile(object):
     """A dummy file for network ops that expect file-like objects."""
@@ -61,8 +86,8 @@ def pkt_line(data):
         None, returns the flush-pkt ('0000').
     """
     if data is None:
-        return '0000'
-    return '%04x%s' % (len(data) + 4, data)
+        return b'0000'
+    return ('%04x' % (len(data) + 4)).encode('ascii') + data
 
 
 class Protocol(object):
@@ -120,12 +145,13 @@ class Protocol(object):
             if self.report_activity:
                 self.report_activity(size, 'read')
             pkt_contents = read(size-4)
-            if len(pkt_contents) + 4 != size:
-                raise AssertionError('Length of pkt read %04x does not match length prefix %04x.'
-                                     .format(len(pkt_contents) + 4, size))
-            return pkt_contents
         except socket.error as e:
             raise GitProtocolError(e)
+        else:
+            if len(pkt_contents) + 4 != size:
+                raise GitProtocolError(
+                    'Length of pkt read %04x does not match length prefix %04x' % (len(pkt_contents) + 4, size))
+            return pkt_contents
 
     def eof(self):
         """Test whether the protocol stream has reached EOF.
@@ -209,7 +235,7 @@ class Protocol(object):
         # 65520-5 = 65515
         # WTF: Why have the len in ASCII, but the channel in binary.
         while blob:
-            self.write_pkt_line("%s%s" % (chr(channel), blob[:65515]))
+            self.write_pkt_line(bytes(bytearray([channel])) + blob[:65515])
             blob = blob[65515:]
 
     def send_cmd(self, cmd, *args):
@@ -220,7 +246,7 @@ class Protocol(object):
         :param cmd: The remote service to access.
         :param args: List of arguments to send to remove service.
         """
-        self.write_pkt_line("%s %s" % (cmd, "".join(["%s\0" % a for a in args])))
+        self.write_pkt_line(cmd + b" " + b"".join([(a + b"\0") for a in args]))
 
     def read_cmd(self):
         """Read a command and some arguments from the git client
@@ -230,10 +256,10 @@ class Protocol(object):
         :return: A tuple of (command, [list of arguments]).
         """
         line = self.read_pkt_line()
-        splice_at = line.find(" ")
+        splice_at = line.find(b" ")
         cmd, args = line[:splice_at], line[splice_at+1:]
-        assert args[-1] == "\x00"
-        return cmd, args[:-1].split(chr(0))
+        assert args[-1:] == b"\x00"
+        return cmd, args[:-1].split(b"\0")
 
 
 _RBUFSIZE = 8192  # Default read buffer size.
@@ -348,10 +374,10 @@ def extract_capabilities(text):
     :param text: String to extract from
     :return: Tuple with text with capabilities removed and list of capabilities
     """
-    if not "\0" in text:
+    if not b"\0" in text:
         return text, []
-    text, capabilities = text.rstrip().split("\0")
-    return (text, capabilities.strip().split(" "))
+    text, capabilities = text.rstrip().split(b"\0")
+    return (text, capabilities.strip().split(b" "))
 
 
 def extract_want_line_capabilities(text):
@@ -365,17 +391,17 @@ def extract_want_line_capabilities(text):
     :param text: Want line to extract from
     :return: Tuple with text with capabilities removed and list of capabilities
     """
-    split_text = text.rstrip().split(" ")
+    split_text = text.rstrip().split(b" ")
     if len(split_text) < 3:
         return text, []
-    return (" ".join(split_text[:2]), split_text[2:])
+    return (b" ".join(split_text[:2]), split_text[2:])
 
 
 def ack_type(capabilities):
     """Extract the ack type from a capabilities list."""
-    if 'multi_ack_detailed' in capabilities:
+    if b'multi_ack_detailed' in capabilities:
         return MULTI_ACK_DETAILED
-    elif 'multi_ack' in capabilities:
+    elif b'multi_ack' in capabilities:
         return MULTI_ACK
     return SINGLE_ACK
 

+ 16 - 18
dulwich/refs.py

@@ -23,6 +23,7 @@
 """
 import errno
 import os
+import sys
 
 from dulwich.errors import (
     PackedRefsException,
@@ -31,6 +32,7 @@ from dulwich.errors import (
 from dulwich.objects import (
     hex_to_sha,
     git_line,
+    valid_hexsha,
     )
 from dulwich.file import (
     GitFile,
@@ -42,6 +44,8 @@ SYMREF = b'ref: '
 LOCAL_BRANCH_PREFIX = b'refs/heads/'
 BAD_REF_CHARS = set(b'\177 ~^:?*[')
 
+path_sep_bytes = os.path.sep.encode(sys.getfilesystemencoding())
+
 
 def check_ref_format(refname):
     """Check if a refname is correctly formatted.
@@ -394,10 +398,9 @@ class DiskRefsContainer(RefsContainer):
         subkeys = set()
         path = self.refpath(base)
         for root, dirs, files in os.walk(path):
-            dir = root[len(path):].strip(os.path.sep).replace(os.path.sep, "/")
+            dir = root[len(path):].strip(path_sep_bytes).replace(path_sep_bytes, b'/')
             for filename in files:
-                refname = (("%s/%s" % (dir, filename))
-                           .strip("/").encode('ascii'))
+                refname = (dir + b'/' + filename).strip(b'/')
                 # check_ref_format requires at least one /, so we prepend the
                 # base before calling it.
                 if check_ref_format(base + b'/' + refname):
@@ -413,9 +416,9 @@ class DiskRefsContainer(RefsContainer):
             allkeys.add(b'HEAD')
         path = self.refpath(b'')
         for root, dirs, files in os.walk(self.refpath(b'refs')):
-            dir = root[len(path):].strip(os.path.sep).replace(os.path.sep, "/")
+            dir = root[len(path):].strip(path_sep_bytes).replace(path_sep_bytes, b'/')
             for filename in files:
-                refname = ("%s/%s" % (dir, filename)).strip("/").encode('ascii')
+                refname = (dir + b'/' + filename).strip(b'/')
                 if check_ref_format(refname):
                     allkeys.add(refname)
         allkeys.update(self.get_packed_refs())
@@ -425,9 +428,8 @@ class DiskRefsContainer(RefsContainer):
         """Return the disk path of a ref.
 
         """
-        name = name.decode('ascii')
-        if os.path.sep != "/":
-            name = name.replace("/", os.path.sep)
+        if path_sep_bytes != b'/':
+            name = name.replace(b'/', path_sep_bytes)
         return os.path.join(self.path, name)
 
     def get_packed_refs(self):
@@ -444,7 +446,7 @@ class DiskRefsContainer(RefsContainer):
             # None if and only if _packed_refs is also None.
             self._packed_refs = {}
             self._peeled_refs = {}
-            path = os.path.join(self.path, 'packed-refs')
+            path = os.path.join(self.path, b'packed-refs')
             try:
                 f = GitFile(path, 'rb')
             except IOError as e:
@@ -512,7 +514,7 @@ class DiskRefsContainer(RefsContainer):
     def _remove_packed_ref(self, name):
         if self._packed_refs is None:
             return
-        filename = os.path.join(self.path, 'packed-refs')
+        filename = os.path.join(self.path, b'packed-refs')
         # reread cached refs from disk, while holding the lock
         f = GitFile(filename, 'wb')
         try:
@@ -659,10 +661,8 @@ def _split_ref_line(line):
     if len(fields) != 2:
         raise PackedRefsException("invalid ref line %r" % line)
     sha, name = fields
-    try:
-        hex_to_sha(sha)
-    except (AssertionError, TypeError) as e:
-        raise PackedRefsException(e)
+    if not valid_hexsha(sha):
+        raise PackedRefsException("Invalid hex sha %r" % sha)
     if not check_ref_format(name):
         raise PackedRefsException("invalid ref name %r" % name)
     return (sha, name)
@@ -700,10 +700,8 @@ def read_packed_refs_with_peeled(f):
         if l.startswith(b'^'):
             if not last:
                 raise PackedRefsException("unexpected peeled ref line")
-            try:
-                hex_to_sha(l[1:])
-            except (AssertionError, TypeError) as e:
-                raise PackedRefsException(e)
+            if not valid_hexsha(l[1:]):
+                raise PackedRefsException("Invalid hex sha %r" % l[1:])
             sha, name = _split_ref_line(last)
             last = None
             yield (sha, name, l[1:])

+ 87 - 71
dulwich/repo.py

@@ -30,6 +30,7 @@ local disk (Repo).
 from io import BytesIO
 import errno
 import os
+import sys
 
 from dulwich.errors import (
     NoIndexPresent,
@@ -81,19 +82,19 @@ from dulwich.refs import (
 import warnings
 
 
-OBJECTDIR = 'objects'
-REFSDIR = 'refs'
-REFSDIR_TAGS = 'tags'
-REFSDIR_HEADS = 'heads'
-INDEX_FILENAME = "index"
+OBJECTDIR = b'objects'
+REFSDIR = b'refs'
+REFSDIR_TAGS = b'tags'
+REFSDIR_HEADS = b'heads'
+INDEX_FILENAME = b'index'
 
 BASE_DIRECTORIES = [
-    ["branches"],
+    [b'branches'],
     [REFSDIR],
     [REFSDIR, REFSDIR_TAGS],
     [REFSDIR, REFSDIR_HEADS],
-    ["hooks"],
-    ["info"]
+    [b'hooks'],
+    [b'info']
     ]
 
 
@@ -140,12 +141,12 @@ def serialize_graftpoints(graftpoints):
 
     """
     graft_lines = []
-    for commit, parents in graftpoints.iteritems():
+    for commit, parents in graftpoints.items():
         if parents:
-            graft_lines.append('%s %s' % (commit, ' '.join(parents)))
+            graft_lines.append(commit + b' ' + b' '.join(parents))
         else:
             graft_lines.append(commit)
-    return '\n'.join(graft_lines)
+    return b'\n'.join(graft_lines)
 
 
 class BaseRepo(object):
@@ -175,16 +176,16 @@ class BaseRepo(object):
     def _init_files(self, bare):
         """Initialize a default set of named files."""
         from dulwich.config import ConfigFile
-        self._put_named_file('description', "Unnamed repository")
+        self._put_named_file(b'description', b"Unnamed repository")
         f = BytesIO()
         cf = ConfigFile()
-        cf.set("core", "repositoryformatversion", "0")
-        cf.set("core", "filemode", "true")
-        cf.set("core", "bare", str(bare).lower())
-        cf.set("core", "logallrefupdates", "true")
+        cf.set(b"core", b"repositoryformatversion", b"0")
+        cf.set(b"core", b"filemode", b"true")
+        cf.set(b"core", b"bare", bare)
+        cf.set(b"core", b"logallrefupdates", True)
         cf.write_to_file(f)
-        self._put_named_file('config', f.getvalue())
-        self._put_named_file(os.path.join('info', 'exclude'), '')
+        self._put_named_file(b'config', f.getvalue())
+        self._put_named_file(os.path.join(b'info', b'exclude'), b'')
 
     def get_named_file(self, path):
         """Get a file from the control dir with a specific name.
@@ -291,7 +292,7 @@ class BaseRepo(object):
         :return: A graph walker object
         """
         if heads is None:
-            heads = self.refs.as_dict('refs/heads').values()
+            heads = self.refs.as_dict(b'refs/heads').values()
         return ObjectStoreGraphWalker(heads, self.get_parents)
 
     def get_refs(self):
@@ -303,7 +304,7 @@ class BaseRepo(object):
 
     def head(self):
         """Return the SHA1 pointed at by HEAD."""
-        return self.refs['HEAD']
+        return self.refs[b'HEAD']
 
     def _get_object(self, sha, cls):
         assert len(sha) in (20, 40)
@@ -439,7 +440,7 @@ class BaseRepo(object):
         :return: A `ShaFile` object, such as a Commit or Blob
         :raise KeyError: when the specified ref or object does not exist
         """
-        if not isinstance(name, str):
+        if not isinstance(name, bytes):
             raise TypeError("'name' must be bytestring, not %.80s" %
                     type(name).__name__)
         if len(name) in (20, 40):
@@ -468,10 +469,10 @@ class BaseRepo(object):
         :param name: ref name
         :param value: Ref value - either a ShaFile object, or a hex sha
         """
-        if name.startswith("refs/") or name == "HEAD":
+        if name.startswith(b"refs/") or name == b'HEAD':
             if isinstance(value, ShaFile):
                 self.refs[name] = value.id
-            elif isinstance(value, str):
+            elif isinstance(value, bytes):
                 self.refs[name] = value
             else:
                 raise TypeError(value)
@@ -483,7 +484,7 @@ class BaseRepo(object):
 
         :param name: Name of the ref to remove
         """
-        if name.startswith("refs/") or name == "HEAD":
+        if name.startswith(b"refs/") or name == b"HEAD":
             del self.refs[name]
         else:
             raise ValueError(name)
@@ -492,9 +493,8 @@ class BaseRepo(object):
         """Determine the identity to use for new commits.
         """
         config = self.get_config_stack()
-        return "%s <%s>" % (
-            config.get(("user", ), "name"),
-            config.get(("user", ), "email"))
+        return (config.get((b"user", ), b"name") + b" <" +
+                config.get((b"user", ), b"email") + b">")
 
     def _add_graftpoints(self, updated_graftpoints):
         """Add or modify graftpoints
@@ -503,7 +503,7 @@ class BaseRepo(object):
         """
 
         # Simple validation
-        for commit, parents in updated_graftpoints.iteritems():
+        for commit, parents in updated_graftpoints.items():
             for sha in [commit] + parents:
                 check_hexsha(sha, 'Invalid graftpoint')
 
@@ -521,7 +521,7 @@ class BaseRepo(object):
                   author=None, commit_timestamp=None,
                   commit_timezone=None, author_timestamp=None,
                   author_timezone=None, tree=None, encoding=None,
-                  ref='HEAD', merge_heads=None):
+                  ref=b'HEAD', merge_heads=None):
         """Create a new commit.
 
         :param message: Commit message
@@ -627,6 +627,7 @@ class BaseRepo(object):
 
         return c.id
 
+path_sep_bytes = os.path.sep.encode(sys.getfilesystemencoding())
 
 class Repo(BaseRepo):
     """A git repository backed by local disk.
@@ -637,36 +638,40 @@ class Repo(BaseRepo):
     To create a new repository, use the Repo.init class method.
     """
 
-    def __init__(self, root):
-        if os.path.isdir(os.path.join(root, ".git", OBJECTDIR)):
+    def __init__(self, path):
+        self.path = path
+        if not isinstance(path, bytes):
+            self._path_bytes = path.encode(sys.getfilesystemencoding())
+        else:
+            self._path_bytes = path
+        if os.path.isdir(os.path.join(self._path_bytes, b'.git', OBJECTDIR)):
             self.bare = False
-            self._controldir = os.path.join(root, ".git")
-        elif (os.path.isdir(os.path.join(root, OBJECTDIR)) and
-              os.path.isdir(os.path.join(root, REFSDIR))):
+            self._controldir = os.path.join(self._path_bytes, b'.git')
+        elif (os.path.isdir(os.path.join(self._path_bytes, OBJECTDIR)) and
+              os.path.isdir(os.path.join(self._path_bytes, REFSDIR))):
             self.bare = True
-            self._controldir = root
-        elif (os.path.isfile(os.path.join(root, ".git"))):
+            self._controldir = self._path_bytes
+        elif (os.path.isfile(os.path.join(self._path_bytes, b'.git'))):
             import re
-            with open(os.path.join(root, ".git"), 'r') as f:
-                _, path = re.match('(gitdir: )(.+$)', f.read()).groups()
+            with open(os.path.join(self._path_bytes, b'.git'), 'rb') as f:
+                _, gitdir = re.match(b'(gitdir: )(.+$)', f.read()).groups()
             self.bare = False
-            self._controldir = os.path.join(root, path)
+            self._controldir = os.path.join(self._path_bytes, gitdir)
         else:
             raise NotGitRepository(
-                "No git repository was found at %(path)s" % dict(path=root)
+                "No git repository was found at %(path)s" % dict(path=path)
             )
-        self.path = root
         object_store = DiskObjectStore(os.path.join(self.controldir(),
                                                     OBJECTDIR))
         refs = DiskRefsContainer(self.controldir())
         BaseRepo.__init__(self, object_store, refs)
 
         self._graftpoints = {}
-        graft_file = self.get_named_file(os.path.join("info", "grafts"))
+        graft_file = self.get_named_file(os.path.join(b'info', b'grafts'))
         if graft_file:
             with graft_file:
                 self._graftpoints.update(parse_graftpoints(graft_file))
-        graft_file = self.get_named_file("shallow")
+        graft_file = self.get_named_file(b'shallow')
         if graft_file:
             with graft_file:
                 self._graftpoints.update(parse_graftpoints(graft_file))
@@ -685,7 +690,7 @@ class Repo(BaseRepo):
         :param path: The path to the file, relative to the control dir.
         :param contents: A string to write to the file.
         """
-        path = path.lstrip(os.path.sep)
+        path = path.lstrip(path_sep_bytes)
         with GitFile(os.path.join(self.controldir(), path), 'wb') as f:
             f.write(contents)
 
@@ -701,7 +706,7 @@ class Repo(BaseRepo):
         """
         # TODO(dborowitz): sanitize filenames, since this is used directly by
         # the dumb web serving code.
-        path = path.lstrip(os.path.sep)
+        path = path.lstrip(path_sep_bytes)
         try:
             return open(os.path.join(self.controldir(), path), 'rb')
         except (IOError, OSError) as e:
@@ -730,12 +735,12 @@ class Repo(BaseRepo):
         # missing index file, which is treated as empty.
         return not self.bare
 
-    def stage(self, paths):
+    def stage(self, paths, fsencoding=sys.getfilesystemencoding()):
         """Stage a set of paths.
 
         :param paths: List of paths, relative to the repository path
         """
-        if isinstance(paths, basestring):
+        if not isinstance(paths, list):
             paths = [paths]
         from dulwich.index import (
             blob_from_path_and_stat,
@@ -743,23 +748,28 @@ class Repo(BaseRepo):
             )
         index = self.open_index()
         for path in paths:
-            full_path = os.path.join(self.path, path)
+            if not isinstance(path, bytes):
+                disk_path_bytes = path.encode(sys.getfilesystemencoding())
+                repo_path_bytes = path.encode(fsencoding)
+            else:
+                disk_path_bytes, repo_path_bytes = path, path
+            full_path = os.path.join(self._path_bytes, disk_path_bytes)
             try:
                 st = os.lstat(full_path)
             except OSError:
                 # File no longer exists
                 try:
-                    del index[path]
+                    del index[repo_path_bytes]
                 except KeyError:
                     pass  # already removed
             else:
                 blob = blob_from_path_and_stat(full_path, st)
                 self.object_store.add_object(blob)
-                index[path] = index_entry_from_stat(st, blob.id, 0)
+                index[repo_path_bytes] = index_entry_from_stat(st, blob.id, 0)
         index.write()
 
     def clone(self, target_path, mkdir=True, bare=False,
-            origin="origin"):
+            origin=b"origin"):
         """Clone this repository.
 
         :param target_path: Target path
@@ -775,21 +785,21 @@ class Repo(BaseRepo):
             target = self.init_bare(target_path)
         self.fetch(target)
         target.refs.import_refs(
-            'refs/remotes/' + origin, self.refs.as_dict('refs/heads'))
+            b'refs/remotes/' + origin, self.refs.as_dict(b'refs/heads'))
         target.refs.import_refs(
-            'refs/tags', self.refs.as_dict('refs/tags'))
+            b'refs/tags', self.refs.as_dict(b'refs/tags'))
         try:
             target.refs.add_if_new(
-                'refs/heads/master',
-                self.refs['refs/heads/master'])
+                b'refs/heads/master',
+                self.refs[b'refs/heads/master'])
         except KeyError:
             pass
 
         # Update target head
-        head, head_sha = self.refs._follow('HEAD')
+        head, head_sha = self.refs._follow(b'HEAD')
         if head is not None and head_sha is not None:
-            target.refs.set_symbolic_ref('HEAD', head)
-            target['HEAD'] = head_sha
+            target.refs.set_symbolic_ref(b'HEAD', head)
+            target[b'HEAD'] = head_sha
 
             if not bare:
                 # Checkout HEAD to target dir
@@ -808,14 +818,14 @@ class Repo(BaseRepo):
             validate_path_element_ntfs,
             )
         if tree is None:
-            tree = self['HEAD'].tree
+            tree = self[b'HEAD'].tree
         config = self.get_config()
         honor_filemode = config.get_boolean('core', 'filemode', os.name != "nt")
         if config.get_boolean('core', 'core.protectNTFS', os.name == "nt"):
             validate_path_element = validate_path_element_ntfs
         else:
             validate_path_element = validate_path_element_default
-        return build_index_from_tree(self.path, self.index_path(),
+        return build_index_from_tree(self._path_bytes, self.index_path(),
                 self.object_store, tree, honor_filemode=honor_filemode,
                 validate_path_element=validate_path_element)
 
@@ -825,7 +835,7 @@ class Repo(BaseRepo):
         :return: `ConfigFile` object for the ``.git/config`` file.
         """
         from dulwich.config import ConfigFile
-        path = os.path.join(self._controldir, 'config')
+        path = os.path.join(self._controldir, b'config')
         try:
             return ConfigFile.from_path(path)
         except (IOError, OSError) as e:
@@ -840,7 +850,7 @@ class Repo(BaseRepo):
 
         :return: A string describing the repository or None.
         """
-        path = os.path.join(self._controldir, 'description')
+        path = os.path.join(self._controldir, b'description')
         try:
             with GitFile(path, 'rb') as f:
                 return f.read()
@@ -858,17 +868,19 @@ class Repo(BaseRepo):
         :param description: Text to set as description for this repository.
         """
 
-        path = os.path.join(self._controldir, 'description')
-        with open(path, 'w') as f:
-            f.write(description)
+        self._put_named_file(b'description', description)
 
     @classmethod
     def _init_maybe_bare(cls, path, bare):
+        if not isinstance(path, bytes):
+            path_bytes = path.encode(sys.getfilesystemencoding())
+        else:
+            path_bytes = path
         for d in BASE_DIRECTORIES:
-            os.mkdir(os.path.join(path, *d))
-        DiskObjectStore.init(os.path.join(path, OBJECTDIR))
+            os.mkdir(os.path.join(path_bytes, *d))
+        DiskObjectStore.init(os.path.join(path_bytes, OBJECTDIR))
         ret = cls(path)
-        ret.refs.set_symbolic_ref("HEAD", "refs/heads/master")
+        ret.refs.set_symbolic_ref(b'HEAD', b"refs/heads/master")
         ret._init_files(bare)
         return ret
 
@@ -880,9 +892,13 @@ class Repo(BaseRepo):
         :param mkdir: Whether to create the directory
         :return: `Repo` instance
         """
+        if not isinstance(path, bytes):
+            path_bytes = path.encode(sys.getfilesystemencoding())
+        else:
+            path_bytes = path
         if mkdir:
-            os.mkdir(path)
-        controldir = os.path.join(path, ".git")
+            os.mkdir(path_bytes)
+        controldir = os.path.join(path_bytes, b'.git')
         os.mkdir(controldir)
         cls._init_maybe_bare(controldir, False)
         return cls(path)
@@ -971,7 +987,7 @@ class MemoryRepo(BaseRepo):
         ret = cls()
         for obj in objects:
             ret.object_store.add_object(obj)
-        for refname, sha in refs.iteritems():
+        for refname, sha in refs.items():
             ret.refs[refname] = sha
         ret._init_files(bare=True)
         return ret

+ 132 - 93
dulwich/server.py

@@ -60,19 +60,38 @@ from dulwich.errors import (
     )
 from dulwich import log_utils
 from dulwich.objects import (
-    hex_to_sha,
     Commit,
+    valid_hexsha,
     )
 from dulwich.pack import (
     write_pack_objects,
     )
 from dulwich.protocol import (
     BufferedPktLineWriter,
+    CAPABILITY_DELETE_REFS,
+    CAPABILITY_INCLUDE_TAG,
+    CAPABILITY_MULTI_ACK_DETAILED,
+    CAPABILITY_MULTI_ACK,
+    CAPABILITY_NO_PROGRESS,
+    CAPABILITY_OFS_DELTA,
+    CAPABILITY_REPORT_STATUS,
+    CAPABILITY_SHALLOW,
+    CAPABILITY_SIDE_BAND_64K,
+    CAPABILITY_THIN_PACK,
+    COMMAND_DEEPEN,
+    COMMAND_DONE,
+    COMMAND_HAVE,
+    COMMAND_SHALLOW,
+    COMMAND_UNSHALLOW,
+    COMMAND_WANT,
     MULTI_ACK,
     MULTI_ACK_DETAILED,
     Protocol,
     ProtocolFile,
     ReceivableProtocol,
+    SIDE_BAND_CHANNEL_DATA,
+    SIDE_BAND_CHANNEL_PROGRESS,
+    SIDE_BAND_CHANNEL_FATAL,
     SINGLE_ACK,
     TCP_GIT_PORT,
     ZERO_SHA,
@@ -187,7 +206,7 @@ class Handler(object):
 
     @classmethod
     def capability_line(cls):
-        return " ".join(cls.capabilities())
+        return b" ".join(cls.capabilities())
 
     @classmethod
     def capabilities(cls):
@@ -195,7 +214,8 @@ class Handler(object):
 
     @classmethod
     def innocuous_capabilities(cls):
-        return ("include-tag", "thin-pack", "no-progress", "ofs-delta")
+        return (CAPABILITY_INCLUDE_TAG, CAPABILITY_THIN_PACK,
+                CAPABILITY_NO_PROGRESS, CAPABILITY_OFS_DELTA)
 
     @classmethod
     def required_capabilities(cls):
@@ -232,20 +252,26 @@ class UploadPackHandler(Handler):
         self.repo = backend.open_repository(args[0])
         self._graph_walker = None
         self.advertise_refs = advertise_refs
+        # A state variable for denoting that the have list is still
+        # being processed, and the client is not accepting any other
+        # data (such as side-band, see the progress method here).
+        self._processing_have_lines = False
 
     @classmethod
     def capabilities(cls):
-        return ("multi_ack_detailed", "multi_ack", "side-band-64k", "thin-pack",
-                "ofs-delta", "no-progress", "include-tag", "shallow")
+        return (CAPABILITY_MULTI_ACK_DETAILED, CAPABILITY_MULTI_ACK,
+                CAPABILITY_SIDE_BAND_64K, CAPABILITY_THIN_PACK,
+                CAPABILITY_OFS_DELTA, CAPABILITY_NO_PROGRESS,
+                CAPABILITY_INCLUDE_TAG, CAPABILITY_SHALLOW)
 
     @classmethod
     def required_capabilities(cls):
-        return ("side-band-64k", "thin-pack", "ofs-delta")
+        return (CAPABILITY_SIDE_BAND_64K, CAPABILITY_THIN_PACK, CAPABILITY_OFS_DELTA)
 
     def progress(self, message):
-        if self.has_capability("no-progress"):
+        if self.has_capability(CAPABILITY_NO_PROGRESS) or self._processing_have_lines:
             return
-        self.proto.write_sideband(2, message)
+        self.proto.write_sideband(SIDE_BAND_CHANNEL_PROGRESS, message)
 
     def get_tagged(self, refs=None, repo=None):
         """Get a dict of peeled values of tags to their original tag shas.
@@ -257,7 +283,7 @@ class UploadPackHandler(Handler):
         :return: dict of peeled_sha -> tag_sha, where tag_sha is the sha of a
             tag whose peeled value is peeled_sha.
         """
-        if not self.has_capability("include-tag"):
+        if not self.has_capability(CAPABILITY_INCLUDE_TAG):
             return {}
         if refs is None:
             refs = self.repo.get_refs()
@@ -270,32 +296,45 @@ class UploadPackHandler(Handler):
                 # TODO: fix behavior when missing
                 return {}
         tagged = {}
-        for name, sha in refs.iteritems():
+        for name, sha in refs.items():
             peeled_sha = repo.get_peeled(name)
             if peeled_sha != sha:
                 tagged[peeled_sha] = sha
         return tagged
 
     def handle(self):
-        write = lambda x: self.proto.write_sideband(1, x)
+        write = lambda x: self.proto.write_sideband(SIDE_BAND_CHANNEL_DATA, x)
 
         graph_walker = ProtocolGraphWalker(self, self.repo.object_store,
             self.repo.get_peeled)
         objects_iter = self.repo.fetch_objects(
-          graph_walker.determine_wants, graph_walker, self.progress,
-          get_tagged=self.get_tagged)
+            graph_walker.determine_wants, graph_walker, self.progress,
+            get_tagged=self.get_tagged)
+
+        # Note the fact that client is only processing responses related
+        # to the have lines it sent, and any other data (including side-
+        # band) will be be considered a fatal error.
+        self._processing_have_lines = True
 
         # Did the process short-circuit (e.g. in a stateless RPC call)? Note
         # that the client still expects a 0-object pack in most cases.
+        # Also, if it also happens that the object_iter is instantiated
+        # with a graph walker with an implementation that talks over the
+        # wire (which is this instance of this class) this will actually
+        # iterate through everything and write things out to the wire.
         if len(objects_iter) == 0:
             return
 
-        self.progress("dul-daemon says what\n")
-        self.progress("counting objects: %d, done.\n" % len(objects_iter))
+        # The provided haves are processed, and it is safe to send side-
+        # band data now.
+        self._processing_have_lines = False
+
+        self.progress(b"dul-daemon says what\n")
+        self.progress(("counting objects: %d, done.\n" % len(objects_iter)).encode('ascii'))
         write_pack_objects(ProtocolFile(None, write), objects_iter)
-        self.progress("how was that, then?\n")
+        self.progress(b"how was that, then?\n")
         # we are done
-        self.proto.write("0000")
+        self.proto.write_pkt_line(None)
 
 
 def _split_proto_line(line, allowed):
@@ -318,22 +357,21 @@ def _split_proto_line(line, allowed):
     if not line:
         fields = [None]
     else:
-        fields = line.rstrip('\n').split(' ', 1)
+        fields = line.rstrip(b'\n').split(b' ', 1)
     command = fields[0]
     if allowed is not None and command not in allowed:
         raise UnexpectedCommandError(command)
-    try:
-        if len(fields) == 1 and command in ('done', None):
-            return (command, None)
-        elif len(fields) == 2:
-            if command in ('want', 'have', 'shallow', 'unshallow'):
-                hex_to_sha(fields[1])
-                return tuple(fields)
-            elif command == 'deepen':
-                return command, int(fields[1])
-    except (TypeError, AssertionError) as e:
-        raise GitProtocolError(e)
-    raise GitProtocolError('Received invalid line from client: %s' % line)
+    if len(fields) == 1 and command in (COMMAND_DONE, None):
+        return (command, None)
+    elif len(fields) == 2:
+        if command in (COMMAND_WANT, COMMAND_HAVE, COMMAND_SHALLOW,
+                       COMMAND_UNSHALLOW):
+            if not valid_hexsha(fields[1]):
+                raise GitProtocolError("Invalid sha")
+            return tuple(fields)
+        elif command == COMMAND_DEEPEN:
+            return command, int(fields[1])
+    raise GitProtocolError('Received invalid line from client: %r' % line)
 
 
 def _find_shallow(store, heads, depth):
@@ -381,7 +419,7 @@ def _want_satisfied(store, haves, want, earliest):
         commit = pending.popleft()
         if commit.id in haves:
             return True
-        if commit.type_name != "commit":
+        if commit.type_name != b"commit":
             # non-commit wants are assumed to be satisfied
             continue
         for parent in commit.parents:
@@ -460,17 +498,16 @@ class ProtocolGraphWalker(object):
         :param heads: a dict of refname->SHA1 to advertise
         :return: a list of SHA1s requested by the client
         """
-        values = set(heads.itervalues())
+        values = set(heads.values())
         if self.advertise_refs or not self.http_req:
-            for i, (ref, sha) in enumerate(sorted(heads.iteritems())):
-                line = "%s %s" % (sha, ref)
+            for i, (ref, sha) in enumerate(sorted(heads.items())):
+                line = sha + b' ' + ref
                 if not i:
-                    line = "%s\x00%s" % (line, self.handler.capability_line())
-                self.proto.write_pkt_line("%s\n" % line)
+                    line += b'\x00' + self.handler.capability_line()
+                self.proto.write_pkt_line(line + b'\n')
                 peeled_sha = self.get_peeled(ref)
                 if peeled_sha != sha:
-                    self.proto.write_pkt_line('%s %s^{}\n' %
-                                              (peeled_sha, ref))
+                    self.proto.write_pkt_line(peeled_sha + b' ' + ref + b'^{}\n')
 
             # i'm done..
             self.proto.write_pkt_line(None)
@@ -485,11 +522,11 @@ class ProtocolGraphWalker(object):
         line, caps = extract_want_line_capabilities(want)
         self.handler.set_client_capabilities(caps)
         self.set_ack_type(ack_type(caps))
-        allowed = ('want', 'shallow', 'deepen', None)
+        allowed = (COMMAND_WANT, COMMAND_SHALLOW, COMMAND_DEEPEN, None)
         command, sha = _split_proto_line(line, allowed)
 
         want_revs = []
-        while command == 'want':
+        while command == COMMAND_WANT:
             if sha not in values:
                 raise GitProtocolError(
                   'Client wants invalid object %s' % sha)
@@ -497,7 +534,7 @@ class ProtocolGraphWalker(object):
             command, sha = self.read_proto_line(allowed)
 
         self.set_wants(want_revs)
-        if command in ('shallow', 'deepen'):
+        if command in (COMMAND_SHALLOW, COMMAND_DEEPEN):
             self.unread_proto_line(command, sha)
             self._handle_shallow_request(want_revs)
 
@@ -510,7 +547,9 @@ class ProtocolGraphWalker(object):
         return want_revs
 
     def unread_proto_line(self, command, value):
-        self.proto.unread_pkt_line('%s %s' % (command, value))
+        if isinstance(value, int):
+            value = str(value).encode('ascii')
+        self.proto.unread_pkt_line(command + b' ' + value)
 
     def ack(self, have_ref):
         if len(have_ref) != 40:
@@ -544,8 +583,8 @@ class ProtocolGraphWalker(object):
 
     def _handle_shallow_request(self, wants):
         while True:
-            command, val = self.read_proto_line(('deepen', 'shallow'))
-            if command == 'deepen':
+            command, val = self.read_proto_line((COMMAND_DEEPEN, COMMAND_SHALLOW))
+            if command == COMMAND_DEEPEN:
                 depth = val
                 break
             self.client_shallow.add(val)
@@ -560,19 +599,19 @@ class ProtocolGraphWalker(object):
         unshallow = self.unshallow = not_shallow & self.client_shallow
 
         for sha in sorted(new_shallow):
-            self.proto.write_pkt_line('shallow %s' % sha)
+            self.proto.write_pkt_line(COMMAND_SHALLOW + b' ' + sha)
         for sha in sorted(unshallow):
-            self.proto.write_pkt_line('unshallow %s' % sha)
+            self.proto.write_pkt_line(COMMAND_UNSHALLOW + b' ' + sha)
 
         self.proto.write_pkt_line(None)
 
-    def send_ack(self, sha, ack_type=''):
+    def send_ack(self, sha, ack_type=b''):
         if ack_type:
-            ack_type = ' %s' % ack_type
-        self.proto.write_pkt_line('ACK %s%s\n' % (sha, ack_type))
+            ack_type = b' ' + ack_type
+        self.proto.write_pkt_line(b'ACK ' + sha + ack_type + b'\n')
 
     def send_nak(self):
-        self.proto.write_pkt_line('NAK\n')
+        self.proto.write_pkt_line(b'NAK\n')
 
     def set_wants(self, wants):
         self._wants = wants
@@ -595,7 +634,7 @@ class ProtocolGraphWalker(object):
         self._impl = impl_classes[ack_type](self)
 
 
-_GRAPH_WALKER_COMMANDS = ('have', 'done', None)
+_GRAPH_WALKER_COMMANDS = (COMMAND_HAVE, COMMAND_DONE, None)
 
 
 class SingleAckGraphWalkerImpl(object):
@@ -612,11 +651,11 @@ class SingleAckGraphWalkerImpl(object):
 
     def next(self):
         command, sha = self.walker.read_proto_line(_GRAPH_WALKER_COMMANDS)
-        if command in (None, 'done'):
+        if command in (None, COMMAND_DONE):
             if not self._sent_ack:
                 self.walker.send_nak()
             return None
-        elif command == 'have':
+        elif command == COMMAND_HAVE:
             return sha
 
     __next__ = next
@@ -633,7 +672,7 @@ class MultiAckGraphWalkerImpl(object):
     def ack(self, have_ref):
         self._common.append(have_ref)
         if not self._found_base:
-            self.walker.send_ack(have_ref, 'continue')
+            self.walker.send_ack(have_ref, b'continue')
             if self.walker.all_wants_satisfied(self._common):
                 self._found_base = True
         # else we blind ack within next
@@ -646,7 +685,7 @@ class MultiAckGraphWalkerImpl(object):
                 # in multi-ack mode, a flush-pkt indicates the client wants to
                 # flush but more have lines are still coming
                 continue
-            elif command == 'done':
+            elif command == COMMAND_DONE:
                 # don't nak unless no common commits were found, even if not
                 # everything is satisfied
                 if self._common:
@@ -654,10 +693,10 @@ class MultiAckGraphWalkerImpl(object):
                 else:
                     self.walker.send_nak()
                 return None
-            elif command == 'have':
+            elif command == COMMAND_HAVE:
                 if self._found_base:
                     # blind ack
-                    self.walker.send_ack(sha, 'continue')
+                    self.walker.send_ack(sha, b'continue')
                 return sha
 
     __next__ = next
@@ -674,10 +713,10 @@ class MultiAckDetailedGraphWalkerImpl(object):
     def ack(self, have_ref):
         self._common.append(have_ref)
         if not self._found_base:
-            self.walker.send_ack(have_ref, 'common')
+            self.walker.send_ack(have_ref, b'common')
             if self.walker.all_wants_satisfied(self._common):
                 self._found_base = True
-                self.walker.send_ack(have_ref, 'ready')
+                self.walker.send_ack(have_ref, b'ready')
         # else we blind ack within next
 
     def next(self):
@@ -688,7 +727,7 @@ class MultiAckDetailedGraphWalkerImpl(object):
                 if self.walker.http_req:
                     return None
                 continue
-            elif command == 'done':
+            elif command == COMMAND_DONE:
                 # don't nak unless no common commits were found, even if not
                 # everything is satisfied
                 if self._common:
@@ -696,11 +735,11 @@ class MultiAckDetailedGraphWalkerImpl(object):
                 else:
                     self.walker.send_nak()
                 return None
-            elif command == 'have':
+            elif command == COMMAND_HAVE:
                 if self._found_base:
                     # blind ack; can happen if the client has more requests
                     # inflight
-                    self.walker.send_ack(sha, 'ready')
+                    self.walker.send_ack(sha, b'ready')
                 return sha
 
     __next__ = next
@@ -717,7 +756,7 @@ class ReceivePackHandler(Handler):
 
     @classmethod
     def capabilities(cls):
-        return ("report-status", "delete-refs", "side-band-64k")
+        return (CAPABILITY_REPORT_STATUS, CAPABILITY_DELETE_REFS, CAPABILITY_SIDE_BAND_64K)
 
     def _apply_pack(self, refs):
         all_exceptions = (IOError, OSError, ChecksumMismatch, ApplyDeltaError,
@@ -735,43 +774,43 @@ class ReceivePackHandler(Handler):
             try:
                 recv = getattr(self.proto, "recv", None)
                 self.repo.object_store.add_thin_pack(self.proto.read, recv)
-                status.append(('unpack', 'ok'))
+                status.append((b'unpack', b'ok'))
             except all_exceptions as e:
-                status.append(('unpack', str(e).replace('\n', '')))
+                status.append((b'unpack', str(e).replace('\n', '')))
                 # The pack may still have been moved in, but it may contain broken
                 # objects. We trust a later GC to clean it up.
         else:
             # The git protocol want to find a status entry related to unpack process
             # even if no pack data has been sent.
-            status.append(('unpack', 'ok'))
+            status.append((b'unpack', b'ok'))
 
         for oldsha, sha, ref in refs:
-            ref_status = 'ok'
+            ref_status = b'ok'
             try:
                 if sha == ZERO_SHA:
-                    if not 'delete-refs' in self.capabilities():
+                    if not CAPABILITY_DELETE_REFS in self.capabilities():
                         raise GitProtocolError(
                           'Attempted to delete refs without delete-refs '
                           'capability.')
                     try:
                         del self.repo.refs[ref]
                     except all_exceptions:
-                        ref_status = 'failed to delete'
+                        ref_status = b'failed to delete'
                 else:
                     try:
                         self.repo.refs[ref] = sha
                     except all_exceptions:
-                        ref_status = 'failed to write'
+                        ref_status = b'failed to write'
             except KeyError as e:
-                ref_status = 'bad ref'
+                ref_status = b'bad ref'
             status.append((ref, ref_status))
 
         return status
 
     def _report_status(self, status):
-        if self.has_capability('side-band-64k'):
+        if self.has_capability(CAPABILITY_SIDE_BAND_64K):
             writer = BufferedPktLineWriter(
-              lambda d: self.proto.write_sideband(1, d))
+              lambda d: self.proto.write_sideband(SIDE_BAND_CHANNEL_DATA, d))
             write = writer.write
 
             def flush():
@@ -782,31 +821,31 @@ class ReceivePackHandler(Handler):
             flush = lambda: None
 
         for name, msg in status:
-            if name == 'unpack':
-                write('unpack %s\n' % msg)
-            elif msg == 'ok':
-                write('ok %s\n' % name)
+            if name == b'unpack':
+                write(b'unpack ' + msg + b'\n')
+            elif msg == b'ok':
+                write(b'ok ' + name + b'\n')
             else:
-                write('ng %s %s\n' % (name, msg))
+                write(b'ng ' + name + b' ' + msg + b'\n')
         write(None)
         flush()
 
     def handle(self):
         if self.advertise_refs or not self.http_req:
-            refs = sorted(self.repo.get_refs().iteritems())
+            refs = sorted(self.repo.get_refs().items())
 
             if refs:
                 self.proto.write_pkt_line(
-                  "%s %s\x00%s\n" % (refs[0][1], refs[0][0],
-                                     self.capability_line()))
+                  refs[0][1] + b' ' + refs[0][0] + b'\0' +
+                  self.capability_line() + b'\n')
                 for i in range(1, len(refs)):
                     ref = refs[i]
-                    self.proto.write_pkt_line("%s %s\n" % (ref[1], ref[0]))
+                    self.proto.write_pkt_line(ref[1] + b' ' + ref[0] + b'\n')
             else:
-                self.proto.write_pkt_line("%s capabilities^{}\0%s" % (
-                  ZERO_SHA, self.capability_line()))
+                self.proto.write_pkt_line(ZERO_SHA + b" capabilities^{}\0" +
+                    self.capability_line())
 
-            self.proto.write("0000")
+            self.proto.write_pkt_line(None)
             if self.advertise_refs:
                 return
 
@@ -830,14 +869,14 @@ class ReceivePackHandler(Handler):
 
         # when we have read all the pack from the client, send a status report
         # if the client asked for it
-        if self.has_capability('report-status'):
+        if self.has_capability(CAPABILITY_REPORT_STATUS):
             self._report_status(status)
 
 
 # Default handler classes for git services.
 DEFAULT_HANDLERS = {
-  'git-upload-pack': UploadPackHandler,
-  'git-receive-pack': ReceivePackHandler,
+  b'git-upload-pack': UploadPackHandler,
+  b'git-receive-pack': ReceivePackHandler,
   }
 
 
@@ -941,7 +980,7 @@ def generate_info_refs(repo):
 def generate_objects_info_packs(repo):
     """Generate an index for for packs."""
     for pack in repo.object_store.packs:
-        yield 'P %s\n' % pack.data.filename
+        yield b'P ' + pack.data.filename.encode(sys.getfilesystemencoding()) + b'\n'
 
 
 def update_server_info(repo):
@@ -950,11 +989,11 @@ def update_server_info(repo):
     This generates info/refs and objects/info/packs,
     similar to "git update-server-info".
     """
-    repo._put_named_file(os.path.join('info', 'refs'),
-        "".join(generate_info_refs(repo)))
+    repo._put_named_file(os.path.join(b'info', b'refs'),
+        b"".join(generate_info_refs(repo)))
 
-    repo._put_named_file(os.path.join('objects', 'info', 'packs'),
-        "".join(generate_objects_info_packs(repo)))
+    repo._put_named_file(os.path.join(b'objects', b'info', b'packs'),
+        b"".join(generate_objects_info_packs(repo)))
 
 
 if __name__ == '__main__':

+ 2 - 1
dulwich/tests/__init__.py

@@ -170,7 +170,8 @@ def nocompat_test_suite():
     result = unittest.TestSuite()
     result.addTests(self_test_suite())
     from dulwich.contrib import test_suite as contrib_test_suite
-    result.addTests(tutorial_test_suite())
+    if sys.version_info[0] == 2:
+        result.addTests(tutorial_test_suite())
     result.addTests(contrib_test_suite())
     return result
 

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

@@ -23,6 +23,7 @@ import errno
 import os
 import shutil
 import socket
+import sys
 import tempfile
 
 from dulwich.repo import Repo
@@ -38,6 +39,7 @@ from dulwich.tests.compat.utils import (
     import_repo,
     run_git_or_fail,
     )
+from dulwich.tests.compat.utils import require_git_version
 
 
 class _StubRepo(object):
@@ -46,6 +48,11 @@ class _StubRepo(object):
     def __init__(self, name):
         temp_dir = tempfile.mkdtemp()
         self.path = os.path.join(temp_dir, name)
+        if not isinstance(self.path, bytes):
+            self._path_bytes = self.path.encode(sys.getfilesystemencoding())
+        else:
+            self._path_bytes = self.path
+
         os.mkdir(self.path)
 
 
@@ -71,6 +78,8 @@ class ServerTests(object):
     Does not inherit from TestCase so tests are not automatically run.
     """
 
+    min_single_branch_version = (1, 7, 10,)
+
     def import_repos(self):
         self._old_repo = import_repo('server_old.export')
         self.addCleanup(tear_down_repo, self._old_repo)
@@ -171,6 +180,7 @@ class ServerTests(object):
         self.assertEqual(len(o.split('\n')), 4)
 
     def test_new_shallow_clone_from_dulwich(self):
+        require_git_version(self.min_single_branch_version)
         self._source_repo = import_repo('server_new.export')
         self.addCleanup(tear_down_repo, self._source_repo)
         self._stub_repo = _StubRepo('shallow')
@@ -187,6 +197,7 @@ class ServerTests(object):
         self.assertReposNotEqual(clone, self._source_repo)
 
     def test_fetch_same_depth_into_shallow_clone_from_dulwich(self):
+        require_git_version(self.min_single_branch_version)
         self._source_repo = import_repo('server_new.export')
         self.addCleanup(tear_down_repo, self._source_repo)
         self._stub_repo = _StubRepo('shallow')
@@ -208,6 +219,7 @@ class ServerTests(object):
         self.assertReposNotEqual(clone, self._source_repo)
 
     def test_fetch_full_depth_into_shallow_clone_from_dulwich(self):
+        require_git_version(self.min_single_branch_version)
         self._source_repo = import_repo('server_new.export')
         self.addCleanup(tear_down_repo, self._source_repo)
         self._stub_repo = _StubRepo('shallow')

+ 44 - 35
dulwich/tests/compat/test_client.py

@@ -30,7 +30,12 @@ import sys
 import tarfile
 import tempfile
 import threading
-import urllib
+
+try:
+    from urlparse import unquote
+except ImportError:
+    from urllib.parse import unquote
+
 
 try:
     import BaseHTTPServer
@@ -68,7 +73,6 @@ from dulwich.tests.compat.utils import (
     )
 
 
-@skipIfPY3
 class DulwichClientTestBase(object):
     """Tests for client/server compatibility."""
 
@@ -97,7 +101,7 @@ class DulwichClientTestBase(object):
         srcpath = os.path.join(self.gitroot, 'server_new.export')
         src = repo.Repo(srcpath)
         sendrefs = dict(src.get_refs())
-        del sendrefs['HEAD']
+        del sendrefs[b'HEAD']
         c.send_pack(self._build_path('/dest'), lambda _: sendrefs,
                     src.object_store.generate_pack_contents)
 
@@ -113,24 +117,24 @@ class DulwichClientTestBase(object):
 
     def test_send_without_report_status(self):
         c = self._client()
-        c._send_capabilities.remove('report-status')
+        c._send_capabilities.remove(b'report-status')
         srcpath = os.path.join(self.gitroot, 'server_new.export')
         src = repo.Repo(srcpath)
         sendrefs = dict(src.get_refs())
-        del sendrefs['HEAD']
+        del sendrefs[b'HEAD']
         c.send_pack(self._build_path('/dest'), lambda _: sendrefs,
                     src.object_store.generate_pack_contents)
         self.assertDestEqualsSrc()
 
     def make_dummy_commit(self, dest):
-        b = objects.Blob.from_string('hi')
+        b = objects.Blob.from_string(b'hi')
         dest.object_store.add_object(b)
-        t = index.commit_tree(dest.object_store, [('hi', b.id, 0o100644)])
+        t = index.commit_tree(dest.object_store, [(b'hi', b.id, 0o100644)])
         c = objects.Commit()
-        c.author = c.committer = 'Foo Bar <foo@example.com>'
+        c.author = c.committer = b'Foo Bar <foo@example.com>'
         c.author_time = c.commit_time = 0
         c.author_timezone = c.commit_timezone = 0
-        c.message = 'hi'
+        c.message = b'hi'
         c.tree = t
         dest.object_store.add_object(c)
         return c.id
@@ -147,26 +151,27 @@ class DulwichClientTestBase(object):
         srcpath = os.path.join(self.gitroot, 'server_new.export')
         src = repo.Repo(srcpath)
         sendrefs = dict(src.get_refs())
-        del sendrefs['HEAD']
+        del sendrefs[b'HEAD']
         return sendrefs, src.object_store.generate_pack_contents
 
     def test_send_pack_one_error(self):
         dest, dummy_commit = self.disable_ff_and_make_dummy_commit()
-        dest.refs['refs/heads/master'] = dummy_commit
+        dest.refs[b'refs/heads/master'] = dummy_commit
         sendrefs, gen_pack = self.compute_send()
         c = self._client()
         try:
             c.send_pack(self._build_path('/dest'), lambda _: sendrefs, gen_pack)
         except errors.UpdateRefsError as e:
-            self.assertEqual('refs/heads/master failed to update', str(e))
-            self.assertEqual({'refs/heads/branch': 'ok',
-                              'refs/heads/master': 'non-fast-forward'},
+            self.assertEqual('refs/heads/master failed to update',
+                             e.args[0])
+            self.assertEqual({b'refs/heads/branch': b'ok',
+                              b'refs/heads/master': b'non-fast-forward'},
                              e.ref_status)
 
     def test_send_pack_multiple_errors(self):
         dest, dummy = self.disable_ff_and_make_dummy_commit()
         # set up for two non-ff errors
-        branch, master = 'refs/heads/branch', 'refs/heads/master'
+        branch, master = b'refs/heads/branch', b'refs/heads/master'
         dest.refs[branch] = dest.refs[master] = dummy
         sendrefs, gen_pack = self.compute_send()
         c = self._client()
@@ -174,16 +179,18 @@ class DulwichClientTestBase(object):
             c.send_pack(self._build_path('/dest'), lambda _: sendrefs, gen_pack)
         except errors.UpdateRefsError as e:
             self.assertIn(str(e),
-                          ['{0}, {1} failed to update'.format(branch, master),
-                           '{1}, {0} failed to update'.format(branch, master)])
-            self.assertEqual({branch: 'non-fast-forward',
-                              master: 'non-fast-forward'},
+                          ['{0}, {1} failed to update'.format(
+                              branch.decode('ascii'), master.decode('ascii')),
+                           '{1}, {0} failed to update'.format(
+                               branch.decode('ascii'), master.decode('ascii'))])
+            self.assertEqual({branch: b'non-fast-forward',
+                              master: b'non-fast-forward'},
                              e.ref_status)
 
     def test_archive(self):
         c = self._client()
         f = BytesIO()
-        c.archive(self._build_path('/server_new.export'), 'HEAD', f.write)
+        c.archive(self._build_path('/server_new.export'), b'HEAD', f.write)
         f.seek(0)
         tf = tarfile.open(fileobj=f)
         self.assertEqual(['baz', 'foo'], tf.getnames())
@@ -199,7 +206,7 @@ class DulwichClientTestBase(object):
     def test_incremental_fetch_pack(self):
         self.test_fetch_pack()
         dest, dummy = self.disable_ff_and_make_dummy_commit()
-        dest.refs['refs/heads/master'] = dummy
+        dest.refs[b'refs/heads/master'] = dummy
         c = self._client()
         dest = repo.Repo(os.path.join(self.gitroot, 'server_new.export'))
         refs = c.fetch(self._build_path('/dest'), dest)
@@ -209,7 +216,7 @@ class DulwichClientTestBase(object):
 
     def test_fetch_pack_no_side_band_64k(self):
         c = self._client()
-        c._fetch_capabilities.remove('side-band-64k')
+        c._fetch_capabilities.remove(b'side-band-64k')
         dest = repo.Repo(os.path.join(self.gitroot, 'dest'))
         refs = c.fetch(self._build_path('/server_new.export'), dest)
         for r in refs.items():
@@ -229,16 +236,16 @@ class DulwichClientTestBase(object):
     def test_send_remove_branch(self):
         dest = repo.Repo(os.path.join(self.gitroot, 'dest'))
         dummy_commit = self.make_dummy_commit(dest)
-        dest.refs['refs/heads/master'] = dummy_commit
-        dest.refs['refs/heads/abranch'] = dummy_commit
+        dest.refs[b'refs/heads/master'] = dummy_commit
+        dest.refs[b'refs/heads/abranch'] = dummy_commit
         sendrefs = dict(dest.refs)
-        sendrefs['refs/heads/abranch'] = "00" * 20
-        del sendrefs['HEAD']
+        sendrefs[b'refs/heads/abranch'] = b"00" * 20
+        del sendrefs[b'HEAD']
         gen_pack = lambda have, want: []
         c = self._client()
-        self.assertEqual(dest.refs["refs/heads/abranch"], dummy_commit)
+        self.assertEqual(dest.refs[b"refs/heads/abranch"], dummy_commit)
         c.send_pack(self._build_path('/dest'), lambda _: sendrefs, gen_pack)
-        self.assertFalse("refs/heads/abranch" in dest.refs)
+        self.assertFalse(b"refs/heads/abranch" in dest.refs)
 
 
 class DulwichTCPClientTest(CompatTestCase, DulwichClientTestBase):
@@ -284,10 +291,10 @@ class DulwichTCPClientTest(CompatTestCase, DulwichClientTestBase):
         CompatTestCase.tearDown(self)
 
     def _client(self):
-        return client.TCPGitClient('localhost')
+        return client.TCPGitClient(b'localhost')
 
     def _build_path(self, path):
-        return path
+        return path.encode(sys.getfilesystemencoding())
 
 
 class TestSSHVendor(object):
@@ -300,6 +307,7 @@ class TestSSHVendor(object):
         return client.SubprocessWrapper(p)
 
 
+@skipIfPY3
 class DulwichMockSSHClientTest(CompatTestCase, DulwichClientTestBase):
 
     def setUp(self):
@@ -379,7 +387,7 @@ class GitHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
         env['GIT_PROJECT_ROOT'] = self.server.root_path
         env["GIT_HTTP_EXPORT_ALL"] = "1"
         env['REQUEST_METHOD'] = self.command
-        uqrest = urllib.unquote(rest)
+        uqrest = unquote(rest)
         env['PATH_INFO'] = uqrest
         env['SCRIPT_NAME'] = "/"
         if query:
@@ -388,7 +396,7 @@ class GitHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
         if host != self.client_address[0]:
             env['REMOTE_HOST'] = host
         env['REMOTE_ADDR'] = self.client_address[0]
-        authorization = self.headers.getheader("authorization")
+        authorization = self.headers.get("authorization")
         if authorization:
             authorization = authorization.split()
             if len(authorization) == 2:
@@ -408,10 +416,10 @@ class GitHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
             env['CONTENT_TYPE'] = self.headers.type
         else:
             env['CONTENT_TYPE'] = self.headers.typeheader
-        length = self.headers.getheader('content-length')
+        length = self.headers.get('content-length')
         if length:
             env['CONTENT_LENGTH'] = length
-        referer = self.headers.getheader('referer')
+        referer = self.headers.get('referer')
         if referer:
             env['HTTP_REFERER'] = referer
         accept = []
@@ -421,7 +429,7 @@ class GitHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
             else:
                 accept = accept + line[7:].split(',')
         env['HTTP_ACCEPT'] = ','.join(accept)
-        ua = self.headers.getheader('user-agent')
+        ua = self.headers.get('user-agent')
         if ua:
             env['HTTP_USER_AGENT'] = ua
         co = filter(None, self.headers.getheaders('cookie'))
@@ -470,6 +478,7 @@ class HTTPGitServer(BaseHTTPServer.HTTPServer):
         return 'http://%s:%s/' % (self.server_name, self.server_port)
 
 
+@skipIfPY3
 class DulwichHttpClientTest(CompatTestCase, DulwichClientTestBase):
 
     min_git_version = (1, 7, 0, 2)

+ 11 - 8
dulwich/tests/compat/test_pack.py

@@ -24,6 +24,7 @@ import binascii
 import os
 import re
 import shutil
+import sys
 import tempfile
 
 from dulwich.pack import (
@@ -45,7 +46,7 @@ from dulwich.tests.compat.utils import (
     run_git_or_fail,
     )
 
-_NON_DELTA_RE = re.compile('non delta: (?P<non_delta>\d+) objects')
+_NON_DELTA_RE = re.compile(b'non delta: (?P<non_delta>\d+) objects')
 
 def _git_verify_pack_object_list(output):
     pack_shas = set()
@@ -66,12 +67,14 @@ class TestPack(PackTests):
         require_git_version((1, 5, 0))
         super(TestPack, self).setUp()
         self._tempdir = tempfile.mkdtemp()
+        if not isinstance(self._tempdir, bytes):
+            self._tempdir = self._tempdir.encode(sys.getfilesystemencoding())
         self.addCleanup(shutil.rmtree, self._tempdir)
 
     def test_copy(self):
         with self.get_pack(pack1_sha) as origpack:
             self.assertSucceeds(origpack.index.check)
-            pack_path = os.path.join(self._tempdir, "Elch")
+            pack_path = os.path.join(self._tempdir, b'Elch')
             write_pack(pack_path, origpack.pack_tuples())
             output = run_git_or_fail(['verify-pack', '-v', pack_path])
             orig_shas = set(o.id for o in origpack.iterobjects())
@@ -81,9 +84,9 @@ class TestPack(PackTests):
         orig_pack = self.get_pack(pack1_sha)
         orig_blob = orig_pack[a_sha]
         new_blob = Blob()
-        new_blob.data = orig_blob.data + 'x'
+        new_blob.data = orig_blob.data + b'x'
         all_to_pack = list(orig_pack.pack_tuples()) + [(new_blob, None)]
-        pack_path = os.path.join(self._tempdir, "pack_with_deltas")
+        pack_path = os.path.join(self._tempdir, b'pack_with_deltas')
         write_pack(pack_path, all_to_pack, deltify=True)
         output = run_git_or_fail(['verify-pack', '-v', pack_path])
         self.assertEqual(set(x[0].id for x in all_to_pack),
@@ -102,12 +105,12 @@ class TestPack(PackTests):
         orig_pack = self.get_pack(pack1_sha)
         orig_blob = orig_pack[a_sha]
         new_blob = Blob()
-        new_blob.data = orig_blob.data + ('x' * 2 ** 20)
+        new_blob.data = orig_blob.data + (b'x' * 2 ** 20)
         new_blob_2 = Blob()
-        new_blob_2.data = new_blob.data + 'y'
+        new_blob_2.data = new_blob.data + b'y'
         all_to_pack = list(orig_pack.pack_tuples()) + [(new_blob, None),
                                                        (new_blob_2, None)]
-        pack_path = os.path.join(self._tempdir, "pack_with_deltas")
+        pack_path = os.path.join(self._tempdir, b'pack_with_deltas')
         write_pack(pack_path, all_to_pack, deltify=True)
         output = run_git_or_fail(['verify-pack', '-v', pack_path])
         self.assertEqual(set(x[0].id for x in all_to_pack),
@@ -121,7 +124,7 @@ class TestPack(PackTests):
             'Expected 3 non-delta objects, got %d' % got_non_delta)
         # We expect one object to have a delta chain length of two
         # (new_blob_2), so let's verify that actually happens:
-        self.assertIn('chain length = 2', output)
+        self.assertIn(b'chain length = 2', output)
 
     # This test is SUPER slow: over 80 seconds on a 2012-era
     # laptop. This is because SequenceMatcher is worst-case quadratic

+ 6 - 8
dulwich/tests/compat/test_repository.py

@@ -32,7 +32,6 @@ from dulwich.repo import (
     )
 from dulwich.tests.utils import (
     tear_down_repo,
-    skipIfPY3,
     )
 
 from dulwich.tests.compat.utils import (
@@ -42,7 +41,6 @@ from dulwich.tests.compat.utils import (
     )
 
 
-@skipIfPY3
 class ObjectStoreTestCase(CompatTestCase):
     """Tests for git repository compatibility."""
 
@@ -57,7 +55,7 @@ class ObjectStoreTestCase(CompatTestCase):
     def _parse_refs(self, output):
         refs = {}
         for line in BytesIO(output):
-            fields = line.rstrip('\n').split(' ')
+            fields = line.rstrip(b'\n').split(b' ')
             self.assertEqual(3, len(fields))
             refname, type_name, sha = fields
             check_ref_format(refname[5:])
@@ -66,7 +64,7 @@ class ObjectStoreTestCase(CompatTestCase):
         return refs
 
     def _parse_objects(self, output):
-        return set(s.rstrip('\n').split(' ')[0] for s in BytesIO(output))
+        return set(s.rstrip(b'\n').split(b' ')[0] for s in BytesIO(output))
 
     def test_bare(self):
         self.assertTrue(self._repo.bare)
@@ -74,9 +72,9 @@ class ObjectStoreTestCase(CompatTestCase):
 
     def test_head(self):
         output = self._run_git(['rev-parse', 'HEAD'])
-        head_sha = output.rstrip('\n')
+        head_sha = output.rstrip(b'\n')
         hex_to_sha(head_sha)
-        self.assertEqual(head_sha, self._repo.refs['HEAD'])
+        self.assertEqual(head_sha, self._repo.refs[b'HEAD'])
 
     def test_refs(self):
         output = self._run_git(
@@ -84,8 +82,8 @@ class ObjectStoreTestCase(CompatTestCase):
         expected_refs = self._parse_refs(output)
 
         actual_refs = {}
-        for refname, sha in self._repo.refs.as_dict().iteritems():
-            if refname == 'HEAD':
+        for refname, sha in self._repo.refs.as_dict().items():
+            if refname == b'HEAD':
                 continue  # handled in test_head
             obj = self._repo[sha]
             self.assertEqual(sha, obj.id)

+ 4 - 1
dulwich/tests/compat/test_server.py

@@ -26,11 +26,13 @@ Warning: these tests should be fairly stable, but when writing/debugging new
 
 import threading
 import os
+import sys
 
 from dulwich.server import (
     DictBackend,
     TCPGitServer,
     )
+from dulwich.tests import skipIf
 from dulwich.tests.compat.server_utils import (
     ServerTests,
     NoSideBand64kReceivePackHandler,
@@ -40,7 +42,7 @@ from dulwich.tests.compat.utils import (
     require_git_version,
     )
 
-
+@skipIf(sys.platform == 'win32', 'Broken on windows, with very long fail time.')
 class GitServerTestCase(ServerTests, CompatTestCase):
     """Tests for client/server compatibility.
 
@@ -70,6 +72,7 @@ class GitServerTestCase(ServerTests, CompatTestCase):
         return port
 
 
+@skipIf(sys.platform == 'win32', 'Broken on windows, with very long fail time.')
 class GitServerSideBand64kTestCase(GitServerTestCase):
     """Tests for client/server compatibility with side-band-64k support."""
 

+ 6 - 0
dulwich/tests/compat/test_web.py

@@ -26,12 +26,14 @@ warning: these tests should be fairly stable, but when writing/debugging new
 
 import threading
 from wsgiref import simple_server
+import sys
 
 from dulwich.server import (
     DictBackend,
     )
 from dulwich.tests import (
     SkipTest,
+    skipIf,
     )
 from dulwich.web import (
     make_wsgi_chain,
@@ -49,6 +51,7 @@ from dulwich.tests.compat.utils import (
     )
 
 
+@skipIf(sys.platform == 'win32', 'Broken on windows, with very long fail time.')
 class WebTests(ServerTests):
     """Base tests for web server tests.
 
@@ -72,6 +75,7 @@ class WebTests(ServerTests):
         return port
 
 
+@skipIf(sys.platform == 'win32', 'Broken on windows, with very long fail time.')
 class SmartWebTestCase(WebTests, CompatTestCase):
     """Test cases for smart HTTP server.
 
@@ -98,6 +102,7 @@ class SmartWebTestCase(WebTests, CompatTestCase):
         return app
 
 
+@skipIf(sys.platform == 'win32', 'Broken on windows, with very long fail time.')
 class SmartWebSideBand64kTestCase(SmartWebTestCase):
     """Test cases for smart HTTP server with side-band-64k support."""
 
@@ -113,6 +118,7 @@ class SmartWebSideBand64kTestCase(SmartWebTestCase):
         self.assertTrue('side-band-64k' in caps)
 
 
+@skipIf(sys.platform == 'win32', 'Broken on windows, with very long fail time.')
 class DumbWebTestCase(WebTests, CompatTestCase):
     """Test cases for dumb HTTP server."""
 

+ 2 - 2
dulwich/tests/compat/utils.py

@@ -139,8 +139,8 @@ def run_git_or_fail(args, git_path=_DEFAULT_GIT, input=None, **popen_kwargs):
     returncode, stdout = run_git(args, git_path=git_path, input=input,
                                  capture_stdout=True, **popen_kwargs)
     if returncode != 0:
-        raise AssertionError("git with args %r failed with %d" % (
-            args, returncode))
+        raise AssertionError("git with args %r failed with %d: %r" % (
+            args, returncode, stdout))
     return stdout
 
 

+ 7 - 12
dulwich/tests/test_blackbox.py

@@ -26,12 +26,8 @@ from dulwich.repo import (
 from dulwich.tests import (
     BlackboxTestCase,
     )
-from dulwich.tests.utils import (
-    skipIfPY3,
-    )
 
 
-@skipIfPY3
 class GitReceivePackTests(BlackboxTestCase):
     """Blackbox tests for dul-receive-pack."""
 
@@ -42,20 +38,19 @@ class GitReceivePackTests(BlackboxTestCase):
 
     def test_basic(self):
         process = self.run_command("dul-receive-pack", [self.path])
-        (stdout, stderr) = process.communicate("0000")
-        self.assertEqual('', stderr)
-        self.assertEqual('0000', stdout[-4:])
+        (stdout, stderr) = process.communicate(b"0000")
+        self.assertEqual(b'', stderr, stderr)
+        self.assertEqual(b'0000', stdout[-4:])
         self.assertEqual(0, process.returncode)
 
     def test_missing_arg(self):
         process = self.run_command("dul-receive-pack", [])
         (stdout, stderr) = process.communicate()
-        self.assertEqual(['usage: dul-receive-pack <git-dir>'], stderr.splitlines())
-        self.assertEqual('', stdout)
+        self.assertEqual([b'usage: dul-receive-pack <git-dir>'], stderr.splitlines())
+        self.assertEqual(b'', stdout)
         self.assertEqual(1, process.returncode)
 
 
-@skipIfPY3
 class GitUploadPackTests(BlackboxTestCase):
     """Blackbox tests for dul-upload-pack."""
 
@@ -67,6 +62,6 @@ class GitUploadPackTests(BlackboxTestCase):
     def test_missing_arg(self):
         process = self.run_command("dul-upload-pack", [])
         (stdout, stderr) = process.communicate()
-        self.assertEqual(['usage: dul-upload-pack <git-dir>'], stderr.splitlines())
-        self.assertEqual('', stdout)
+        self.assertEqual([b'usage: dul-upload-pack <git-dir>'], stderr.splitlines())
+        self.assertEqual(b'', stdout)
         self.assertEqual(1, process.returncode)

+ 117 - 130
dulwich/tests/test_client.py

@@ -21,10 +21,6 @@ import sys
 import shutil
 import tempfile
 
-try:
-    from unittest import skipIf
-except ImportError:
-    from unittest2 import skipIf
 
 from dulwich import (
     client,
@@ -60,9 +56,9 @@ from dulwich.repo import (
     MemoryRepo,
     Repo,
     )
+from dulwich.tests import skipIf
 from dulwich.tests.utils import (
     open_repo,
-    skipIfPY3,
     )
 
 
@@ -79,7 +75,6 @@ class DummyClient(TraditionalGitClient):
 
 
 # TODO(durin42): add unit-level tests of GitClient
-@skipIfPY3
 class GitClientTests(TestCase):
 
     def setUp(self):
@@ -90,63 +85,63 @@ class GitClientTests(TestCase):
                                   self.rout.write)
 
     def test_caps(self):
-        self.assertEqual(set(['multi_ack', 'side-band-64k', 'ofs-delta',
-                               'thin-pack', 'multi_ack_detailed']),
+        self.assertEqual(set([b'multi_ack', b'side-band-64k', b'ofs-delta',
+                               b'thin-pack', b'multi_ack_detailed']),
                           set(self.client._fetch_capabilities))
-        self.assertEqual(set(['ofs-delta', 'report-status', 'side-band-64k']),
+        self.assertEqual(set([b'ofs-delta', b'report-status', b'side-band-64k']),
                           set(self.client._send_capabilities))
 
     def test_archive_ack(self):
         self.rin.write(
-            '0009NACK\n'
-            '0000')
+            b'0009NACK\n'
+            b'0000')
         self.rin.seek(0)
-        self.client.archive('bla', 'HEAD', None, None)
-        self.assertEqual(self.rout.getvalue(), '0011argument HEAD0000')
+        self.client.archive(b'bla', b'HEAD', None, None)
+        self.assertEqual(self.rout.getvalue(), b'0011argument HEAD0000')
 
     def test_fetch_empty(self):
-        self.rin.write('0000')
+        self.rin.write(b'0000')
         self.rin.seek(0)
-        self.client.fetch_pack('/', lambda heads: [], None, None)
+        self.client.fetch_pack(b'/', lambda heads: [], None, None)
 
     def test_fetch_pack_none(self):
         self.rin.write(
-            '008855dcc6bf963f922e1ed5c4bbaaefcfacef57b1d7 HEAD.multi_ack '
-            'thin-pack side-band side-band-64k ofs-delta shallow no-progress '
-            'include-tag\n'
-            '0000')
+            b'008855dcc6bf963f922e1ed5c4bbaaefcfacef57b1d7 HEAD.multi_ack '
+            b'thin-pack side-band side-band-64k ofs-delta shallow no-progress '
+            b'include-tag\n'
+            b'0000')
         self.rin.seek(0)
-        self.client.fetch_pack('bla', lambda heads: [], None, None, None)
-        self.assertEqual(self.rout.getvalue(), '0000')
+        self.client.fetch_pack(b'bla', lambda heads: [], None, None, None)
+        self.assertEqual(self.rout.getvalue(), b'0000')
 
     def test_send_pack_no_sideband64k_with_update_ref_error(self):
         # No side-bank-64k reported by server shouldn't try to parse
         # side band data
-        pkts = ['55dcc6bf963f922e1ed5c4bbaaefcfacef57b1d7 capabilities^{}'
-                '\x00 report-status delete-refs ofs-delta\n',
-                '',
-                "unpack ok",
-                "ng refs/foo/bar pre-receive hook declined",
-                '']
+        pkts = [b'55dcc6bf963f922e1ed5c4bbaaefcfacef57b1d7 capabilities^{}'
+                b'\x00 report-status delete-refs ofs-delta\n',
+                b'',
+                b"unpack ok",
+                b"ng refs/foo/bar pre-receive hook declined",
+                b'']
         for pkt in pkts:
-            if pkt == '':
-                self.rin.write("0000")
+            if pkt ==  b'':
+                self.rin.write(b"0000")
             else:
-                self.rin.write("%04x%s" % (len(pkt)+4, pkt))
+                self.rin.write(("%04x" % (len(pkt)+4)).encode('ascii') + pkt)
         self.rin.seek(0)
 
         tree = Tree()
         commit = Commit()
         commit.tree = tree
         commit.parents = []
-        commit.author = commit.committer = 'test user'
+        commit.author = commit.committer = b'test user'
         commit.commit_time = commit.author_time = 1174773719
         commit.commit_timezone = commit.author_timezone = 0
-        commit.encoding = 'UTF-8'
-        commit.message = 'test message'
+        commit.encoding = b'UTF-8'
+        commit.message = b'test message'
 
         def determine_wants(refs):
-            return {'refs/foo/bar': commit.id, }
+            return {b'refs/foo/bar': commit.id, }
 
         def generate_pack_contents(have, want):
             return [(commit, None), (tree, ''), ]
@@ -157,62 +152,62 @@ class GitClientTests(TestCase):
 
     def test_send_pack_none(self):
         self.rin.write(
-            '0078310ca9477129b8586fa2afc779c1f57cf64bba6c '
-            'refs/heads/master\x00 report-status delete-refs '
-            'side-band-64k quiet ofs-delta\n'
-            '0000')
+            b'0078310ca9477129b8586fa2afc779c1f57cf64bba6c '
+            b'refs/heads/master\x00 report-status delete-refs '
+            b'side-band-64k quiet ofs-delta\n'
+            b'0000')
         self.rin.seek(0)
 
         def determine_wants(refs):
             return {
-                'refs/heads/master': '310ca9477129b8586fa2afc779c1f57cf64bba6c'
+                b'refs/heads/master': b'310ca9477129b8586fa2afc779c1f57cf64bba6c'
             }
 
         def generate_pack_contents(have, want):
             return {}
 
-        self.client.send_pack('/', determine_wants, generate_pack_contents)
-        self.assertEqual(self.rout.getvalue(), '0000')
+        self.client.send_pack(b'/', determine_wants, generate_pack_contents)
+        self.assertEqual(self.rout.getvalue(), b'0000')
 
     def test_send_pack_delete_only(self):
         self.rin.write(
-            '0063310ca9477129b8586fa2afc779c1f57cf64bba6c '
-            'refs/heads/master\x00report-status delete-refs ofs-delta\n'
-            '0000000eunpack ok\n'
-            '0019ok refs/heads/master\n'
-            '0000')
+            b'0063310ca9477129b8586fa2afc779c1f57cf64bba6c '
+            b'refs/heads/master\x00report-status delete-refs ofs-delta\n'
+            b'0000000eunpack ok\n'
+            b'0019ok refs/heads/master\n'
+            b'0000')
         self.rin.seek(0)
 
         def determine_wants(refs):
-            return {'refs/heads/master': '0' * 40}
+            return {b'refs/heads/master': b'0' * 40}
 
         def generate_pack_contents(have, want):
             return {}
 
-        self.client.send_pack('/', determine_wants, generate_pack_contents)
+        self.client.send_pack(b'/', determine_wants, generate_pack_contents)
         self.assertIn(
             self.rout.getvalue(),
-            ['007f310ca9477129b8586fa2afc779c1f57cf64bba6c '
-             '0000000000000000000000000000000000000000 '
-             'refs/heads/master\x00report-status ofs-delta0000',
-             '007f310ca9477129b8586fa2afc779c1f57cf64bba6c '
-             '0000000000000000000000000000000000000000 '
-             'refs/heads/master\x00ofs-delta report-status0000'])
+            [b'007f310ca9477129b8586fa2afc779c1f57cf64bba6c '
+             b'0000000000000000000000000000000000000000 '
+             b'refs/heads/master\x00report-status ofs-delta0000',
+             b'007f310ca9477129b8586fa2afc779c1f57cf64bba6c '
+             b'0000000000000000000000000000000000000000 '
+             b'refs/heads/master\x00ofs-delta report-status0000'])
 
     def test_send_pack_new_ref_only(self):
         self.rin.write(
-            '0063310ca9477129b8586fa2afc779c1f57cf64bba6c '
-            'refs/heads/master\x00report-status delete-refs ofs-delta\n'
-            '0000000eunpack ok\n'
-            '0019ok refs/heads/blah12\n'
-            '0000')
+            b'0063310ca9477129b8586fa2afc779c1f57cf64bba6c '
+            b'refs/heads/master\x00report-status delete-refs ofs-delta\n'
+            b'0000000eunpack ok\n'
+            b'0019ok refs/heads/blah12\n'
+            b'0000')
         self.rin.seek(0)
 
         def determine_wants(refs):
             return {
-                'refs/heads/blah12':
-                '310ca9477129b8586fa2afc779c1f57cf64bba6c',
-                'refs/heads/master': '310ca9477129b8586fa2afc779c1f57cf64bba6c'
+                b'refs/heads/blah12':
+                b'310ca9477129b8586fa2afc779c1f57cf64bba6c',
+                b'refs/heads/master': b'310ca9477129b8586fa2afc779c1f57cf64bba6c'
             }
 
         def generate_pack_contents(have, want):
@@ -223,80 +218,77 @@ class GitClientTests(TestCase):
         self.client.send_pack('/', determine_wants, generate_pack_contents)
         self.assertIn(
             self.rout.getvalue(),
-            ['007f0000000000000000000000000000000000000000 '
-             '310ca9477129b8586fa2afc779c1f57cf64bba6c '
-             'refs/heads/blah12\x00report-status ofs-delta0000%s'
-             % f.getvalue(),
-             '007f0000000000000000000000000000000000000000 '
-             '310ca9477129b8586fa2afc779c1f57cf64bba6c '
-             'refs/heads/blah12\x00ofs-delta report-status0000%s'
-             % f.getvalue()])
+            [b'007f0000000000000000000000000000000000000000 '
+             b'310ca9477129b8586fa2afc779c1f57cf64bba6c '
+             b'refs/heads/blah12\x00report-status ofs-delta0000' +
+             f.getvalue(),
+             b'007f0000000000000000000000000000000000000000 '
+             b'310ca9477129b8586fa2afc779c1f57cf64bba6c '
+             b'refs/heads/blah12\x00ofs-delta report-status0000' +
+             f.getvalue()])
 
     def test_send_pack_new_ref(self):
         self.rin.write(
-            '0064310ca9477129b8586fa2afc779c1f57cf64bba6c '
-            'refs/heads/master\x00 report-status delete-refs ofs-delta\n'
-            '0000000eunpack ok\n'
-            '0019ok refs/heads/blah12\n'
-            '0000')
+            b'0064310ca9477129b8586fa2afc779c1f57cf64bba6c '
+            b'refs/heads/master\x00 report-status delete-refs ofs-delta\n'
+            b'0000000eunpack ok\n'
+            b'0019ok refs/heads/blah12\n'
+            b'0000')
         self.rin.seek(0)
 
         tree = Tree()
         commit = Commit()
         commit.tree = tree
         commit.parents = []
-        commit.author = commit.committer = 'test user'
+        commit.author = commit.committer = b'test user'
         commit.commit_time = commit.author_time = 1174773719
         commit.commit_timezone = commit.author_timezone = 0
-        commit.encoding = 'UTF-8'
-        commit.message = 'test message'
+        commit.encoding = b'UTF-8'
+        commit.message = b'test message'
 
         def determine_wants(refs):
             return {
-                'refs/heads/blah12': commit.id,
-                'refs/heads/master': '310ca9477129b8586fa2afc779c1f57cf64bba6c'
+                b'refs/heads/blah12': commit.id,
+                b'refs/heads/master': b'310ca9477129b8586fa2afc779c1f57cf64bba6c'
             }
 
         def generate_pack_contents(have, want):
-            return [(commit, None), (tree, ''), ]
+            return [(commit, None), (tree, b''), ]
 
         f = BytesIO()
         write_pack_objects(f, generate_pack_contents(None, None))
-        self.client.send_pack('/', determine_wants, generate_pack_contents)
+        self.client.send_pack(b'/', determine_wants, generate_pack_contents)
         self.assertIn(
             self.rout.getvalue(),
-            ['007f0000000000000000000000000000000000000000 %s '
-             'refs/heads/blah12\x00report-status ofs-delta0000%s'
-             % (commit.id, f.getvalue()),
-             '007f0000000000000000000000000000000000000000 %s '
-             'refs/heads/blah12\x00ofs-delta report-status0000%s'
-             % (commit.id, f.getvalue())])
+            [b'007f0000000000000000000000000000000000000000 ' + commit.id +
+             b' refs/heads/blah12\x00report-status ofs-delta0000' + f.getvalue(),
+             b'007f0000000000000000000000000000000000000000 ' + commit.id +
+             b' refs/heads/blah12\x00ofs-delta report-status0000' + f.getvalue()])
 
     def test_send_pack_no_deleteref_delete_only(self):
-        pkts = ['310ca9477129b8586fa2afc779c1f57cf64bba6c refs/heads/master'
-                '\x00 report-status ofs-delta\n',
-                '',
-                '']
+        pkts = [b'310ca9477129b8586fa2afc779c1f57cf64bba6c refs/heads/master'
+                b'\x00 report-status ofs-delta\n',
+                b'',
+                b'']
         for pkt in pkts:
-            if pkt == '':
-                self.rin.write("0000")
+            if pkt == b'':
+                self.rin.write(b"0000")
             else:
-                self.rin.write("%04x%s" % (len(pkt)+4, pkt))
+                self.rin.write(("%04x" % (len(pkt)+4)).encode('ascii') + pkt)
         self.rin.seek(0)
 
         def determine_wants(refs):
-            return {'refs/heads/master': '0' * 40}
+            return {b'refs/heads/master': b'0' * 40}
 
         def generate_pack_contents(have, want):
             return {}
 
         self.assertRaises(UpdateRefsError,
-                          self.client.send_pack, "/",
+                          self.client.send_pack, b"/",
                           determine_wants, generate_pack_contents)
-        self.assertEqual(self.rout.getvalue(), '0000')
+        self.assertEqual(self.rout.getvalue(), b'0000')
 
 
-@skipIfPY3
 class TestGetTransportAndPath(TestCase):
 
     def test_tcp(self):
@@ -417,7 +409,6 @@ class TestGetTransportAndPath(TestCase):
         self.assertEqual('/jelmer/dulwich', path)
 
 
-@skipIfPY3
 class TestGetTransportAndPathFromUrl(TestCase):
 
     def test_tcp(self):
@@ -496,7 +487,6 @@ class TestGetTransportAndPathFromUrl(TestCase):
         self.assertEqual('/home/jelmer/foo', path)
 
 
-@skipIfPY3
 class TestSSHVendor(object):
 
     def __init__(self):
@@ -519,7 +509,6 @@ class TestSSHVendor(object):
         return Subprocess()
 
 
-@skipIfPY3
 class SSHGitClientTests(TestCase):
 
     def setUp(self):
@@ -536,58 +525,56 @@ class SSHGitClientTests(TestCase):
         client.get_ssh_vendor = self.real_vendor
 
     def test_default_command(self):
-        self.assertEqual('git-upload-pack',
-                self.client._get_cmd_path('upload-pack'))
+        self.assertEqual(b'git-upload-pack',
+                self.client._get_cmd_path(b'upload-pack'))
 
     def test_alternative_command_path(self):
-        self.client.alternative_paths['upload-pack'] = (
-            '/usr/lib/git/git-upload-pack')
-        self.assertEqual('/usr/lib/git/git-upload-pack',
-            self.client._get_cmd_path('upload-pack'))
+        self.client.alternative_paths[b'upload-pack'] = (
+            b'/usr/lib/git/git-upload-pack')
+        self.assertEqual(b'/usr/lib/git/git-upload-pack',
+            self.client._get_cmd_path(b'upload-pack'))
 
     def test_connect(self):
         server = self.server
         client = self.client
 
-        client.username = "username"
+        client.username = b"username"
         client.port = 1337
 
-        client._connect("command", "/path/to/repo")
-        self.assertEqual("username", server.username)
+        client._connect(b"command", b"/path/to/repo")
+        self.assertEqual(b"username", server.username)
         self.assertEqual(1337, server.port)
-        self.assertEqual(["git-command '/path/to/repo'"], server.command)
+        self.assertEqual([b"git-command '/path/to/repo'"], server.command)
 
-        client._connect("relative-command", "/~/path/to/repo")
-        self.assertEqual(["git-relative-command '~/path/to/repo'"],
+        client._connect(b"relative-command", b"/~/path/to/repo")
+        self.assertEqual([b"git-relative-command '~/path/to/repo'"],
                           server.command)
 
 
-@skipIfPY3
 class ReportStatusParserTests(TestCase):
 
     def test_invalid_pack(self):
         parser = ReportStatusParser()
-        parser.handle_packet("unpack error - foo bar")
-        parser.handle_packet("ok refs/foo/bar")
+        parser.handle_packet(b"unpack error - foo bar")
+        parser.handle_packet(b"ok refs/foo/bar")
         parser.handle_packet(None)
         self.assertRaises(SendPackError, parser.check)
 
     def test_update_refs_error(self):
         parser = ReportStatusParser()
-        parser.handle_packet("unpack ok")
-        parser.handle_packet("ng refs/foo/bar need to pull")
+        parser.handle_packet(b"unpack ok")
+        parser.handle_packet(b"ng refs/foo/bar need to pull")
         parser.handle_packet(None)
         self.assertRaises(UpdateRefsError, parser.check)
 
     def test_ok(self):
         parser = ReportStatusParser()
-        parser.handle_packet("unpack ok")
-        parser.handle_packet("ok refs/foo/bar")
+        parser.handle_packet(b"unpack ok")
+        parser.handle_packet(b"ok refs/foo/bar")
         parser.handle_packet(None)
         parser.check()
 
 
-@skipIfPY3
 class LocalGitClientTests(TestCase):
 
     def test_fetch_into_empty(self):
@@ -603,8 +590,8 @@ class LocalGitClientTests(TestCase):
         walker = {}
         c.fetch_pack(s.path, lambda heads: [], graph_walker=walker,
             pack_data=out.write)
-        self.assertEqual("PACK\x00\x00\x00\x02\x00\x00\x00\x00\x02\x9d\x08"
-            "\x82;\xd8\xa8\xea\xb5\x10\xadj\xc7\\\x82<\xfd>\xd3\x1e", out.getvalue())
+        self.assertEqual(b"PACK\x00\x00\x00\x02\x00\x00\x00\x00\x02\x9d\x08"
+            b"\x82;\xd8\xa8\xea\xb5\x10\xadj\xc7\\\x82<\xfd>\xd3\x1e", out.getvalue())
 
     def test_fetch_pack_none(self):
         c = LocalGitClient()
@@ -612,33 +599,33 @@ class LocalGitClientTests(TestCase):
         out = BytesIO()
         walker = MemoryRepo().get_graph_walker()
         c.fetch_pack(s.path,
-            lambda heads: ["a90fa2d900a17e99b433217e988c4eb4a2e9a097"],
+            lambda heads: [b"a90fa2d900a17e99b433217e988c4eb4a2e9a097"],
             graph_walker=walker, pack_data=out.write)
         # Hardcoding is not ideal, but we'll fix that some other day..
-        self.assertTrue(out.getvalue().startswith('PACK\x00\x00\x00\x02\x00\x00\x00\x07'))
+        self.assertTrue(out.getvalue().startswith(b'PACK\x00\x00\x00\x02\x00\x00\x00\x07'))
 
     def test_send_pack_without_changes(self):
         local = open_repo('a.git')
         target = open_repo('a.git')
-        self.send_and_verify("master", local, target)
+        self.send_and_verify(b"master", local, target)
 
     def test_send_pack_with_changes(self):
         local = open_repo('a.git')
         target_path = tempfile.mkdtemp()
         self.addCleanup(shutil.rmtree, target_path)
         target = Repo.init_bare(target_path)
-        self.send_and_verify("master", local, target)
+        self.send_and_verify(b"master", local, target)
 
     def send_and_verify(self, branch, local, target):
         client = LocalGitClient()
-        ref_name = "refs/heads/" + branch
+        ref_name = b"refs/heads/" + branch
         new_refs = client.send_pack(target.path,
                                     lambda _: { ref_name: local.refs[ref_name] },
                                     local.object_store.generate_pack_contents)
 
         self.assertEqual(local.refs[ref_name], new_refs[ref_name])
 
-        for name, sha in new_refs.iteritems():
+        for name, sha in new_refs.items():
             self.assertEqual(new_refs[name], target.refs[name])
 
         obj_local = local.get_object(new_refs[ref_name])

+ 98 - 108
dulwich/tests/test_config.py

@@ -31,10 +31,8 @@ from dulwich.config import (
     _unescape_value,
     )
 from dulwich.tests import TestCase
-from dulwich.tests.utils import skipIfPY3
 
 
-@skipIfPY3
 class ConfigFileTests(TestCase):
 
     def from_file(self, text):
@@ -47,251 +45,243 @@ class ConfigFileTests(TestCase):
         self.assertEqual(ConfigFile(), ConfigFile())
 
     def test_default_config(self):
-        cf = self.from_file("""[core]
+        cf = self.from_file(b"""[core]
 	repositoryformatversion = 0
 	filemode = true
 	bare = false
 	logallrefupdates = true
 """)
-        self.assertEqual(ConfigFile({("core", ): {
-            "repositoryformatversion": "0",
-            "filemode": "true",
-            "bare": "false",
-            "logallrefupdates": "true"}}), cf)
+        self.assertEqual(ConfigFile({(b"core", ): {
+            b"repositoryformatversion": b"0",
+            b"filemode": b"true",
+            b"bare": b"false",
+            b"logallrefupdates": b"true"}}), cf)
 
     def test_from_file_empty(self):
-        cf = self.from_file("")
+        cf = self.from_file(b"")
         self.assertEqual(ConfigFile(), cf)
 
     def test_empty_line_before_section(self):
-        cf = self.from_file("\n[section]\n")
-        self.assertEqual(ConfigFile({("section", ): {}}), cf)
+        cf = self.from_file(b"\n[section]\n")
+        self.assertEqual(ConfigFile({(b"section", ): {}}), cf)
 
     def test_comment_before_section(self):
-        cf = self.from_file("# foo\n[section]\n")
-        self.assertEqual(ConfigFile({("section", ): {}}), cf)
+        cf = self.from_file(b"# foo\n[section]\n")
+        self.assertEqual(ConfigFile({(b"section", ): {}}), cf)
 
     def test_comment_after_section(self):
-        cf = self.from_file("[section] # foo\n")
-        self.assertEqual(ConfigFile({("section", ): {}}), cf)
+        cf = self.from_file(b"[section] # foo\n")
+        self.assertEqual(ConfigFile({(b"section", ): {}}), cf)
 
     def test_comment_after_variable(self):
-        cf = self.from_file("[section]\nbar= foo # a comment\n")
-        self.assertEqual(ConfigFile({("section", ): {"bar": "foo"}}), cf)
+        cf = self.from_file(b"[section]\nbar= foo # a comment\n")
+        self.assertEqual(ConfigFile({(b"section", ): {b"bar": b"foo"}}), cf)
 
     def test_from_file_section(self):
-        cf = self.from_file("[core]\nfoo = bar\n")
-        self.assertEqual("bar", cf.get(("core", ), "foo"))
-        self.assertEqual("bar", cf.get(("core", "foo"), "foo"))
+        cf = self.from_file(b"[core]\nfoo = bar\n")
+        self.assertEqual(b"bar", cf.get((b"core", ), b"foo"))
+        self.assertEqual(b"bar", cf.get((b"core", b"foo"), b"foo"))
 
     def test_from_file_section_case_insensitive(self):
-        cf = self.from_file("[cOre]\nfOo = bar\n")
-        self.assertEqual("bar", cf.get(("core", ), "foo"))
-        self.assertEqual("bar", cf.get(("core", "foo"), "foo"))
+        cf = self.from_file(b"[cOre]\nfOo = bar\n")
+        self.assertEqual(b"bar", cf.get((b"core", ), b"foo"))
+        self.assertEqual(b"bar", cf.get((b"core", b"foo"), b"foo"))
 
     def test_from_file_with_mixed_quoted(self):
-        cf = self.from_file("[core]\nfoo = \"bar\"la\n")
-        self.assertEqual("barla", cf.get(("core", ), "foo"))
+        cf = self.from_file(b"[core]\nfoo = \"bar\"la\n")
+        self.assertEqual(b"barla", cf.get((b"core", ), b"foo"))
 
     def test_from_file_with_open_quoted(self):
         self.assertRaises(ValueError,
-            self.from_file, "[core]\nfoo = \"bar\n")
+            self.from_file, b"[core]\nfoo = \"bar\n")
 
     def test_from_file_with_quotes(self):
         cf = self.from_file(
-            "[core]\n"
-            'foo = " bar"\n')
-        self.assertEqual(" bar", cf.get(("core", ), "foo"))
+            b"[core]\n"
+            b'foo = " bar"\n')
+        self.assertEqual(b" bar", cf.get((b"core", ), b"foo"))
 
     def test_from_file_with_interrupted_line(self):
         cf = self.from_file(
-            "[core]\n"
-            'foo = bar\\\n'
-            ' la\n')
-        self.assertEqual("barla", cf.get(("core", ), "foo"))
+            b"[core]\n"
+            b'foo = bar\\\n'
+            b' la\n')
+        self.assertEqual(b"barla", cf.get((b"core", ), b"foo"))
 
     def test_from_file_with_boolean_setting(self):
         cf = self.from_file(
-            "[core]\n"
-            'foo\n')
-        self.assertEqual("true", cf.get(("core", ), "foo"))
+            b"[core]\n"
+            b'foo\n')
+        self.assertEqual(b"true", cf.get((b"core", ), b"foo"))
 
     def test_from_file_subsection(self):
-        cf = self.from_file("[branch \"foo\"]\nfoo = bar\n")
-        self.assertEqual("bar", cf.get(("branch", "foo"), "foo"))
+        cf = self.from_file(b"[branch \"foo\"]\nfoo = bar\n")
+        self.assertEqual(b"bar", cf.get((b"branch", b"foo"), b"foo"))
 
     def test_from_file_subsection_invalid(self):
         self.assertRaises(ValueError,
-            self.from_file, "[branch \"foo]\nfoo = bar\n")
+            self.from_file, b"[branch \"foo]\nfoo = bar\n")
 
     def test_from_file_subsection_not_quoted(self):
-        cf = self.from_file("[branch.foo]\nfoo = bar\n")
-        self.assertEqual("bar", cf.get(("branch", "foo"), "foo"))
+        cf = self.from_file(b"[branch.foo]\nfoo = bar\n")
+        self.assertEqual(b"bar", cf.get((b"branch", b"foo"), b"foo"))
 
     def test_write_to_file_empty(self):
         c = ConfigFile()
         f = BytesIO()
         c.write_to_file(f)
-        self.assertEqual("", f.getvalue())
+        self.assertEqual(b"", f.getvalue())
 
     def test_write_to_file_section(self):
         c = ConfigFile()
-        c.set(("core", ), "foo", "bar")
+        c.set((b"core", ), b"foo", b"bar")
         f = BytesIO()
         c.write_to_file(f)
-        self.assertEqual("[core]\n\tfoo = bar\n", f.getvalue())
+        self.assertEqual(b"[core]\n\tfoo = bar\n", f.getvalue())
 
     def test_write_to_file_subsection(self):
         c = ConfigFile()
-        c.set(("branch", "blie"), "foo", "bar")
+        c.set((b"branch", b"blie"), b"foo", b"bar")
         f = BytesIO()
         c.write_to_file(f)
-        self.assertEqual("[branch \"blie\"]\n\tfoo = bar\n", f.getvalue())
+        self.assertEqual(b"[branch \"blie\"]\n\tfoo = bar\n", f.getvalue())
 
     def test_same_line(self):
-        cf = self.from_file("[branch.foo] foo = bar\n")
-        self.assertEqual("bar", cf.get(("branch", "foo"), "foo"))
+        cf = self.from_file(b"[branch.foo] foo = bar\n")
+        self.assertEqual(b"bar", cf.get((b"branch", b"foo"), b"foo"))
 
 
-@skipIfPY3
 class ConfigDictTests(TestCase):
 
     def test_get_set(self):
         cd = ConfigDict()
-        self.assertRaises(KeyError, cd.get, "foo", "core")
-        cd.set(("core", ), "foo", "bla")
-        self.assertEqual("bla", cd.get(("core", ), "foo"))
-        cd.set(("core", ), "foo", "bloe")
-        self.assertEqual("bloe", cd.get(("core", ), "foo"))
+        self.assertRaises(KeyError, cd.get, b"foo", b"core")
+        cd.set((b"core", ), b"foo", b"bla")
+        self.assertEqual(b"bla", cd.get((b"core", ), b"foo"))
+        cd.set((b"core", ), b"foo", b"bloe")
+        self.assertEqual(b"bloe", cd.get((b"core", ), b"foo"))
 
     def test_get_boolean(self):
         cd = ConfigDict()
-        cd.set(("core", ), "foo", "true")
-        self.assertTrue(cd.get_boolean(("core", ), "foo"))
-        cd.set(("core", ), "foo", "false")
-        self.assertFalse(cd.get_boolean(("core", ), "foo"))
-        cd.set(("core", ), "foo", "invalid")
-        self.assertRaises(ValueError, cd.get_boolean, ("core", ), "foo")
+        cd.set((b"core", ), b"foo", b"true")
+        self.assertTrue(cd.get_boolean((b"core", ), b"foo"))
+        cd.set((b"core", ), b"foo", b"false")
+        self.assertFalse(cd.get_boolean((b"core", ), b"foo"))
+        cd.set((b"core", ), b"foo", b"invalid")
+        self.assertRaises(ValueError, cd.get_boolean, (b"core", ), b"foo")
 
     def test_dict(self):
         cd = ConfigDict()
-        cd.set(("core", ), "foo", "bla")
-        cd.set(("core2", ), "foo", "bloe")
+        cd.set((b"core", ), b"foo", b"bla")
+        cd.set((b"core2", ), b"foo", b"bloe")
 
-        self.assertEqual([("core", ), ("core2", )], cd.keys())
-        self.assertEqual(cd[("core", )], {'foo': 'bla'})
+        self.assertEqual([(b"core", ), (b"core2", )], list(cd.keys()))
+        self.assertEqual(cd[(b"core", )], {b'foo': b'bla'})
 
-        cd['a'] = 'b'
-        self.assertEqual(cd['a'], 'b')
+        cd[b'a'] = b'b'
+        self.assertEqual(cd[b'a'], b'b')
 
     def test_iteritems(self):
         cd = ConfigDict()
-        cd.set(("core", ), "foo", "bla")
-        cd.set(("core2", ), "foo", "bloe")
+        cd.set((b"core", ), b"foo", b"bla")
+        cd.set((b"core2", ), b"foo", b"bloe")
 
         self.assertEqual(
-            [('foo', 'bla')],
-            list(cd.iteritems(("core", ))))
+            [(b'foo', b'bla')],
+            list(cd.iteritems((b"core", ))))
 
     def test_iteritems_nonexistant(self):
         cd = ConfigDict()
-        cd.set(("core2", ), "foo", "bloe")
+        cd.set((b"core2", ), b"foo", b"bloe")
 
         self.assertEqual([],
-            list(cd.iteritems(("core", ))))
+            list(cd.iteritems((b"core", ))))
 
     def test_itersections(self):
         cd = ConfigDict()
-        cd.set(("core2", ), "foo", "bloe")
+        cd.set((b"core2", ), b"foo", b"bloe")
 
-        self.assertEqual([("core2", )],
+        self.assertEqual([(b"core2", )],
             list(cd.itersections()))
 
 
 
-@skipIfPY3
 class StackedConfigTests(TestCase):
 
     def test_default_backends(self):
         StackedConfig.default_backends()
 
 
-@skipIfPY3
 class UnescapeTests(TestCase):
 
     def test_nothing(self):
-        self.assertEqual("", _unescape_value(""))
+        self.assertEqual(b"", bytes(_unescape_value(bytearray())))
 
     def test_tab(self):
-        self.assertEqual("\tbar\t", _unescape_value("\\tbar\\t"))
+        self.assertEqual(b"\tbar\t", bytes(_unescape_value(bytearray(b"\\tbar\\t"))))
 
     def test_newline(self):
-        self.assertEqual("\nbar\t", _unescape_value("\\nbar\\t"))
+        self.assertEqual(b"\nbar\t", bytes(_unescape_value(bytearray(b"\\nbar\\t"))))
 
     def test_quote(self):
-        self.assertEqual("\"foo\"", _unescape_value("\\\"foo\\\""))
+        self.assertEqual(b"\"foo\"", bytes(_unescape_value(bytearray(b"\\\"foo\\\""))))
 
 
-@skipIfPY3
 class EscapeValueTests(TestCase):
 
     def test_nothing(self):
-        self.assertEqual("foo", _escape_value("foo"))
+        self.assertEqual(b"foo", _escape_value(b"foo"))
 
     def test_backslash(self):
-        self.assertEqual("foo\\\\", _escape_value("foo\\"))
+        self.assertEqual(b"foo\\\\", _escape_value(b"foo\\"))
 
     def test_newline(self):
-        self.assertEqual("foo\\n", _escape_value("foo\n"))
+        self.assertEqual(b"foo\\n", _escape_value(b"foo\n"))
 
 
-@skipIfPY3
 class FormatStringTests(TestCase):
 
     def test_quoted(self):
-        self.assertEqual('" foo"', _format_string(" foo"))
-        self.assertEqual('"\\tfoo"', _format_string("\tfoo"))
+        self.assertEqual(b'" foo"', _format_string(b" foo"))
+        self.assertEqual(b'"\\tfoo"', _format_string(b"\tfoo"))
 
     def test_not_quoted(self):
-        self.assertEqual('foo', _format_string("foo"))
-        self.assertEqual('foo bar', _format_string("foo bar"))
+        self.assertEqual(b'foo', _format_string(b"foo"))
+        self.assertEqual(b'foo bar', _format_string(b"foo bar"))
 
 
-@skipIfPY3
 class ParseStringTests(TestCase):
 
     def test_quoted(self):
-        self.assertEqual(' foo', _parse_string('" foo"'))
-        self.assertEqual('\tfoo', _parse_string('"\\tfoo"'))
+        self.assertEqual(b' foo', _parse_string(b'" foo"'))
+        self.assertEqual(b'\tfoo', _parse_string(b'"\\tfoo"'))
 
     def test_not_quoted(self):
-        self.assertEqual('foo', _parse_string("foo"))
-        self.assertEqual('foo bar', _parse_string("foo bar"))
+        self.assertEqual(b'foo', _parse_string(b"foo"))
+        self.assertEqual(b'foo bar', _parse_string(b"foo bar"))
 
 
-@skipIfPY3
 class CheckVariableNameTests(TestCase):
 
     def test_invalid(self):
-        self.assertFalse(_check_variable_name("foo "))
-        self.assertFalse(_check_variable_name("bar,bar"))
-        self.assertFalse(_check_variable_name("bar.bar"))
+        self.assertFalse(_check_variable_name(b"foo "))
+        self.assertFalse(_check_variable_name(b"bar,bar"))
+        self.assertFalse(_check_variable_name(b"bar.bar"))
 
     def test_valid(self):
-        self.assertTrue(_check_variable_name("FOO"))
-        self.assertTrue(_check_variable_name("foo"))
-        self.assertTrue(_check_variable_name("foo-bar"))
+        self.assertTrue(_check_variable_name(b"FOO"))
+        self.assertTrue(_check_variable_name(b"foo"))
+        self.assertTrue(_check_variable_name(b"foo-bar"))
 
 
-@skipIfPY3
 class CheckSectionNameTests(TestCase):
 
     def test_invalid(self):
-        self.assertFalse(_check_section_name("foo "))
-        self.assertFalse(_check_section_name("bar,bar"))
+        self.assertFalse(_check_section_name(b"foo "))
+        self.assertFalse(_check_section_name(b"bar,bar"))
 
     def test_valid(self):
-        self.assertTrue(_check_section_name("FOO"))
-        self.assertTrue(_check_section_name("foo"))
-        self.assertTrue(_check_section_name("foo-bar"))
-        self.assertTrue(_check_section_name("bar.bar"))
+        self.assertTrue(_check_section_name(b"FOO"))
+        self.assertTrue(_check_section_name(b"foo"))
+        self.assertTrue(_check_section_name(b"foo-bar"))
+        self.assertTrue(_check_section_name(b"bar.bar"))

+ 8 - 11
dulwich/tests/test_diff_tree.py

@@ -57,11 +57,9 @@ from dulwich.tests.utils import (
     make_object,
     functest_builder,
     ext_functest_builder,
-    skipIfPY3,
     )
 
 
-@skipIfPY3
 class DiffTestCase(TestCase):
 
     def setUp(self):
@@ -86,7 +84,6 @@ class DiffTestCase(TestCase):
         return self.store[commit_tree(self.store, commit_blobs)]
 
 
-@skipIfPY3
 class TreeChangesTest(DiffTestCase):
 
     def setUp(self):
@@ -470,7 +467,6 @@ class TreeChangesTest(DiffTestCase):
             [parent1, parent2], merge, rename_detector=self.detector)
 
 
-@skipIfPY3
 class RenameDetectionTest(DiffTestCase):
 
     def _do_test_count_blocks(self, count_blocks):
@@ -759,15 +755,16 @@ class RenameDetectionTest(DiffTestCase):
             self.detect_renames(tree1, tree2))
 
     def test_content_rename_with_more_deletions(self):
-        blob1 = make_object(Blob, data='')
-        tree1 = self.commit_tree([('a', blob1), ('b', blob1), ('c', blob1), ('d', blob1)])
-        tree2 = self.commit_tree([('e', blob1), ('f', blob1), ('g', blob1)])
+        blob1 = make_object(Blob, data=b'')
+        tree1 = self.commit_tree([(b'a', blob1), (b'b', blob1), (b'c', blob1),
+                                  (b'd', blob1)])
+        tree2 = self.commit_tree([(b'e', blob1), (b'f', blob1), (b'g', blob1)])
         self.maxDiff = None
         self.assertEqual(
-          [TreeChange(CHANGE_RENAME, ('a', F, blob1.id), ('e', F, blob1.id)),
-           TreeChange(CHANGE_RENAME, ('b', F, blob1.id), ('f', F, blob1.id)),
-           TreeChange(CHANGE_RENAME, ('c', F, blob1.id), ('g', F, blob1.id)),
-           TreeChange.delete(('d', F, blob1.id))],
+          [TreeChange(CHANGE_RENAME, (b'a', F, blob1.id), (b'e', F, blob1.id)),
+           TreeChange(CHANGE_RENAME, (b'b', F, blob1.id), (b'f', F, blob1.id)),
+           TreeChange(CHANGE_RENAME, (b'c', F, blob1.id), (b'g', F, blob1.id)),
+           TreeChange.delete((b'd', F, blob1.id))],
           self.detect_renames(tree1, tree2))
 
     def test_content_rename_gitlink(self):

+ 14 - 12
dulwich/tests/test_file.py

@@ -92,7 +92,9 @@ class GitFileTests(TestCase):
     def setUp(self):
         super(GitFileTests, self).setUp()
         self._tempdir = tempfile.mkdtemp()
-        f = open(self.path('foo'), 'wb')
+        if not isinstance(self._tempdir, bytes):
+            self._tempdir = self._tempdir.encode(sys.getfilesystemencoding())
+        f = open(self.path(b'foo'), 'wb')
         f.write(b'foo contents')
         f.close()
 
@@ -104,7 +106,7 @@ class GitFileTests(TestCase):
         return os.path.join(self._tempdir, filename)
 
     def test_invalid(self):
-        foo = self.path('foo')
+        foo = self.path(b'foo')
         self.assertRaises(IOError, GitFile, foo, mode='r')
         self.assertRaises(IOError, GitFile, foo, mode='ab')
         self.assertRaises(IOError, GitFile, foo, mode='r+b')
@@ -112,7 +114,7 @@ class GitFileTests(TestCase):
         self.assertRaises(IOError, GitFile, foo, mode='a+bU')
 
     def test_readonly(self):
-        f = GitFile(self.path('foo'), 'rb')
+        f = GitFile(self.path(b'foo'), 'rb')
         self.assertTrue(isinstance(f, io.IOBase))
         self.assertEqual(b'foo contents', f.read())
         self.assertEqual(b'', f.read())
@@ -121,13 +123,13 @@ class GitFileTests(TestCase):
         f.close()
 
     def test_default_mode(self):
-        f = GitFile(self.path('foo'))
+        f = GitFile(self.path(b'foo'))
         self.assertEqual(b'foo contents', f.read())
         f.close()
 
     def test_write(self):
-        foo = self.path('foo')
-        foo_lock = '%s.lock' % foo
+        foo = self.path(b'foo')
+        foo_lock = foo + b'.lock'
 
         orig_f = open(foo, 'rb')
         self.assertEqual(orig_f.read(), b'foo contents')
@@ -150,7 +152,7 @@ class GitFileTests(TestCase):
         new_f.close()
 
     def test_open_twice(self):
-        foo = self.path('foo')
+        foo = self.path(b'foo')
         f1 = GitFile(foo, 'wb')
         f1.write(b'new')
         try:
@@ -169,8 +171,8 @@ class GitFileTests(TestCase):
         f.close()
 
     def test_abort(self):
-        foo = self.path('foo')
-        foo_lock = '%s.lock' % foo
+        foo = self.path(b'foo')
+        foo_lock = foo + b'.lock'
 
         orig_f = open(foo, 'rb')
         self.assertEqual(orig_f.read(), b'foo contents')
@@ -187,7 +189,7 @@ class GitFileTests(TestCase):
         new_orig_f.close()
 
     def test_abort_close(self):
-        foo = self.path('foo')
+        foo = self.path(b'foo')
         f = GitFile(foo, 'wb')
         f.abort()
         try:
@@ -203,11 +205,11 @@ class GitFileTests(TestCase):
             self.fail()
 
     def test_abort_close_removed(self):
-        foo = self.path('foo')
+        foo = self.path(b'foo')
         f = GitFile(foo, 'wb')
 
         f._file.close()
-        os.remove(foo+".lock")
+        os.remove(foo + b'.lock')
 
         f.abort()
         self.assertTrue(f._closed)

+ 22 - 28
dulwich/tests/test_grafts.py

@@ -23,7 +23,6 @@ import shutil
 
 from dulwich.errors import ObjectFormatException
 from dulwich.tests import TestCase
-from dulwich.tests.utils import skipIfPY3
 from dulwich.objects import (
     Tree,
     )
@@ -36,10 +35,9 @@ from dulwich.repo import (
 
 
 def makesha(digit):
-    return (str(digit) * 40)[:40]
+    return (str(digit).encode('ascii') * 40)[:40]
 
 
-@skipIfPY3
 class GraftParserTests(TestCase):
 
     def assertParse(self, expected, graftpoints):
@@ -53,7 +51,7 @@ class GraftParserTests(TestCase):
 
     def test_parents(self):
         self.assertParse({makesha(0): [makesha(1), makesha(2)]},
-                         [' '.join([makesha(0), makesha(1), makesha(2)])])
+                         [b' '.join([makesha(0), makesha(1), makesha(2)])])
 
     def test_multiple_hybrid(self):
         self.assertParse(
@@ -61,11 +59,10 @@ class GraftParserTests(TestCase):
              makesha(1): [makesha(2)],
              makesha(3): [makesha(4), makesha(5)]},
             [makesha(0),
-             ' '.join([makesha(1), makesha(2)]),
-             ' '.join([makesha(3), makesha(4), makesha(5)])])
+             b' '.join([makesha(1), makesha(2)]),
+             b' '.join([makesha(3), makesha(4), makesha(5)])])
 
 
-@skipIfPY3
 class GraftSerializerTests(TestCase):
 
     def assertSerialize(self, expected, graftpoints):
@@ -74,27 +71,26 @@ class GraftSerializerTests(TestCase):
             sorted(serialize_graftpoints(graftpoints)))
 
     def test_no_grafts(self):
-        self.assertSerialize('', {})
+        self.assertSerialize(b'', {})
 
     def test_no_parents(self):
         self.assertSerialize(makesha(0), {makesha(0): []})
 
     def test_parents(self):
-        self.assertSerialize(' '.join([makesha(0), makesha(1), makesha(2)]),
+        self.assertSerialize(b' '.join([makesha(0), makesha(1), makesha(2)]),
                              {makesha(0): [makesha(1), makesha(2)]})
 
     def test_multiple_hybrid(self):
         self.assertSerialize(
-            '\n'.join([
+            b'\n'.join([
                 makesha(0),
-                ' '.join([makesha(1), makesha(2)]),
-                ' '.join([makesha(3), makesha(4), makesha(5)])]),
+                b' '.join([makesha(1), makesha(2)]),
+                b' '.join([makesha(3), makesha(4), makesha(5)])]),
             {makesha(0): [],
              makesha(1): [makesha(2)],
              makesha(3): [makesha(4), makesha(5)]})
 
 
-@skipIfPY3
 class GraftsInRepositoryBase(object):
 
     def tearDown(self):
@@ -139,7 +135,6 @@ class GraftsInRepositoryBase(object):
             {self._shas[-1]: ['1']})
 
 
-@skipIfPY3
 class GraftsInRepoTests(GraftsInRepositoryBase, TestCase):
 
     def setUp(self):
@@ -151,8 +146,8 @@ class GraftsInRepoTests(GraftsInRepositoryBase, TestCase):
         self._shas = []
 
         commit_kwargs = {
-            'committer': 'Test Committer <test@nodomain.com>',
-            'author': 'Test Author <test@nodomain.com>',
+            'committer': b'Test Committer <test@nodomain.com>',
+            'author': b'Test Author <test@nodomain.com>',
             'commit_timestamp': 12395,
             'commit_timezone': 0,
             'author_timestamp': 12395,
@@ -160,15 +155,15 @@ class GraftsInRepoTests(GraftsInRepositoryBase, TestCase):
         }
 
         self._shas.append(r.do_commit(
-            'empty commit', **commit_kwargs))
+            b'empty commit', **commit_kwargs))
         self._shas.append(r.do_commit(
-            'empty commit', **commit_kwargs))
+            b'empty commit', **commit_kwargs))
         self._shas.append(r.do_commit(
-            'empty commit', **commit_kwargs))
+            b'empty commit', **commit_kwargs))
 
     def test_init_with_empty_info_grafts(self):
         r = self._repo
-        r._put_named_file(os.path.join('info', 'grafts'), '')
+        r._put_named_file(os.path.join(b'info', b'grafts'), b'')
 
         r = Repo(self._repo_dir)
         self.assertEqual({}, r._graftpoints)
@@ -176,14 +171,13 @@ class GraftsInRepoTests(GraftsInRepositoryBase, TestCase):
     def test_init_with_info_grafts(self):
         r = self._repo
         r._put_named_file(
-            os.path.join('info', 'grafts'),
-            "%s %s" % (self._shas[-1], self._shas[0]))
+            os.path.join(b'info', b'grafts'),
+            self._shas[-1] + b' ' + self._shas[0])
 
         r = Repo(self._repo_dir)
         self.assertEqual({self._shas[-1]: [self._shas[0]]}, r._graftpoints)
 
 
-@skipIfPY3
 class GraftsInMemoryRepoTests(GraftsInRepositoryBase, TestCase):
 
     def setUp(self):
@@ -195,8 +189,8 @@ class GraftsInMemoryRepoTests(GraftsInRepositoryBase, TestCase):
         tree = Tree()
 
         commit_kwargs = {
-            'committer': 'Test Committer <test@nodomain.com>',
-            'author': 'Test Author <test@nodomain.com>',
+            'committer': b'Test Committer <test@nodomain.com>',
+            'author': b'Test Author <test@nodomain.com>',
             'commit_timestamp': 12395,
             'commit_timezone': 0,
             'author_timestamp': 12395,
@@ -205,8 +199,8 @@ class GraftsInMemoryRepoTests(GraftsInRepositoryBase, TestCase):
         }
 
         self._shas.append(r.do_commit(
-            'empty commit', **commit_kwargs))
+            b'empty commit', **commit_kwargs))
         self._shas.append(r.do_commit(
-            'empty commit', **commit_kwargs))
+            b'empty commit', **commit_kwargs))
         self._shas.append(r.do_commit(
-            'empty commit', **commit_kwargs))
+            b'empty commit', **commit_kwargs))

+ 2 - 6
dulwich/tests/test_greenthreads.py

@@ -22,6 +22,7 @@
 import time
 
 from dulwich.tests import (
+    skipIf,
     TestCase,
     )
 from dulwich.object_store import (
@@ -35,11 +36,6 @@ from dulwich.objects import (
     parse_timezone,
     )
 
-try:
-    from unittest import skipIf
-except ImportError:
-    from unittest2 import skipIf
-
 try:
     import gevent
     gevent_support = True
@@ -70,7 +66,7 @@ def create_commit(marker=None):
 
 def init_store(store, count=1):
     ret = []
-    for i in xrange(0, count):
+    for i in range(0, count):
         objs = create_commit(marker=i)
         for obj in objs:
             ret.append(obj)

+ 30 - 20
dulwich/tests/test_hooks.py

@@ -41,28 +41,31 @@ class ShellHookTests(TestCase):
             self.skipTest('shell hook tests requires POSIX shell')
 
     def test_hook_pre_commit(self):
-        pre_commit_fail = b"""#!/bin/sh
+        pre_commit_fail = """#!/bin/sh
 exit 1
 """
 
-        pre_commit_success = b"""#!/bin/sh
+        pre_commit_success = """#!/bin/sh
 exit 0
 """
 
-        repo_dir = os.path.join(tempfile.mkdtemp())
-        os.mkdir(os.path.join(repo_dir, 'hooks'))
+        repo_dir = tempfile.mkdtemp()
+        if not isinstance(repo_dir, bytes):
+            repo_dir = repo_dir.encode(sys.getfilesystemencoding())
+
+        os.mkdir(os.path.join(repo_dir, b'hooks'))
         self.addCleanup(shutil.rmtree, repo_dir)
 
-        pre_commit = os.path.join(repo_dir, 'hooks', 'pre-commit')
+        pre_commit = os.path.join(repo_dir, b'hooks', b'pre-commit')
         hook = PreCommitShellHook(repo_dir)
 
-        with open(pre_commit, 'wb') as f:
+        with open(pre_commit, 'w') as f:
             f.write(pre_commit_fail)
         os.chmod(pre_commit, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
 
         self.assertRaises(errors.HookError, hook.execute)
 
-        with open(pre_commit, 'wb') as f:
+        with open(pre_commit, 'w') as f:
             f.write(pre_commit_success)
         os.chmod(pre_commit, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
 
@@ -70,28 +73,31 @@ exit 0
 
     def test_hook_commit_msg(self):
 
-        commit_msg_fail = b"""#!/bin/sh
+        commit_msg_fail = """#!/bin/sh
 exit 1
 """
 
-        commit_msg_success = b"""#!/bin/sh
+        commit_msg_success = """#!/bin/sh
 exit 0
 """
 
         repo_dir = os.path.join(tempfile.mkdtemp())
-        os.mkdir(os.path.join(repo_dir, 'hooks'))
+        if not isinstance(repo_dir, bytes):
+            repo_dir = repo_dir.encode(sys.getfilesystemencoding())
+
+        os.mkdir(os.path.join(repo_dir, b'hooks'))
         self.addCleanup(shutil.rmtree, repo_dir)
 
-        commit_msg = os.path.join(repo_dir, 'hooks', 'commit-msg')
+        commit_msg = os.path.join(repo_dir, b'hooks', b'commit-msg')
         hook = CommitMsgShellHook(repo_dir)
 
-        with open(commit_msg, 'wb') as f:
+        with open(commit_msg, 'w') as f:
             f.write(commit_msg_fail)
         os.chmod(commit_msg, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
 
         self.assertRaises(errors.HookError, hook.execute, b'failed commit')
 
-        with open(commit_msg, 'wb') as f:
+        with open(commit_msg, 'w') as f:
             f.write(commit_msg_success)
         os.chmod(commit_msg, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
 
@@ -100,27 +106,31 @@ exit 0
     def test_hook_post_commit(self):
 
         (fd, path) = tempfile.mkstemp()
-        post_commit_msg = b"""#!/bin/sh
-rm """ + path.encode(sys.getfilesystemencoding()) + b"\n"
 
-        post_commit_msg_fail = b"""#!/bin/sh
+        post_commit_msg = """#!/bin/sh
+rm """ + path + "\n"
+
+        post_commit_msg_fail = """#!/bin/sh
 exit 1
 """
 
         repo_dir = os.path.join(tempfile.mkdtemp())
-        os.mkdir(os.path.join(repo_dir, 'hooks'))
+        if not isinstance(repo_dir, bytes):
+            repo_dir = repo_dir.encode(sys.getfilesystemencoding())
+
+        os.mkdir(os.path.join(repo_dir, b'hooks'))
         self.addCleanup(shutil.rmtree, repo_dir)
 
-        post_commit = os.path.join(repo_dir, 'hooks', 'post-commit')
+        post_commit = os.path.join(repo_dir, b'hooks', b'post-commit')
         hook = PostCommitShellHook(repo_dir)
 
-        with open(post_commit, 'wb') as f:
+        with open(post_commit, 'w') as f:
             f.write(post_commit_msg_fail)
         os.chmod(post_commit, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
 
         self.assertRaises(errors.HookError, hook.execute)
 
-        with open(post_commit, 'wb') as f:
+        with open(post_commit, 'w') as f:
             f.write(post_commit_msg)
         os.chmod(post_commit, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
 

+ 65 - 77
dulwich/tests/test_index.py

@@ -50,9 +50,7 @@ from dulwich.objects import (
     )
 from dulwich.repo import Repo
 from dulwich.tests import TestCase
-from dulwich.tests.utils import skipIfPY3
 
-@skipIfPY3
 class IndexTestCase(TestCase):
 
     datadir = os.path.join(os.path.dirname(__file__), 'data/indexes')
@@ -61,20 +59,19 @@ class IndexTestCase(TestCase):
         return Index(os.path.join(self.datadir, name))
 
 
-@skipIfPY3
 class SimpleIndexTestCase(IndexTestCase):
 
     def test_len(self):
         self.assertEqual(1, len(self.get_simple_index("index")))
 
     def test_iter(self):
-        self.assertEqual(['bla'], list(self.get_simple_index("index")))
+        self.assertEqual([b'bla'], list(self.get_simple_index("index")))
 
     def test_getitem(self):
         self.assertEqual(((1230680220, 0), (1230680220, 0), 2050, 3761020,
                            33188, 1000, 1000, 0,
-                           'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391', 0),
-                          self.get_simple_index("index")["bla"])
+                           b'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391', 0),
+                          self.get_simple_index("index")[b"bla"])
 
     def test_empty(self):
         i = self.get_simple_index("notanindex")
@@ -86,11 +83,9 @@ class SimpleIndexTestCase(IndexTestCase):
         changes = list(i.changes_from_tree(MemoryObjectStore(), None))
         self.assertEqual(1, len(changes))
         (oldname, newname), (oldmode, newmode), (oldsha, newsha) = changes[0]
-        self.assertEqual('bla', newname)
-        self.assertEqual('e69de29bb2d1d6434b8b29ae775ad8c2e48c5391', newsha)
+        self.assertEqual(b'bla', newname)
+        self.assertEqual(b'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391', newsha)
 
-
-@skipIfPY3
 class SimpleIndexWriterTestCase(IndexTestCase):
 
     def setUp(self):
@@ -102,18 +97,17 @@ class SimpleIndexWriterTestCase(IndexTestCase):
         shutil.rmtree(self.tempdir)
 
     def test_simple_write(self):
-        entries = [('barbla', (1230680220, 0), (1230680220, 0), 2050, 3761020,
+        entries = [(b'barbla', (1230680220, 0), (1230680220, 0), 2050, 3761020,
                     33188, 1000, 1000, 0,
-                    'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391', 0)]
+                    b'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391', 0)]
         filename = os.path.join(self.tempdir, 'test-simple-write-index')
-        with open(filename, 'w+') as x:
+        with open(filename, 'wb+') as x:
             write_index(x, entries)
 
-        with open(filename, 'r') as x:
+        with open(filename, 'rb') as x:
             self.assertEqual(entries, list(read_index(x)))
 
 
-@skipIfPY3
 class ReadIndexDictTests(IndexTestCase):
 
     def setUp(self):
@@ -125,18 +119,17 @@ class ReadIndexDictTests(IndexTestCase):
         shutil.rmtree(self.tempdir)
 
     def test_simple_write(self):
-        entries = {'barbla': ((1230680220, 0), (1230680220, 0), 2050, 3761020,
+        entries = {b'barbla': ((1230680220, 0), (1230680220, 0), 2050, 3761020,
                     33188, 1000, 1000, 0,
-                    'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391', 0)}
+                    b'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391', 0)}
         filename = os.path.join(self.tempdir, 'test-simple-write-index')
-        with open(filename, 'w+') as x:
+        with open(filename, 'wb+') as x:
             write_index_dict(x, entries)
 
-        with open(filename, 'r') as x:
+        with open(filename, 'rb') as x:
             self.assertEqual(entries, read_index_dict(x))
 
 
-@skipIfPY3
 class CommitTreeTests(TestCase):
 
     def setUp(self):
@@ -145,30 +138,29 @@ class CommitTreeTests(TestCase):
 
     def test_single_blob(self):
         blob = Blob()
-        blob.data = "foo"
+        blob.data = b"foo"
         self.store.add_object(blob)
-        blobs = [("bla", blob.id, stat.S_IFREG)]
+        blobs = [(b"bla", blob.id, stat.S_IFREG)]
         rootid = commit_tree(self.store, blobs)
-        self.assertEqual(rootid, "1a1e80437220f9312e855c37ac4398b68e5c1d50")
-        self.assertEqual((stat.S_IFREG, blob.id), self.store[rootid]["bla"])
+        self.assertEqual(rootid, b"1a1e80437220f9312e855c37ac4398b68e5c1d50")
+        self.assertEqual((stat.S_IFREG, blob.id), self.store[rootid][b"bla"])
         self.assertEqual(set([rootid, blob.id]), set(self.store._data.keys()))
 
     def test_nested(self):
         blob = Blob()
-        blob.data = "foo"
+        blob.data = b"foo"
         self.store.add_object(blob)
-        blobs = [("bla/bar", blob.id, stat.S_IFREG)]
+        blobs = [(b"bla/bar", blob.id, stat.S_IFREG)]
         rootid = commit_tree(self.store, blobs)
-        self.assertEqual(rootid, "d92b959b216ad0d044671981196781b3258fa537")
-        dirid = self.store[rootid]["bla"][1]
-        self.assertEqual(dirid, "c1a1deb9788150829579a8b4efa6311e7b638650")
-        self.assertEqual((stat.S_IFDIR, dirid), self.store[rootid]["bla"])
-        self.assertEqual((stat.S_IFREG, blob.id), self.store[dirid]["bar"])
+        self.assertEqual(rootid, b"d92b959b216ad0d044671981196781b3258fa537")
+        dirid = self.store[rootid][b"bla"][1]
+        self.assertEqual(dirid, b"c1a1deb9788150829579a8b4efa6311e7b638650")
+        self.assertEqual((stat.S_IFDIR, dirid), self.store[rootid][b"bla"])
+        self.assertEqual((stat.S_IFREG, blob.id), self.store[dirid][b"bar"])
         self.assertEqual(set([rootid, dirid, blob.id]),
                           set(self.store._data.keys()))
 
 
-@skipIfPY3
 class CleanupModeTests(TestCase):
 
     def test_file(self):
@@ -187,7 +179,6 @@ class CleanupModeTests(TestCase):
         self.assertEqual(0o160000, cleanup_mode(0o160744))
 
 
-@skipIfPY3
 class WriteCacheTimeTests(TestCase):
 
     def test_write_string(self):
@@ -210,7 +201,6 @@ class WriteCacheTimeTests(TestCase):
         self.assertEqual(struct.pack(">LL", 434343, 21), f.getvalue())
 
 
-@skipIfPY3
 class IndexEntryFromStatTests(TestCase):
 
     def test_simple(self):
@@ -249,7 +239,6 @@ class IndexEntryFromStatTests(TestCase):
             0))
 
 
-@skipIfPY3
 class BuildIndexTests(TestCase):
 
     def assertReasonableIndexEntry(self, index_entry, mode, filesize, sha):
@@ -291,12 +280,12 @@ class BuildIndexTests(TestCase):
         self.addCleanup(shutil.rmtree, repo_dir)
 
         # Populate repo
-        filea = Blob.from_string('file a')
-        filee = Blob.from_string('d')
+        filea = Blob.from_string(b'file a')
+        filee = Blob.from_string(b'd')
 
         tree = Tree()
-        tree['.git/a'] = (stat.S_IFREG | 0o644, filea.id)
-        tree['c/e'] = (stat.S_IFREG | 0o644, filee.id)
+        tree[b'.git/a'] = (stat.S_IFREG | 0o644, filea.id)
+        tree[b'c/e'] = (stat.S_IFREG | 0o644, filee.id)
 
         repo.object_store.add_objects([(o, None)
             for o in [filea, filee, tree]])
@@ -315,9 +304,9 @@ class BuildIndexTests(TestCase):
         # filee
         epath = os.path.join(repo.path, 'c', 'e')
         self.assertTrue(os.path.exists(epath))
-        self.assertReasonableIndexEntry(index['c/e'],
+        self.assertReasonableIndexEntry(index[b'c/e'],
             stat.S_IFREG | 0o644, 1, filee.id)
-        self.assertFileContents(epath, 'd')
+        self.assertFileContents(epath, b'd')
 
     def test_nonempty(self):
         if os.name != 'posix':
@@ -328,16 +317,16 @@ class BuildIndexTests(TestCase):
         self.addCleanup(shutil.rmtree, repo_dir)
 
         # Populate repo
-        filea = Blob.from_string('file a')
-        fileb = Blob.from_string('file b')
-        filed = Blob.from_string('file d')
-        filee = Blob.from_string('d')
+        filea = Blob.from_string(b'file a')
+        fileb = Blob.from_string(b'file b')
+        filed = Blob.from_string(b'file d')
+        filee = Blob.from_string(b'd')
 
         tree = Tree()
-        tree['a'] = (stat.S_IFREG | 0o644, filea.id)
-        tree['b'] = (stat.S_IFREG | 0o644, fileb.id)
-        tree['c/d'] = (stat.S_IFREG | 0o644, filed.id)
-        tree['c/e'] = (stat.S_IFLNK, filee.id)  # symlink
+        tree[b'a'] = (stat.S_IFREG | 0o644, filea.id)
+        tree[b'b'] = (stat.S_IFREG | 0o644, fileb.id)
+        tree[b'c/d'] = (stat.S_IFREG | 0o644, filed.id)
+        tree[b'c/e'] = (stat.S_IFLNK, filee.id)  # symlink
 
         repo.object_store.add_objects([(o, None)
             for o in [filea, fileb, filed, filee, tree]])
@@ -352,28 +341,28 @@ class BuildIndexTests(TestCase):
         # filea
         apath = os.path.join(repo.path, 'a')
         self.assertTrue(os.path.exists(apath))
-        self.assertReasonableIndexEntry(index['a'],
+        self.assertReasonableIndexEntry(index[b'a'],
             stat.S_IFREG | 0o644, 6, filea.id)
-        self.assertFileContents(apath, 'file a')
+        self.assertFileContents(apath, b'file a')
 
         # fileb
         bpath = os.path.join(repo.path, 'b')
         self.assertTrue(os.path.exists(bpath))
-        self.assertReasonableIndexEntry(index['b'],
+        self.assertReasonableIndexEntry(index[b'b'],
             stat.S_IFREG | 0o644, 6, fileb.id)
-        self.assertFileContents(bpath, 'file b')
+        self.assertFileContents(bpath, b'file b')
 
         # filed
         dpath = os.path.join(repo.path, 'c', 'd')
         self.assertTrue(os.path.exists(dpath))
-        self.assertReasonableIndexEntry(index['c/d'],
+        self.assertReasonableIndexEntry(index[b'c/d'],
             stat.S_IFREG | 0o644, 6, filed.id)
-        self.assertFileContents(dpath, 'file d')
+        self.assertFileContents(dpath, b'file d')
 
         # symlink to d
         epath = os.path.join(repo.path, 'c', 'e')
         self.assertTrue(os.path.exists(epath))
-        self.assertReasonableIndexEntry(index['c/e'],
+        self.assertReasonableIndexEntry(index[b'c/e'],
             stat.S_IFLNK, 1, filee.id)
         self.assertFileContents(epath, 'd', symlink=True)
 
@@ -384,7 +373,6 @@ class BuildIndexTests(TestCase):
             sorted(os.listdir(os.path.join(repo.path, 'c'))))
 
 
-@skipIfPY3
 class GetUnstagedChangesTests(TestCase):
 
     def test_get_unstaged_changes(self):
@@ -396,41 +384,41 @@ class GetUnstagedChangesTests(TestCase):
 
         # Commit a dummy file then modify it
         foo1_fullpath = os.path.join(repo_dir, 'foo1')
-        with open(foo1_fullpath, 'w') as f:
-            f.write('origstuff')
+        with open(foo1_fullpath, 'wb') as f:
+            f.write(b'origstuff')
 
         foo2_fullpath = os.path.join(repo_dir, 'foo2')
-        with open(foo2_fullpath, 'w') as f:
-            f.write('origstuff')
+        with open(foo2_fullpath, 'wb') as f:
+            f.write(b'origstuff')
 
         repo.stage(['foo1', 'foo2'])
-        repo.do_commit('test status', author='', committer='')
+        repo.do_commit(b'test status', author=b'', committer=b'')
 
-        with open(foo1_fullpath, 'w') as f:
-            f.write('newstuff')
+        with open(foo1_fullpath, 'wb') as f:
+            f.write(b'newstuff')
 
         # modify access and modify time of path
         os.utime(foo1_fullpath, (0, 0))
 
         changes = get_unstaged_changes(repo.open_index(), repo_dir)
 
-        self.assertEqual(list(changes), ['foo1'])
+        self.assertEqual(list(changes), [b'foo1'])
 
 
 class TestValidatePathElement(TestCase):
 
     def test_default(self):
-        self.assertTrue(validate_path_element_default("bla"))
-        self.assertTrue(validate_path_element_default(".bla"))
-        self.assertFalse(validate_path_element_default(".git"))
-        self.assertFalse(validate_path_element_default(".giT"))
-        self.assertFalse(validate_path_element_default(".."))
-        self.assertTrue(validate_path_element_default("git~1"))
+        self.assertTrue(validate_path_element_default(b"bla"))
+        self.assertTrue(validate_path_element_default(b".bla"))
+        self.assertFalse(validate_path_element_default(b".git"))
+        self.assertFalse(validate_path_element_default(b".giT"))
+        self.assertFalse(validate_path_element_default(b".."))
+        self.assertTrue(validate_path_element_default(b"git~1"))
 
     def test_ntfs(self):
-        self.assertTrue(validate_path_element_ntfs("bla"))
-        self.assertTrue(validate_path_element_ntfs(".bla"))
-        self.assertFalse(validate_path_element_ntfs(".git"))
-        self.assertFalse(validate_path_element_ntfs(".giT"))
-        self.assertFalse(validate_path_element_ntfs(".."))
-        self.assertFalse(validate_path_element_ntfs("git~1"))
+        self.assertTrue(validate_path_element_ntfs(b"bla"))
+        self.assertTrue(validate_path_element_ntfs(b".bla"))
+        self.assertFalse(validate_path_element_ntfs(b".git"))
+        self.assertFalse(validate_path_element_ntfs(b".giT"))
+        self.assertFalse(validate_path_element_ntfs(b".."))
+        self.assertFalse(validate_path_element_ntfs(b"git~1"))

+ 26 - 29
dulwich/tests/test_missing_obj_finder.py

@@ -27,11 +27,9 @@ from dulwich.tests.utils import (
     make_object,
     make_tag,
     build_commit_graph,
-    skipIfPY3,
     )
 
 
-@skipIfPY3
 class MissingObjectFinderTest(TestCase):
 
     def setUp(self):
@@ -52,21 +50,20 @@ class MissingObjectFinderTest(TestCase):
             "some objects are not reported as missing: %s" % (expected, ))
 
 
-@skipIfPY3
 class MOFLinearRepoTest(MissingObjectFinderTest):
 
     def setUp(self):
         super(MOFLinearRepoTest, self).setUp()
-        f1_1 = make_object(Blob, data='f1') # present in 1, removed in 3
-        f2_1 = make_object(Blob, data='f2') # present in all revisions, changed in 2 and 3
-        f2_2 = make_object(Blob, data='f2-changed')
-        f2_3 = make_object(Blob, data='f2-changed-again')
-        f3_2 = make_object(Blob, data='f3') # added in 2, left unmodified in 3
+        f1_1 = make_object(Blob, data=b'f1') # present in 1, removed in 3
+        f2_1 = make_object(Blob, data=b'f2') # present in all revisions, changed in 2 and 3
+        f2_2 = make_object(Blob, data=b'f2-changed')
+        f2_3 = make_object(Blob, data=b'f2-changed-again')
+        f3_2 = make_object(Blob, data=b'f3') # added in 2, left unmodified in 3
 
         commit_spec = [[1], [2, 1], [3, 2]]
-        trees = {1: [('f1', f1_1), ('f2', f2_1)],
-                2: [('f1', f1_1), ('f2', f2_2), ('f3', f3_2)],
-                3: [('f2', f2_3), ('f3', f3_2)] }
+        trees = {1: [(b'f1', f1_1), (b'f2', f2_1)],
+                 2: [(b'f1', f1_1), (b'f2', f2_2), (b'f3', f3_2)],
+                 3: [(b'f2', f2_3), (b'f3', f3_2)] }
         # commit 1: f1 and f2
         # commit 2: f3 added, f2 changed. Missing shall report commit id and a
         # tree referenced by commit
@@ -111,7 +108,6 @@ class MOFLinearRepoTest(MissingObjectFinderTest):
         self.assertMissingMatch([self.cmt(3).id], [self.cmt(3).id], [])
 
 
-@skipIfPY3
 class MOFMergeForkRepoTest(MissingObjectFinderTest):
     # 1 --- 2 --- 4 --- 6 --- 7
     #          \        /
@@ -121,24 +117,24 @@ class MOFMergeForkRepoTest(MissingObjectFinderTest):
 
     def setUp(self):
         super(MOFMergeForkRepoTest, self).setUp()
-        f1_1 = make_object(Blob, data='f1')
-        f1_2 = make_object(Blob, data='f1-2')
-        f1_4 = make_object(Blob, data='f1-4')
-        f1_7 = make_object(Blob, data='f1-2') # same data as in rev 2
-        f2_1 = make_object(Blob, data='f2')
-        f2_3 = make_object(Blob, data='f2-3')
-        f3_3 = make_object(Blob, data='f3')
-        f3_5 = make_object(Blob, data='f3-5')
+        f1_1 = make_object(Blob, data=b'f1')
+        f1_2 = make_object(Blob, data=b'f1-2')
+        f1_4 = make_object(Blob, data=b'f1-4')
+        f1_7 = make_object(Blob, data=b'f1-2') # same data as in rev 2
+        f2_1 = make_object(Blob, data=b'f2')
+        f2_3 = make_object(Blob, data=b'f2-3')
+        f3_3 = make_object(Blob, data=b'f3')
+        f3_5 = make_object(Blob, data=b'f3-5')
         commit_spec = [[1], [2, 1], [3, 2], [4, 2], [5, 3], [6, 3, 4], [7, 6]]
-        trees = {1: [('f1', f1_1), ('f2', f2_1)],
-                2: [('f1', f1_2), ('f2', f2_1)], # f1 changed
+        trees = {1: [(b'f1', f1_1), (b'f2', f2_1)],
+                2: [(b'f1', f1_2), (b'f2', f2_1)], # f1 changed
                 # f3 added, f2 changed
-                3: [('f1', f1_2), ('f2', f2_3), ('f3', f3_3)],
-                4: [('f1', f1_4), ('f2', f2_1)],  # f1 changed
-                5: [('f1', f1_2), ('f3', f3_5)], # f2 removed, f3 changed
-                6: [('f1', f1_4), ('f2', f2_3), ('f3', f3_3)], # merged 3 and 4
+                3: [(b'f1', f1_2), (b'f2', f2_3), (b'f3', f3_3)],
+                4: [(b'f1', f1_4), (b'f2', f2_1)],  # f1 changed
+                5: [(b'f1', f1_2), (b'f3', f3_5)], # f2 removed, f3 changed
+                6: [(b'f1', f1_4), (b'f2', f2_3), (b'f3', f3_3)], # merged 3 and 4
                 # f1 changed to match rev2. f3 removed
-                7: [('f1', f1_7), ('f2', f2_3)]}
+                7: [(b'f1', f1_7), (b'f2', f2_3)]}
         self.commits = build_commit_graph(self.store, commit_spec, trees)
 
         self.f1_2_id = f1_2.id
@@ -198,11 +194,12 @@ class MOFMergeForkRepoTest(MissingObjectFinderTest):
 
 
 class MOFTagsTest(MissingObjectFinderTest):
+
     def setUp(self):
         super(MOFTagsTest, self).setUp()
-        f1_1 = make_object(Blob, data='f1')
+        f1_1 = make_object(Blob, data=b'f1')
         commit_spec = [[1]]
-        trees = {1: [('f1', f1_1)]}
+        trees = {1: [(b'f1', f1_1)]}
         self.commits = build_commit_graph(self.store, commit_spec, trees)
 
         self._normal_tag = make_tag(self.cmt(1))

+ 112 - 111
dulwich/tests/test_object_store.py

@@ -22,6 +22,7 @@
 from io import BytesIO
 import os
 import shutil
+import sys
 import tempfile
 
 from dulwich.index import (
@@ -53,7 +54,6 @@ from dulwich.tests.utils import (
     make_object,
     make_tag,
     build_pack,
-    skipIfPY3,
     )
 
 
@@ -63,21 +63,21 @@ testobject = make_object(Blob, data=b"yummy data")
 class ObjectStoreTests(object):
 
     def test_determine_wants_all(self):
-        self.assertEqual(["1" * 40],
-            self.store.determine_wants_all({"refs/heads/foo": "1" * 40}))
+        self.assertEqual([b"1" * 40],
+            self.store.determine_wants_all({b"refs/heads/foo": b"1" * 40}))
 
     def test_determine_wants_all_zero(self):
         self.assertEqual([],
-            self.store.determine_wants_all({"refs/heads/foo": "0" * 40}))
+            self.store.determine_wants_all({b"refs/heads/foo": b"0" * 40}))
 
     def test_iter(self):
         self.assertEqual([], list(self.store))
 
     def test_get_nonexistant(self):
-        self.assertRaises(KeyError, lambda: self.store["a" * 40])
+        self.assertRaises(KeyError, lambda: self.store[b"a" * 40])
 
     def test_contains_nonexistant(self):
-        self.assertFalse(("a" * 40) in self.store)
+        self.assertFalse((b"a" * 40) in self.store)
 
     def test_add_objects_empty(self):
         self.store.add_objects([])
@@ -103,66 +103,66 @@ class ObjectStoreTests(object):
         self.assertEqual(r, testobject)
 
     def test_tree_changes(self):
-        blob_a1 = make_object(Blob, data='a1')
-        blob_a2 = make_object(Blob, data='a2')
-        blob_b = make_object(Blob, data='b')
+        blob_a1 = make_object(Blob, data=b'a1')
+        blob_a2 = make_object(Blob, data=b'a2')
+        blob_b = make_object(Blob, data=b'b')
         for blob in [blob_a1, blob_a2, blob_b]:
             self.store.add_object(blob)
 
-        blobs_1 = [('a', blob_a1.id, 0o100644), ('b', blob_b.id, 0o100644)]
+        blobs_1 = [(b'a', blob_a1.id, 0o100644), (b'b', blob_b.id, 0o100644)]
         tree1_id = commit_tree(self.store, blobs_1)
-        blobs_2 = [('a', blob_a2.id, 0o100644), ('b', blob_b.id, 0o100644)]
+        blobs_2 = [(b'a', blob_a2.id, 0o100644), (b'b', blob_b.id, 0o100644)]
         tree2_id = commit_tree(self.store, blobs_2)
-        change_a = (('a', 'a'), (0o100644, 0o100644), (blob_a1.id, blob_a2.id))
+        change_a = ((b'a', b'a'), (0o100644, 0o100644), (blob_a1.id, blob_a2.id))
         self.assertEqual([change_a],
                           list(self.store.tree_changes(tree1_id, tree2_id)))
         self.assertEqual(
-          [change_a, (('b', 'b'), (0o100644, 0o100644), (blob_b.id, blob_b.id))],
-          list(self.store.tree_changes(tree1_id, tree2_id,
-                                       want_unchanged=True)))
+            [change_a, ((b'b', b'b'), (0o100644, 0o100644), (blob_b.id, blob_b.id))],
+            list(self.store.tree_changes(tree1_id, tree2_id,
+                                         want_unchanged=True)))
 
     def test_iter_tree_contents(self):
-        blob_a = make_object(Blob, data='a')
-        blob_b = make_object(Blob, data='b')
-        blob_c = make_object(Blob, data='c')
+        blob_a = make_object(Blob, data=b'a')
+        blob_b = make_object(Blob, data=b'b')
+        blob_c = make_object(Blob, data=b'c')
         for blob in [blob_a, blob_b, blob_c]:
             self.store.add_object(blob)
 
         blobs = [
-          ('a', blob_a.id, 0o100644),
-          ('ad/b', blob_b.id, 0o100644),
-          ('ad/bd/c', blob_c.id, 0o100755),
-          ('ad/c', blob_c.id, 0o100644),
-          ('c', blob_c.id, 0o100644),
-          ]
+            (b'a', blob_a.id, 0o100644),
+            (b'ad/b', blob_b.id, 0o100644),
+            (b'ad/bd/c', blob_c.id, 0o100755),
+            (b'ad/c', blob_c.id, 0o100644),
+            (b'c', blob_c.id, 0o100644),
+        ]
         tree_id = commit_tree(self.store, blobs)
         self.assertEqual([TreeEntry(p, m, h) for (p, h, m) in blobs],
                           list(self.store.iter_tree_contents(tree_id)))
 
     def test_iter_tree_contents_include_trees(self):
-        blob_a = make_object(Blob, data='a')
-        blob_b = make_object(Blob, data='b')
-        blob_c = make_object(Blob, data='c')
+        blob_a = make_object(Blob, data=b'a')
+        blob_b = make_object(Blob, data=b'b')
+        blob_c = make_object(Blob, data=b'c')
         for blob in [blob_a, blob_b, blob_c]:
             self.store.add_object(blob)
 
         blobs = [
-          ('a', blob_a.id, 0o100644),
-          ('ad/b', blob_b.id, 0o100644),
-          ('ad/bd/c', blob_c.id, 0o100755),
+          (b'a', blob_a.id, 0o100644),
+          (b'ad/b', blob_b.id, 0o100644),
+          (b'ad/bd/c', blob_c.id, 0o100755),
           ]
         tree_id = commit_tree(self.store, blobs)
         tree = self.store[tree_id]
-        tree_ad = self.store[tree['ad'][1]]
-        tree_bd = self.store[tree_ad['bd'][1]]
+        tree_ad = self.store[tree[b'ad'][1]]
+        tree_bd = self.store[tree_ad[b'bd'][1]]
 
         expected = [
-          TreeEntry('', 0o040000, tree_id),
-          TreeEntry('a', 0o100644, blob_a.id),
-          TreeEntry('ad', 0o040000, tree_ad.id),
-          TreeEntry('ad/b', 0o100644, blob_b.id),
-          TreeEntry('ad/bd', 0o040000, tree_bd.id),
-          TreeEntry('ad/bd/c', 0o100755, blob_c.id),
+          TreeEntry(b'', 0o040000, tree_id),
+          TreeEntry(b'a', 0o100644, blob_a.id),
+          TreeEntry(b'ad', 0o040000, tree_ad.id),
+          TreeEntry(b'ad/b', 0o100644, blob_b.id),
+          TreeEntry(b'ad/bd', 0o040000, tree_bd.id),
+          TreeEntry(b'ad/bd/c', 0o100755, blob_c.id),
           ]
         actual = self.store.iter_tree_contents(tree_id, include_trees=True)
         self.assertEqual(expected, list(actual))
@@ -174,15 +174,15 @@ class ObjectStoreTests(object):
 
     def test_peel_sha(self):
         self.store.add_object(testobject)
-        tag1 = self.make_tag('1', testobject)
-        tag2 = self.make_tag('2', testobject)
-        tag3 = self.make_tag('3', testobject)
+        tag1 = self.make_tag(b'1', testobject)
+        tag2 = self.make_tag(b'2', testobject)
+        tag3 = self.make_tag(b'3', testobject)
         for obj in [testobject, tag1, tag2, tag3]:
             self.assertEqual(testobject, self.store.peel_sha(obj.id))
 
     def test_get_raw(self):
         self.store.add_object(testobject)
-        self.assertEqual((Blob.type_num, 'yummy data'),
+        self.assertEqual((Blob.type_num, b'yummy data'),
                          self.store.get_raw(testobject.id))
 
     def test_close(self):
@@ -191,7 +191,6 @@ class ObjectStoreTests(object):
         self.store.close()
 
 
-@skipIfPY3
 class MemoryObjectStoreTests(ObjectStoreTests, TestCase):
 
     def setUp(self):
@@ -202,7 +201,7 @@ class MemoryObjectStoreTests(ObjectStoreTests, TestCase):
         o = MemoryObjectStore()
         f, commit, abort = o.add_pack()
         try:
-            b = make_object(Blob, data="more yummy data")
+            b = make_object(Blob, data=b"more yummy data")
             write_pack_objects(f, [(b, None)])
         except:
             abort()
@@ -217,16 +216,16 @@ class MemoryObjectStoreTests(ObjectStoreTests, TestCase):
 
     def test_add_thin_pack(self):
         o = MemoryObjectStore()
-        blob = make_object(Blob, data='yummy data')
+        blob = make_object(Blob, data=b'yummy data')
         o.add_object(blob)
 
         f = BytesIO()
         entries = build_pack(f, [
-          (REF_DELTA, (blob.id, 'more yummy data')),
-          ], store=o)
+            (REF_DELTA, (blob.id, b'more yummy data')),
+            ], store=o)
         o.add_thin_pack(f.read, None)
         packed_blob_sha = sha_to_hex(entries[0][3])
-        self.assertEqual((Blob.type_num, 'more yummy data'),
+        self.assertEqual((Blob.type_num, b'more yummy data'),
                          o.get_raw(packed_blob_sha))
 
 
@@ -239,7 +238,6 @@ class MemoryObjectStoreTests(ObjectStoreTests, TestCase):
         o.add_thin_pack(f.read, None)
 
 
-@skipIfPY3
 class PackBasedObjectStoreTests(ObjectStoreTests):
 
     def tearDown(self):
@@ -247,25 +245,26 @@ class PackBasedObjectStoreTests(ObjectStoreTests):
             pack.close()
 
     def test_empty_packs(self):
-        self.assertEqual([], self.store.packs)
+        self.assertEqual([], list(self.store.packs))
 
     def test_pack_loose_objects(self):
-        b1 = make_object(Blob, data="yummy data")
+        b1 = make_object(Blob, data=b"yummy data")
         self.store.add_object(b1)
-        b2 = make_object(Blob, data="more yummy data")
+        b2 = make_object(Blob, data=b"more yummy data")
         self.store.add_object(b2)
-        self.assertEqual([], self.store.packs)
+        self.assertEqual([], list(self.store.packs))
         self.assertEqual(2, self.store.pack_loose_objects())
-        self.assertNotEqual([], self.store.packs)
+        self.assertNotEqual([], list(self.store.packs))
         self.assertEqual(0, self.store.pack_loose_objects())
 
 
-@skipIfPY3
 class DiskObjectStoreTests(PackBasedObjectStoreTests, TestCase):
 
     def setUp(self):
         TestCase.setUp(self)
         self.store_dir = tempfile.mkdtemp()
+        if not isinstance(self.store_dir, bytes):
+            self.store_dir = self.store_dir.encode(sys.getfilesystemencoding())
         self.addCleanup(shutil.rmtree, self.store_dir)
         self.store = DiskObjectStore.init(self.store_dir)
 
@@ -275,9 +274,11 @@ class DiskObjectStoreTests(PackBasedObjectStoreTests, TestCase):
 
     def test_alternates(self):
         alternate_dir = tempfile.mkdtemp()
+        if not isinstance(alternate_dir, bytes):
+            alternate_dir = alternate_dir.encode(sys.getfilesystemencoding())
         self.addCleanup(shutil.rmtree, alternate_dir)
         alternate_store = DiskObjectStore(alternate_dir)
-        b2 = make_object(Blob, data="yummy data")
+        b2 = make_object(Blob, data=b"yummy data")
         alternate_store.add_object(b2)
         store = DiskObjectStore(self.store_dir)
         self.assertRaises(KeyError, store.__getitem__, b2.id)
@@ -287,19 +288,21 @@ class DiskObjectStoreTests(PackBasedObjectStoreTests, TestCase):
 
     def test_add_alternate_path(self):
         store = DiskObjectStore(self.store_dir)
-        self.assertEqual([], store._read_alternate_paths())
-        store.add_alternate_path("/foo/path")
-        self.assertEqual(["/foo/path"], store._read_alternate_paths())
-        store.add_alternate_path("/bar/path")
+        self.assertEqual([], list(store._read_alternate_paths()))
+        store.add_alternate_path(b'/foo/path')
+        self.assertEqual([b'/foo/path'], list(store._read_alternate_paths()))
+        store.add_alternate_path(b'/bar/path')
         self.assertEqual(
-            ["/foo/path", "/bar/path"],
-            store._read_alternate_paths())
+            [b'/foo/path', b'/bar/path'],
+            list(store._read_alternate_paths()))
 
     def test_rel_alternative_path(self):
         alternate_dir = tempfile.mkdtemp()
+        if not isinstance(alternate_dir, bytes):
+            alternate_dir = alternate_dir.encode(sys.getfilesystemencoding())
         self.addCleanup(shutil.rmtree, alternate_dir)
         alternate_store = DiskObjectStore(alternate_dir)
-        b2 = make_object(Blob, data="yummy data")
+        b2 = make_object(Blob, data=b"yummy data")
         alternate_store.add_object(b2)
         store = DiskObjectStore(self.store_dir)
         self.assertRaises(KeyError, store.__getitem__, b2.id)
@@ -310,13 +313,13 @@ class DiskObjectStoreTests(PackBasedObjectStoreTests, TestCase):
 
     def test_pack_dir(self):
         o = DiskObjectStore(self.store_dir)
-        self.assertEqual(os.path.join(self.store_dir, "pack"), o.pack_dir)
+        self.assertEqual(os.path.join(self.store_dir, b'pack'), o.pack_dir)
 
     def test_add_pack(self):
         o = DiskObjectStore(self.store_dir)
         f, commit, abort = o.add_pack()
         try:
-            b = make_object(Blob, data="more yummy data")
+            b = make_object(Blob, data=b"more yummy data")
             write_pack_objects(f, [(b, None)])
         except:
             abort()
@@ -327,12 +330,12 @@ class DiskObjectStoreTests(PackBasedObjectStoreTests, TestCase):
     def test_add_thin_pack(self):
         o = DiskObjectStore(self.store_dir)
         try:
-            blob = make_object(Blob, data='yummy data')
+            blob = make_object(Blob, data=b'yummy data')
             o.add_object(blob)
 
             f = BytesIO()
             entries = build_pack(f, [
-              (REF_DELTA, (blob.id, 'more yummy data')),
+              (REF_DELTA, (blob.id, b'more yummy data')),
               ], store=o)
 
             with o.add_thin_pack(f.read, None) as pack:
@@ -341,7 +344,7 @@ class DiskObjectStoreTests(PackBasedObjectStoreTests, TestCase):
                 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'),
+                self.assertEqual((Blob.type_num, b'more yummy data'),
                                  o.get_raw(packed_blob_sha))
         finally:
             o.close()
@@ -355,24 +358,23 @@ class DiskObjectStoreTests(PackBasedObjectStoreTests, TestCase):
         o.add_thin_pack(f.read, None)
 
 
-@skipIfPY3
 class TreeLookupPathTests(TestCase):
 
     def setUp(self):
         TestCase.setUp(self)
         self.store = MemoryObjectStore()
-        blob_a = make_object(Blob, data='a')
-        blob_b = make_object(Blob, data='b')
-        blob_c = make_object(Blob, data='c')
+        blob_a = make_object(Blob, data=b'a')
+        blob_b = make_object(Blob, data=b'b')
+        blob_c = make_object(Blob, data=b'c')
         for blob in [blob_a, blob_b, blob_c]:
             self.store.add_object(blob)
 
         blobs = [
-          ('a', blob_a.id, 0o100644),
-          ('ad/b', blob_b.id, 0o100644),
-          ('ad/bd/c', blob_c.id, 0o100755),
-          ('ad/c', blob_c.id, 0o100644),
-          ('c', blob_c.id, 0o100644),
+          (b'a', blob_a.id, 0o100644),
+          (b'ad/b', blob_b.id, 0o100644),
+          (b'ad/bd/c', blob_c.id, 0o100755),
+          (b'ad/c', blob_c.id, 0o100644),
+          (b'c', blob_c.id, 0o100644),
           ]
         self.tree_id = commit_tree(self.store, blobs)
 
@@ -380,24 +382,23 @@ class TreeLookupPathTests(TestCase):
         return self.store[sha]
 
     def test_lookup_blob(self):
-        o_id = tree_lookup_path(self.get_object, self.tree_id, 'a')[1]
+        o_id = tree_lookup_path(self.get_object, self.tree_id, b'a')[1]
         self.assertTrue(isinstance(self.store[o_id], Blob))
 
     def test_lookup_tree(self):
-        o_id = tree_lookup_path(self.get_object, self.tree_id, 'ad')[1]
+        o_id = tree_lookup_path(self.get_object, self.tree_id, b'ad')[1]
         self.assertTrue(isinstance(self.store[o_id], Tree))
-        o_id = tree_lookup_path(self.get_object, self.tree_id, 'ad/bd')[1]
+        o_id = tree_lookup_path(self.get_object, self.tree_id, b'ad/bd')[1]
         self.assertTrue(isinstance(self.store[o_id], Tree))
-        o_id = tree_lookup_path(self.get_object, self.tree_id, 'ad/bd/')[1]
+        o_id = tree_lookup_path(self.get_object, self.tree_id, b'ad/bd/')[1]
         self.assertTrue(isinstance(self.store[o_id], Tree))
 
     def test_lookup_nonexistent(self):
-        self.assertRaises(KeyError, tree_lookup_path, self.get_object, self.tree_id, 'j')
+        self.assertRaises(KeyError, tree_lookup_path, self.get_object, self.tree_id, b'j')
 
     def test_lookup_not_tree(self):
-        self.assertRaises(NotTreeError, tree_lookup_path, self.get_object, self.tree_id, 'ad/b/j')
+        self.assertRaises(NotTreeError, tree_lookup_path, self.get_object, self.tree_id, b'ad/b/j')
 
-@skipIfPY3
 class ObjectStoreGraphWalkerTests(TestCase):
 
     def get_walker(self, heads, parent_map):
@@ -413,30 +414,30 @@ class ObjectStoreGraphWalkerTests(TestCase):
     def test_empty(self):
         gw = self.get_walker([], {})
         self.assertIs(None, next(gw))
-        gw.ack("a" * 40)
+        gw.ack(b"a" * 40)
         self.assertIs(None, next(gw))
 
     def test_descends(self):
-        gw = self.get_walker(["a"], {"a": ["b"], "b": []})
-        self.assertEqual("a" * 40, next(gw))
-        self.assertEqual("b" * 40, next(gw))
+        gw = self.get_walker([b"a"], {b"a": [b"b"], b"b": []})
+        self.assertEqual(b"a" * 40, next(gw))
+        self.assertEqual(b"b" * 40, next(gw))
 
     def test_present(self):
-        gw = self.get_walker(["a"], {"a": ["b"], "b": []})
-        gw.ack("a" * 40)
+        gw = self.get_walker([b"a"], {b"a": [b"b"], b"b": []})
+        gw.ack(b"a" * 40)
         self.assertIs(None, next(gw))
 
     def test_parent_present(self):
-        gw = self.get_walker(["a"], {"a": ["b"], "b": []})
-        self.assertEqual("a" * 40, next(gw))
-        gw.ack("a" * 40)
+        gw = self.get_walker([b"a"], {b"a": [b"b"], b"b": []})
+        self.assertEqual(b"a" * 40, next(gw))
+        gw.ack(b"a" * 40)
         self.assertIs(None, next(gw))
 
     def test_child_ack_later(self):
-        gw = self.get_walker(["a"], {"a": ["b"], "b": ["c"], "c": []})
-        self.assertEqual("a" * 40, next(gw))
-        self.assertEqual("b" * 40, next(gw))
-        gw.ack("a" * 40)
+        gw = self.get_walker([b"a"], {b"a": [b"b"], b"b": [b"c"], b"c": []})
+        self.assertEqual(b"a" * 40, next(gw))
+        self.assertEqual(b"b" * 40, next(gw))
+        gw.ack(b"a" * 40)
         self.assertIs(None, next(gw))
 
     def test_only_once(self):
@@ -445,12 +446,12 @@ class ObjectStoreGraphWalkerTests(TestCase):
         # c  d
         # \ /
         #  e
-        gw = self.get_walker(["a", "b"], {
-                "a": ["c"],
-                "b": ["d"],
-                "c": ["e"],
-                "d": ["e"],
-                "e": [],
+        gw = self.get_walker([b"a", b"b"], {
+                b"a": [b"c"],
+                b"b": [b"d"],
+                b"c": [b"e"],
+                b"d": [b"e"],
+                b"e": [],
                 })
         walk = []
         acked = False
@@ -458,18 +459,18 @@ class ObjectStoreGraphWalkerTests(TestCase):
         walk.append(next(gw))
         # A branch (a, c) or (b, d) may be done after 2 steps or 3 depending on
         # the order walked: 3-step walks include (a, b, c) and (b, a, d), etc.
-        if walk == ["a" * 40, "c" * 40] or walk == ["b" * 40, "d" * 40]:
+        if walk == [b"a" * 40, b"c" * 40] or walk == [b"b" * 40, b"d" * 40]:
           gw.ack(walk[0])
           acked = True
 
         walk.append(next(gw))
-        if not acked and walk[2] == "c" * 40:
-          gw.ack("a" * 40)
-        elif not acked and walk[2] == "d" * 40:
-          gw.ack("b" * 40)
+        if not acked and walk[2] == b"c" * 40:
+          gw.ack(b"a" * 40)
+        elif not acked and walk[2] == b"d" * 40:
+          gw.ack(b"b" * 40)
         walk.append(next(gw))
         self.assertIs(None, next(gw))
 
-        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))
+        self.assertEqual([b"a" * 40, b"b" * 40, b"c" * 40, b"d" * 40], sorted(walk))
+        self.assertLess(walk.index(b"a" * 40), walk.index(b"c" * 40))
+        self.assertLess(walk.index(b"b" * 40), walk.index(b"d" * 40))

+ 73 - 6
dulwich/tests/test_objects.py

@@ -29,7 +29,9 @@ from itertools import (
     )
 import os
 import stat
+import sys
 import warnings
+from contextlib import contextmanager
 
 from dulwich.errors import (
     ObjectFormatException,
@@ -84,21 +86,23 @@ class BlobReadTests(TestCase):
     """Test decompression of blobs"""
 
     def get_sha_file(self, cls, base, sha):
-        dir = os.path.join(os.path.dirname(__file__), 'data', base)
+        dir = os.path.join(
+            os.path.dirname(__file__.encode(sys.getfilesystemencoding())),
+            b'data', base)
         return cls.from_path(hex_to_filename(dir, sha))
 
     def get_blob(self, sha):
         """Return the blob named sha from the test data dir"""
-        return self.get_sha_file(Blob, 'blobs', sha)
+        return self.get_sha_file(Blob, b'blobs', sha)
 
     def get_tree(self, sha):
-        return self.get_sha_file(Tree, 'trees', sha)
+        return self.get_sha_file(Tree, b'trees', sha)
 
     def get_tag(self, sha):
-        return self.get_sha_file(Tag, 'tags', sha)
+        return self.get_sha_file(Tag, b'tags', sha)
 
     def commit(self, sha):
-        return self.get_sha_file(Commit, 'commits', sha)
+        return self.get_sha_file(Commit, b'commits', sha)
 
     def test_decompress_simple_blob(self):
         b = self.get_blob(a_sha)
@@ -701,7 +705,9 @@ class TreeTests(ShaFileCheckTests):
         self.assertEqual(_SORTED_TREE_ITEMS, x.items())
 
     def _do_test_parse_tree(self, parse_tree):
-        dir = os.path.join(os.path.dirname(__file__), 'data', 'trees')
+        dir = os.path.join(
+            os.path.dirname(__file__.encode(sys.getfilesystemencoding())),
+            b'data', b'trees')
         o = Tree.from_path(hex_to_filename(dir, tree_sha))
         self.assertEqual([(b'a', 0o100644, a_sha), (b'b', 0o100644, b_sha)],
                          list(parse_tree(o.as_raw_string())))
@@ -1029,3 +1035,64 @@ class ShaFileCopyTests(TestCase):
             tag_time=12345, tag_timezone=0,
             object=(Commit, b'0' * 40))
         self.assert_copy(tag)
+
+
+class ShaFileSerializeTests(TestCase):
+    """
+    Test that `ShaFile` objects only gets serialized once if they haven't changed.
+    """
+
+    @contextmanager
+    def assert_serialization_on_change(self, obj, needs_serialization_after_change=True):
+        old_id = obj.id
+        self.assertFalse(obj._needs_serialization)
+
+        yield obj
+
+        if needs_serialization_after_change:
+            self.assertTrue(obj._needs_serialization)
+        else:
+            self.assertFalse(obj._needs_serialization)
+        new_id = obj.id
+        self.assertFalse(obj._needs_serialization)
+        self.assertNotEqual(old_id, new_id)
+
+    def test_commit_serialize(self):
+        attrs = {'tree': b'd80c186a03f423a81b39df39dc87fd269736ca86',
+                 'parents': [b'ab64bbdcc51b170d21588e5c5d391ee5c0c96dfd',
+                             b'4cffe90e0a41ad3f5190079d7c8f036bde29cbe6'],
+                 'author': b'James Westby <jw+debian@jameswestby.net>',
+                 'committer': b'James Westby <jw+debian@jameswestby.net>',
+                 'commit_time': 1174773719,
+                 'author_time': 1174773719,
+                 'commit_timezone': 0,
+                 'author_timezone': 0,
+                 'message':  b'Merge ../b\n'}
+        commit = make_commit(**attrs)
+
+        with self.assert_serialization_on_change(commit):
+            commit.parents = [b'ab64bbdcc51b170d21588e5c5d391ee5c0c96dfd']
+
+    def test_blob_serialize(self):
+        blob = make_object(Blob, data=b'i am a blob')
+
+        with self.assert_serialization_on_change(blob, needs_serialization_after_change=False):
+            blob.data = b'i am another blob'
+
+    def test_tree_serialize(self):
+        blob = make_object(Blob, data=b'i am a blob')
+        tree = Tree()
+        tree[b'blob'] = (stat.S_IFREG, blob.id)
+
+        with self.assert_serialization_on_change(tree):
+            tree[b'blob2'] = (stat.S_IFREG, blob.id)
+
+    def test_tag_serialize(self):
+        tag = make_object(
+            Tag, name=b'tag', message=b'',
+            tagger=b'Tagger <test@example.com>',
+            tag_time=12345, tag_timezone=0,
+            object=(Commit, b'0' * 40))
+
+        with self.assert_serialization_on_change(tag):
+            tag.message = b'new message'

+ 1 - 4
dulwich/tests/test_objectspec.py

@@ -35,11 +35,9 @@ from dulwich.tests import (
     )
 from dulwich.tests.utils import (
     build_commit_graph,
-    skipIfPY3,
     )
 
 
-@skipIfPY3
 class ParseObjectTests(TestCase):
     """Test parse_object."""
 
@@ -49,12 +47,11 @@ class ParseObjectTests(TestCase):
 
     def test_blob_by_sha(self):
         r = MemoryRepo()
-        b = Blob.from_string("Blah")
+        b = Blob.from_string(b"Blah")
         r.object_store.add_object(b)
         self.assertEqual(b, parse_object(r, b.id))
 
 
-@skipIfPY3
 class ParseCommitRangeTests(TestCase):
     """Test parse_commit_range."""
 

+ 197 - 204
dulwich/tests/test_pack.py

@@ -24,6 +24,7 @@ from io import BytesIO
 from hashlib import sha1
 import os
 import shutil
+import sys
 import tempfile
 import zlib
 
@@ -74,38 +75,39 @@ from dulwich.tests import (
 from dulwich.tests.utils import (
     make_object,
     build_pack,
-    skipIfPY3,
     )
 
-pack1_sha = 'bc63ddad95e7321ee734ea11a7a62d314e0d7481'
+pack1_sha = b'bc63ddad95e7321ee734ea11a7a62d314e0d7481'
 
-a_sha = '6f670c0fb53f9463760b7295fbb814e965fb20c8'
-tree_sha = 'b2a2766a2879c209ab1176e7e778b81ae422eeaa'
-commit_sha = 'f18faa16531ac570a3fdc8c7ca16682548dafd12'
+a_sha = b'6f670c0fb53f9463760b7295fbb814e965fb20c8'
+tree_sha = b'b2a2766a2879c209ab1176e7e778b81ae422eeaa'
+commit_sha = b'f18faa16531ac570a3fdc8c7ca16682548dafd12'
 
 
-@skipIfPY3
 class PackTests(TestCase):
     """Base class for testing packs"""
 
     def setUp(self):
         super(PackTests, self).setUp()
         self.tempdir = tempfile.mkdtemp()
+        if not isinstance(self.tempdir, bytes):
+            self.tempdir = self.tempdir.encode(sys.getfilesystemencoding())
         self.addCleanup(shutil.rmtree, self.tempdir)
 
-    datadir = os.path.abspath(os.path.join(os.path.dirname(__file__),
-        'data/packs'))
+    datadir = os.path.abspath(
+        os.path.join(os.path.dirname(__file__.encode(sys.getfilesystemencoding())),
+        b'data/packs'))
 
     def get_pack_index(self, sha):
         """Returns a PackIndex from the datadir with the given sha"""
-        return load_pack_index(os.path.join(self.datadir, 'pack-%s.idx' % sha))
+        return load_pack_index(os.path.join(self.datadir, b'pack-' + sha + b'.idx'))
 
     def get_pack_data(self, sha):
         """Returns a PackData object from the datadir with the given sha"""
-        return PackData(os.path.join(self.datadir, 'pack-%s.pack' % sha))
+        return PackData(os.path.join(self.datadir, b'pack-' + sha + b'.pack'))
 
     def get_pack(self, sha):
-        return Pack(os.path.join(self.datadir, 'pack-%s' % sha))
+        return Pack(os.path.join(self.datadir, b'pack-' + sha))
 
     def assertSucceeds(self, func, *args, **kwargs):
         try:
@@ -114,7 +116,6 @@ class PackTests(TestCase):
             self.fail(e)
 
 
-@skipIfPY3
 class PackIndexTests(PackTests):
     """Class that tests the index of packfiles"""
 
@@ -132,10 +133,10 @@ class PackIndexTests(PackTests):
 
     def test_get_stored_checksum(self):
         p = self.get_pack_index(pack1_sha)
-        self.assertEqual('f2848e2ad16f329ae1c92e3b95e91888daa5bd01',
-                          sha_to_hex(p.get_stored_checksum()))
-        self.assertEqual('721980e866af9a5f93ad674144e1459b8ba3e7b7',
-                          sha_to_hex(p.get_pack_checksum()))
+        self.assertEqual(b'f2848e2ad16f329ae1c92e3b95e91888daa5bd01',
+                         sha_to_hex(p.get_stored_checksum()))
+        self.assertEqual(b'721980e866af9a5f93ad674144e1459b8ba3e7b7',
+                         sha_to_hex(p.get_pack_checksum()))
 
     def test_index_check(self):
         p = self.get_pack_index(pack1_sha)
@@ -145,30 +146,29 @@ class PackIndexTests(PackTests):
         p = self.get_pack_index(pack1_sha)
         entries = [(sha_to_hex(s), o, c) for s, o, c in p.iterentries()]
         self.assertEqual([
-          ('6f670c0fb53f9463760b7295fbb814e965fb20c8', 178, None),
-          ('b2a2766a2879c209ab1176e7e778b81ae422eeaa', 138, None),
-          ('f18faa16531ac570a3fdc8c7ca16682548dafd12', 12, None)
-          ], entries)
+            (b'6f670c0fb53f9463760b7295fbb814e965fb20c8', 178, None),
+            (b'b2a2766a2879c209ab1176e7e778b81ae422eeaa', 138, None),
+            (b'f18faa16531ac570a3fdc8c7ca16682548dafd12', 12, None)
+        ], entries)
 
     def test_iter(self):
         p = self.get_pack_index(pack1_sha)
         self.assertEqual(set([tree_sha, commit_sha, a_sha]), set(p))
 
 
-@skipIfPY3
 class TestPackDeltas(TestCase):
 
-    test_string1 = 'The answer was flailing in the wind'
-    test_string2 = 'The answer was falling down the pipe'
-    test_string3 = 'zzzzz'
+    test_string1 = b'The answer was flailing in the wind'
+    test_string2 = b'The answer was falling down the pipe'
+    test_string3 = b'zzzzz'
 
-    test_string_empty = ''
-    test_string_big = 'Z' * 8192
-    test_string_huge = 'Z' * 100000
+    test_string_empty = b''
+    test_string_big = b'Z' * 8192
+    test_string_huge = b'Z' * 100000
 
     def _test_roundtrip(self, base, target):
         self.assertEqual(target,
-                          ''.join(apply_delta(base, create_delta(base, target))))
+                          b''.join(apply_delta(base, create_delta(base, target))))
 
     def test_nochange(self):
         self._test_roundtrip(self.test_string1, self.test_string1)
@@ -195,13 +195,12 @@ class TestPackDeltas(TestCase):
     def test_dest_overflow(self):
         self.assertRaises(
             ApplyDeltaError,
-            apply_delta, 'a'*0x10000, '\x80\x80\x04\x80\x80\x04\x80' + 'a'*0x10000)
+            apply_delta, b'a'*0x10000, b'\x80\x80\x04\x80\x80\x04\x80' + b'a'*0x10000)
         self.assertRaises(
             ApplyDeltaError,
-            apply_delta, '', '\x00\x80\x02\xb0\x11\x11')
+            apply_delta, b'', b'\x00\x80\x02\xb0\x11\x11')
 
 
-@skipIfPY3
 class TestPackData(PackTests):
     """Tests getting the data from the packfile."""
 
@@ -209,8 +208,9 @@ class TestPackData(PackTests):
         self.get_pack_data(pack1_sha).close()
 
     def test_from_file(self):
-        path = os.path.join(self.datadir, 'pack-%s.pack' % pack1_sha)
-        PackData.from_file(open(path), os.path.getsize(path))
+        path = os.path.join(self.datadir, b'pack-' + pack1_sha + b'.pack')
+        with open(path, 'rb') as f:
+            PackData.from_file(f, os.path.getsize(path))
 
     def test_pack_len(self):
         with self.get_pack_data(pack1_sha) as p:
@@ -222,36 +222,36 @@ class TestPackData(PackTests):
 
     def test_iterobjects(self):
         with self.get_pack_data(pack1_sha) as p:
-            commit_data = ('tree b2a2766a2879c209ab1176e7e778b81ae422eeaa\n'
-                           'author James Westby <jw+debian@jameswestby.net> '
-                           '1174945067 +0100\n'
-                           'committer James Westby <jw+debian@jameswestby.net> '
-                           '1174945067 +0100\n'
-                           '\n'
-                           'Test commit\n')
-            blob_sha = '6f670c0fb53f9463760b7295fbb814e965fb20c8'
-            tree_data = '100644 a\0%s' % hex_to_sha(blob_sha)
+            commit_data = (b'tree b2a2766a2879c209ab1176e7e778b81ae422eeaa\n'
+                           b'author James Westby <jw+debian@jameswestby.net> '
+                           b'1174945067 +0100\n'
+                           b'committer James Westby <jw+debian@jameswestby.net> '
+                           b'1174945067 +0100\n'
+                           b'\n'
+                           b'Test commit\n')
+            blob_sha = b'6f670c0fb53f9463760b7295fbb814e965fb20c8'
+            tree_data = b'100644 a\0' + hex_to_sha(blob_sha)
             actual = []
             for offset, type_num, chunks, crc32 in p.iterobjects():
-                actual.append((offset, type_num, ''.join(chunks), crc32))
+                actual.append((offset, type_num, b''.join(chunks), crc32))
             self.assertEqual([
-              (12, 1, commit_data, 3775879613),
-              (138, 2, tree_data, 912998690),
-              (178, 3, 'test 1\n', 1373561701)
-              ], actual)
+                (12, 1, commit_data, 3775879613),
+                (138, 2, tree_data, 912998690),
+                (178, 3, b'test 1\n', 1373561701)
+                ], actual)
 
     def test_iterentries(self):
         with self.get_pack_data(pack1_sha) as p:
             entries = set((sha_to_hex(s), o, c) for s, o, c in p.iterentries())
             self.assertEqual(set([
-              ('6f670c0fb53f9463760b7295fbb814e965fb20c8', 178, 1373561701),
-              ('b2a2766a2879c209ab1176e7e778b81ae422eeaa', 138, 912998690),
-              ('f18faa16531ac570a3fdc8c7ca16682548dafd12', 12, 3775879613),
+              (b'6f670c0fb53f9463760b7295fbb814e965fb20c8', 178, 1373561701),
+              (b'b2a2766a2879c209ab1176e7e778b81ae422eeaa', 138, 912998690),
+              (b'f18faa16531ac570a3fdc8c7ca16682548dafd12', 12, 3775879613),
               ]), entries)
 
     def test_create_index_v1(self):
         with self.get_pack_data(pack1_sha) as p:
-            filename = os.path.join(self.tempdir, 'v1test.idx')
+            filename = os.path.join(self.tempdir, b'v1test.idx')
             p.create_index_v1(filename)
             idx1 = load_pack_index(filename)
             idx2 = self.get_pack_index(pack1_sha)
@@ -259,35 +259,34 @@ class TestPackData(PackTests):
 
     def test_create_index_v2(self):
         with self.get_pack_data(pack1_sha) as p:
-            filename = os.path.join(self.tempdir, 'v2test.idx')
+            filename = os.path.join(self.tempdir, b'v2test.idx')
             p.create_index_v2(filename)
             idx1 = load_pack_index(filename)
             idx2 = self.get_pack_index(pack1_sha)
             self.assertEqual(idx1, idx2)
 
     def test_compute_file_sha(self):
-        f = BytesIO('abcd1234wxyz')
-        self.assertEqual(sha1('abcd1234wxyz').hexdigest(),
+        f = BytesIO(b'abcd1234wxyz')
+        self.assertEqual(sha1(b'abcd1234wxyz').hexdigest(),
                          compute_file_sha(f).hexdigest())
-        self.assertEqual(sha1('abcd1234wxyz').hexdigest(),
+        self.assertEqual(sha1(b'abcd1234wxyz').hexdigest(),
                          compute_file_sha(f, buffer_size=5).hexdigest())
-        self.assertEqual(sha1('abcd1234').hexdigest(),
+        self.assertEqual(sha1(b'abcd1234').hexdigest(),
                          compute_file_sha(f, end_ofs=-4).hexdigest())
-        self.assertEqual(sha1('1234wxyz').hexdigest(),
+        self.assertEqual(sha1(b'1234wxyz').hexdigest(),
                          compute_file_sha(f, start_ofs=4).hexdigest())
         self.assertEqual(
-          sha1('1234').hexdigest(),
-          compute_file_sha(f, start_ofs=4, end_ofs=-4).hexdigest())
+            sha1(b'1234').hexdigest(),
+            compute_file_sha(f, start_ofs=4, end_ofs=-4).hexdigest())
 
     def test_compute_file_sha_short_file(self):
-        f = BytesIO('abcd1234wxyz')
+        f = BytesIO(b'abcd1234wxyz')
         self.assertRaises(AssertionError, compute_file_sha, f, end_ofs=-20)
         self.assertRaises(AssertionError, compute_file_sha, f, end_ofs=20)
         self.assertRaises(AssertionError, compute_file_sha, f, start_ofs=10,
             end_ofs=-12)
 
 
-@skipIfPY3
 class TestPack(PackTests):
 
     def test_len(self):
@@ -323,19 +322,19 @@ class TestPack(PackTests):
         """Tests random access for non-delta objects"""
         with self.get_pack(pack1_sha) as p:
             obj = p[a_sha]
-            self.assertEqual(obj.type_name, 'blob')
-            self.assertEqual(obj.sha().hexdigest(), a_sha)
+            self.assertEqual(obj.type_name, b'blob')
+            self.assertEqual(obj.sha().hexdigest().encode('ascii'), a_sha)
             obj = p[tree_sha]
-            self.assertEqual(obj.type_name, 'tree')
-            self.assertEqual(obj.sha().hexdigest(), tree_sha)
+            self.assertEqual(obj.type_name, b'tree')
+            self.assertEqual(obj.sha().hexdigest().encode('ascii'), tree_sha)
             obj = p[commit_sha]
-            self.assertEqual(obj.type_name, 'commit')
-            self.assertEqual(obj.sha().hexdigest(), commit_sha)
+            self.assertEqual(obj.type_name, b'commit')
+            self.assertEqual(obj.sha().hexdigest().encode('ascii'), commit_sha)
 
     def test_copy(self):
         with self.get_pack(pack1_sha) as origpack:
             self.assertSucceeds(origpack.index.check)
-            basename = os.path.join(self.tempdir, 'Elch')
+            basename = os.path.join(self.tempdir, b'Elch')
             write_pack(basename, origpack.pack_tuples())
 
             with Pack(basename) as newpack:
@@ -353,12 +352,12 @@ class TestPack(PackTests):
     def test_commit_obj(self):
         with self.get_pack(pack1_sha) as p:
             commit = p[commit_sha]
-            self.assertEqual('James Westby <jw+debian@jameswestby.net>',
+            self.assertEqual(b'James Westby <jw+debian@jameswestby.net>',
                              commit.author)
             self.assertEqual([], commit.parents)
 
     def _copy_pack(self, origpack):
-        basename = os.path.join(self.tempdir, 'somepack')
+        basename = os.path.join(self.tempdir, b'somepack')
         write_pack(basename, origpack.pack_tuples())
         return Pack(basename)
 
@@ -380,7 +379,7 @@ class TestPack(PackTests):
         with self.get_pack(pack1_sha) as p:
             p = self._copy_pack(p)
 
-        msg = 'some message'
+        msg = b'some message'
         with p:
             keepfile_name = p.keep(msg)
 
@@ -388,9 +387,9 @@ class TestPack(PackTests):
         self.assertTrue(os.path.exists(keepfile_name))
 
         # and contain the right message, with a linefeed
-        with open(keepfile_name, 'r') as f:
+        with open(keepfile_name, 'rb') as f:
             buf = f.read()
-            self.assertEqual(msg + '\n', buf)
+            self.assertEqual(msg + b'\n', buf)
 
     def test_name(self):
         with self.get_pack(pack1_sha) as p:
@@ -406,7 +405,7 @@ class TestPack(PackTests):
             write_pack_header(bad_file, 9999)
             bad_file.write(data._file.read())
             bad_file = BytesIO(bad_file.getvalue())
-            bad_data = PackData('', file=bad_file)
+            bad_data = PackData(b'', file=bad_file)
             bad_pack = Pack.from_lazy_objects(lambda: bad_data, lambda: index)
             self.assertRaises(AssertionError, lambda: bad_pack.data)
             self.assertRaises(AssertionError,
@@ -418,8 +417,8 @@ class TestPack(PackTests):
             Pack.from_objects(data, index).check_length_and_checksum()
 
             data._file.seek(0)
-            bad_file = BytesIO(data._file.read()[:-20] + ('\xff' * 20))
-            bad_data = PackData('', file=bad_file)
+            bad_file = BytesIO(data._file.read()[:-20] + (b'\xff' * 20))
+            bad_data = PackData(b'', file=bad_file)
             bad_pack = Pack.from_lazy_objects(lambda: bad_data, lambda: index)
             self.assertRaises(ChecksumMismatch, lambda: bad_pack.data)
             self.assertRaises(ChecksumMismatch, lambda:
@@ -435,38 +434,39 @@ class TestPack(PackTests):
             self.assertTrue(isinstance(objs[commit_sha], Commit))
 
 
-@skipIfPY3
 class TestThinPack(PackTests):
 
     def setUp(self):
         super(TestThinPack, self).setUp()
         self.store = MemoryObjectStore()
         self.blobs = {}
-        for blob in ('foo', 'bar', 'foo1234', 'bar2468'):
+        for blob in (b'foo', b'bar', b'foo1234', b'bar2468'):
             self.blobs[blob] = make_object(Blob, data=blob)
-        self.store.add_object(self.blobs['foo'])
-        self.store.add_object(self.blobs['bar'])
+        self.store.add_object(self.blobs[b'foo'])
+        self.store.add_object(self.blobs[b'bar'])
 
         # Build a thin pack. 'foo' is as an external reference, 'bar' an
         # internal reference.
         self.pack_dir = tempfile.mkdtemp()
+        if not isinstance(self.pack_dir, bytes):
+            self.pack_dir = self.pack_dir.encode(sys.getfilesystemencoding())
         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, b'pack')
 
-        with open(self.pack_prefix + '.pack', 'wb') as f:
+        with open(self.pack_prefix + b'.pack', 'wb') as f:
             build_pack(f, [
-                (REF_DELTA, (self.blobs['foo'].id, 'foo1234')),
-                (Blob.type_num, 'bar'),
-                (REF_DELTA, (self.blobs['bar'].id, 'bar2468'))],
+                (REF_DELTA, (self.blobs[b'foo'].id, b'foo1234')),
+                (Blob.type_num, b'bar'),
+                (REF_DELTA, (self.blobs[b'bar'].id, b'bar2468'))],
                 store=self.store)
 
         # Index the new pack.
         with self.make_pack(True) as pack:
             with PackData(pack._data_path) as data:
                 data.pack = pack
-                data.create_index(self.pack_prefix + '.idx')
+                data.create_index(self.pack_prefix + b'.idx')
 
-        del self.store[self.blobs['bar'].id]
+        del self.store[self.blobs[b'bar'].id]
 
     def make_pack(self, resolve_ext_ref):
         return Pack(
@@ -476,54 +476,53 @@ class TestThinPack(PackTests):
     def test_get_raw(self):
         with self.make_pack(False) as p:
             self.assertRaises(
-                KeyError, p.get_raw, self.blobs['foo1234'].id)
+                KeyError, p.get_raw, self.blobs[b'foo1234'].id)
         with self.make_pack(True) as p:
             self.assertEqual(
-                (3, 'foo1234'),
-                p.get_raw(self.blobs['foo1234'].id))
+                (3, b'foo1234'),
+                p.get_raw(self.blobs[b'foo1234'].id))
 
     def test_iterobjects(self):
         with self.make_pack(False) as p:
             self.assertRaises(KeyError, list, p.iterobjects())
         with self.make_pack(True) as p:
             self.assertEqual(
-                sorted([self.blobs['foo1234'].id, self.blobs[b'bar'].id,
-                        self.blobs['bar2468'].id]),
+                sorted([self.blobs[b'foo1234'].id, self.blobs[b'bar'].id,
+                        self.blobs[b'bar2468'].id]),
                 sorted(o.id for o in p.iterobjects()))
 
 
-@skipIfPY3
 class WritePackTests(TestCase):
 
     def test_write_pack_header(self):
         f = BytesIO()
         write_pack_header(f, 42)
-        self.assertEqual('PACK\x00\x00\x00\x02\x00\x00\x00*',
-                f.getvalue())
+        self.assertEqual(b'PACK\x00\x00\x00\x02\x00\x00\x00*',
+                         f.getvalue())
 
     def test_write_pack_object(self):
         f = BytesIO()
-        f.write('header')
+        f.write(b'header')
         offset = f.tell()
-        crc32 = write_pack_object(f, Blob.type_num, 'blob')
+        crc32 = write_pack_object(f, Blob.type_num, b'blob')
         self.assertEqual(crc32, zlib.crc32(f.getvalue()[6:]) & 0xffffffff)
 
-        f.write('x')  # unpack_object needs extra trailing data.
+        f.write(b'x')  # unpack_object needs extra trailing data.
         f.seek(offset)
         unpacked, unused = unpack_object(f.read, compute_crc32=True)
         self.assertEqual(Blob.type_num, unpacked.pack_type_num)
         self.assertEqual(Blob.type_num, unpacked.obj_type_num)
-        self.assertEqual(['blob'], unpacked.decomp_chunks)
+        self.assertEqual([b'blob'], unpacked.decomp_chunks)
         self.assertEqual(crc32, unpacked.crc32)
-        self.assertEqual('x', unused)
+        self.assertEqual(b'x', unused)
 
     def test_write_pack_object_sha(self):
         f = BytesIO()
-        f.write('header')
+        f.write(b'header')
         offset = f.tell()
-        sha_a = sha1('foo')
+        sha_a = sha1(b'foo')
         sha_b = sha_a.copy()
-        write_pack_object(f, Blob.type_num, 'blob', sha=sha_a)
+        write_pack_object(f, Blob.type_num, b'blob', sha=sha_a)
         self.assertNotEqual(sha_a.digest(), sha_b.digest())
         sha_b.update(f.getvalue()[offset:])
         self.assertEqual(sha_a.digest(), sha_b.digest())
@@ -544,7 +543,7 @@ class BaseTestPackIndexWriting(object):
         raise NotImplementedError(self.index)
 
     def test_empty(self):
-        idx = self.index('empty.idx', [], pack_checksum)
+        idx = self.index(b'empty.idx', [], pack_checksum)
         self.assertEqual(idx.get_pack_checksum(), pack_checksum)
         self.assertEqual(0, len(idx))
 
@@ -557,7 +556,7 @@ class BaseTestPackIndexWriting(object):
             self.assertRaises(TypeError, self.index, 'single.idx',
                 entries, pack_checksum)
             return
-        idx = self.index('single.idx', entries, pack_checksum)
+        idx = self.index(b'single.idx', entries, pack_checksum)
         self.assertEqual(idx.get_pack_checksum(), pack_checksum)
         self.assertEqual(2, len(idx))
         actual_entries = list(idx.iterentries())
@@ -575,7 +574,7 @@ class BaseTestPackIndexWriting(object):
     def test_single(self):
         entry_sha = hex_to_sha('6f670c0fb53f9463760b7295fbb814e965fb20c8')
         my_entries = [(entry_sha, 178, 42)]
-        idx = self.index('single.idx', my_entries, pack_checksum)
+        idx = self.index(b'single.idx', my_entries, pack_checksum)
         self.assertEqual(idx.get_pack_checksum(), pack_checksum)
         self.assertEqual(1, len(idx))
         actual_entries = list(idx.iterentries())
@@ -595,6 +594,8 @@ class BaseTestFilePackIndexWriting(BaseTestPackIndexWriting):
 
     def setUp(self):
         self.tempdir = tempfile.mkdtemp()
+        if not isinstance(self.tempdir, bytes):
+            self.tempdir = self.tempdir.encode(sys.getfilesystemencoding())
 
     def tearDown(self):
         shutil.rmtree(self.tempdir)
@@ -613,7 +614,6 @@ class BaseTestFilePackIndexWriting(BaseTestPackIndexWriting):
             self._write_fn(f, entries, pack_checksum)
 
 
-@skipIfPY3
 class TestMemoryIndexWriting(TestCase, BaseTestPackIndexWriting):
 
     def setUp(self):
@@ -628,7 +628,6 @@ class TestMemoryIndexWriting(TestCase, BaseTestPackIndexWriting):
         TestCase.tearDown(self)
 
 
-@skipIfPY3
 class TestPackIndexWritingv1(TestCase, BaseTestFilePackIndexWriting):
 
     def setUp(self):
@@ -644,7 +643,6 @@ class TestPackIndexWritingv1(TestCase, BaseTestFilePackIndexWriting):
         BaseTestFilePackIndexWriting.tearDown(self)
 
 
-@skipIfPY3
 class TestPackIndexWritingv2(TestCase, BaseTestFilePackIndexWriting):
 
     def setUp(self):
@@ -660,7 +658,6 @@ class TestPackIndexWritingv2(TestCase, BaseTestFilePackIndexWriting):
         BaseTestFilePackIndexWriting.tearDown(self)
 
 
-@skipIfPY3
 class ReadZlibTests(TestCase):
 
     decomp = (
@@ -671,7 +668,7 @@ class ReadZlibTests(TestCase):
       b'\n'
       b"Provide replacement for mmap()'s offset argument.")
     comp = zlib.compress(decomp)
-    extra = 'nextobject'
+    extra = b'nextobject'
 
     def setUp(self):
         super(ReadZlibTests, self).setUp()
@@ -699,11 +696,11 @@ class ReadZlibTests(TestCase):
 
     def test_decompress_empty(self):
         unpacked = UnpackedObject(Tree.type_num, None, 0, None)
-        comp = zlib.compress('')
+        comp = zlib.compress(b'')
         read = BytesIO(comp + self.extra).read
         unused = read_zlib_chunks(read, unpacked)
-        self.assertEqual('', ''.join(unpacked.decomp_chunks))
-        self.assertNotEqual('', unused)
+        self.assertEqual(b'', b''.join(unpacked.decomp_chunks))
+        self.assertNotEqual(b'', unused)
         self.assertEqual(self.extra, unused + read())
 
     def test_decompress_no_crc32(self):
@@ -714,9 +711,9 @@ class ReadZlibTests(TestCase):
     def _do_decompress_test(self, buffer_size, **kwargs):
         unused = read_zlib_chunks(self.read, self.unpacked,
                                   buffer_size=buffer_size, **kwargs)
-        self.assertEqual(self.decomp, ''.join(self.unpacked.decomp_chunks))
+        self.assertEqual(self.decomp, b''.join(self.unpacked.decomp_chunks))
         self.assertEqual(zlib.crc32(self.comp), self.unpacked.crc32)
-        self.assertNotEqual('', unused)
+        self.assertNotEqual(b'', unused)
         self.assertEqual(self.extra, unused + self.read())
 
     def test_simple_decompress(self):
@@ -739,33 +736,31 @@ class ReadZlibTests(TestCase):
 
     def test_decompress_include_comp(self):
         self._do_decompress_test(4096, include_comp=True)
-        self.assertEqual(self.comp, ''.join(self.unpacked.comp_chunks))
+        self.assertEqual(self.comp, b''.join(self.unpacked.comp_chunks))
 
 
-@skipIfPY3
 class DeltifyTests(TestCase):
 
     def test_empty(self):
         self.assertEqual([], list(deltify_pack_objects([])))
 
     def test_single(self):
-        b = Blob.from_string("foo")
+        b = Blob.from_string(b"foo")
         self.assertEqual(
             [(b.type_num, b.sha().digest(), None, b.as_raw_string())],
-            list(deltify_pack_objects([(b, "")])))
+            list(deltify_pack_objects([(b, b"")])))
 
     def test_simple_delta(self):
-        b1 = Blob.from_string("a" * 101)
-        b2 = Blob.from_string("a" * 100)
+        b1 = Blob.from_string(b"a" * 101)
+        b2 = Blob.from_string(b"a" * 100)
         delta = create_delta(b1.as_raw_string(), b2.as_raw_string())
         self.assertEqual([
             (b1.type_num, b1.sha().digest(), None, b1.as_raw_string()),
             (b2.type_num, b2.sha().digest(), b1.sha().digest(), delta)
             ],
-            list(deltify_pack_objects([(b1, ""), (b2, "")])))
+            list(deltify_pack_objects([(b1, b""), (b2, b"")])))
 
 
-@skipIfPY3
 class TestPackStreamReader(TestCase):
 
     def test_read_objects_emtpy(self):
@@ -777,9 +772,9 @@ class TestPackStreamReader(TestCase):
     def test_read_objects(self):
         f = BytesIO()
         entries = build_pack(f, [
-          (Blob.type_num, 'blob'),
-          (OFS_DELTA, (0, 'blob1')),
-          ])
+            (Blob.type_num, b'blob'),
+            (OFS_DELTA, (0, b'blob1')),
+        ])
         reader = PackStreamReader(f.read)
         objects = list(reader.read_objects(compute_crc32=True))
         self.assertEqual(2, len(objects))
@@ -790,7 +785,7 @@ class TestPackStreamReader(TestCase):
         self.assertEqual(Blob.type_num, unpacked_blob.pack_type_num)
         self.assertEqual(Blob.type_num, unpacked_blob.obj_type_num)
         self.assertEqual(None, unpacked_blob.delta_base)
-        self.assertEqual('blob', ''.join(unpacked_blob.decomp_chunks))
+        self.assertEqual(b'blob', b''.join(unpacked_blob.decomp_chunks))
         self.assertEqual(entries[0][4], unpacked_blob.crc32)
 
         self.assertEqual(entries[1][0], unpacked_delta.offset)
@@ -798,16 +793,16 @@ class TestPackStreamReader(TestCase):
         self.assertEqual(None, unpacked_delta.obj_type_num)
         self.assertEqual(unpacked_delta.offset - unpacked_blob.offset,
                          unpacked_delta.delta_base)
-        delta = create_delta('blob', 'blob1')
-        self.assertEqual(delta, ''.join(unpacked_delta.decomp_chunks))
+        delta = create_delta(b'blob', b'blob1')
+        self.assertEqual(delta, b''.join(unpacked_delta.decomp_chunks))
         self.assertEqual(entries[1][4], unpacked_delta.crc32)
 
     def test_read_objects_buffered(self):
         f = BytesIO()
         build_pack(f, [
-          (Blob.type_num, 'blob'),
-          (OFS_DELTA, (0, 'blob1')),
-          ])
+            (Blob.type_num, b'blob'),
+            (OFS_DELTA, (0, b'blob1')),
+        ])
         reader = PackStreamReader(f.read, zlib_bufsize=4)
         self.assertEqual(2, len(list(reader.read_objects())))
 
@@ -816,7 +811,6 @@ class TestPackStreamReader(TestCase):
         self.assertEqual([], list(reader.read_objects()))
 
 
-@skipIfPY3
 class TestPackIterator(DeltaChainIterator):
 
     _compute_crc32 = True
@@ -828,7 +822,7 @@ class TestPackIterator(DeltaChainIterator):
     def _result(self, unpacked):
         """Return entries in the same format as build_pack."""
         return (unpacked.offset, unpacked.obj_type_num,
-                ''.join(unpacked.obj_chunks), unpacked.sha(), unpacked.crc32)
+                b''.join(unpacked.obj_chunks), unpacked.sha(), unpacked.crc32)
 
     def _resolve_object(self, offset, pack_type_num, base_chunks):
         assert offset not in self._unpacked_offsets, (
@@ -838,7 +832,6 @@ class TestPackIterator(DeltaChainIterator):
           offset, pack_type_num, base_chunks)
 
 
-@skipIfPY3
 class DeltaChainIteratorTests(TestCase):
 
     def setUp(self):
@@ -877,46 +870,46 @@ class DeltaChainIteratorTests(TestCase):
     def test_no_deltas(self):
         f = BytesIO()
         entries = build_pack(f, [
-          (Commit.type_num, 'commit'),
-          (Blob.type_num, 'blob'),
-          (Tree.type_num, 'tree'),
-          ])
+            (Commit.type_num, b'commit'),
+            (Blob.type_num, b'blob'),
+            (Tree.type_num, b'tree'),
+        ])
         self.assertEntriesMatch([0, 1, 2], entries, self.make_pack_iter(f))
 
     def test_ofs_deltas(self):
         f = BytesIO()
         entries = build_pack(f, [
-          (Blob.type_num, 'blob'),
-          (OFS_DELTA, (0, 'blob1')),
-          (OFS_DELTA, (0, 'blob2')),
-          ])
+            (Blob.type_num, b'blob'),
+            (OFS_DELTA, (0, b'blob1')),
+            (OFS_DELTA, (0, b'blob2')),
+        ])
         self.assertEntriesMatch([0, 1, 2], entries, self.make_pack_iter(f))
 
     def test_ofs_deltas_chain(self):
         f = BytesIO()
         entries = build_pack(f, [
-          (Blob.type_num, 'blob'),
-          (OFS_DELTA, (0, 'blob1')),
-          (OFS_DELTA, (1, 'blob2')),
-          ])
+            (Blob.type_num, b'blob'),
+            (OFS_DELTA, (0, b'blob1')),
+            (OFS_DELTA, (1, b'blob2')),
+        ])
         self.assertEntriesMatch([0, 1, 2], entries, self.make_pack_iter(f))
 
     def test_ref_deltas(self):
         f = BytesIO()
         entries = build_pack(f, [
-          (REF_DELTA, (1, 'blob1')),
-          (Blob.type_num, ('blob')),
-          (REF_DELTA, (1, 'blob2')),
-          ])
+            (REF_DELTA, (1, b'blob1')),
+            (Blob.type_num, (b'blob')),
+            (REF_DELTA, (1, b'blob2')),
+        ])
         self.assertEntriesMatch([1, 0, 2], entries, self.make_pack_iter(f))
 
     def test_ref_deltas_chain(self):
         f = BytesIO()
         entries = build_pack(f, [
-          (REF_DELTA, (2, 'blob1')),
-          (Blob.type_num, ('blob')),
-          (REF_DELTA, (1, 'blob2')),
-          ])
+            (REF_DELTA, (2, b'blob1')),
+            (Blob.type_num, (b'blob')),
+            (REF_DELTA, (1, b'blob2')),
+        ])
         self.assertEntriesMatch([1, 2, 0], entries, self.make_pack_iter(f))
 
     def test_ofs_and_ref_deltas(self):
@@ -924,58 +917,58 @@ class DeltaChainIteratorTests(TestCase):
         # this ref.
         f = BytesIO()
         entries = build_pack(f, [
-          (REF_DELTA, (1, 'blob1')),
-          (Blob.type_num, ('blob')),
-          (OFS_DELTA, (1, 'blob2')),
-          ])
+            (REF_DELTA, (1, b'blob1')),
+            (Blob.type_num, (b'blob')),
+            (OFS_DELTA, (1, b'blob2')),
+        ])
         self.assertEntriesMatch([1, 2, 0], entries, self.make_pack_iter(f))
 
     def test_mixed_chain(self):
         f = BytesIO()
         entries = build_pack(f, [
-          (Blob.type_num, 'blob'),
-          (REF_DELTA, (2, 'blob2')),
-          (OFS_DELTA, (0, 'blob1')),
-          (OFS_DELTA, (1, 'blob3')),
-          (OFS_DELTA, (0, 'bob')),
-          ])
+            (Blob.type_num, b'blob'),
+            (REF_DELTA, (2, b'blob2')),
+            (OFS_DELTA, (0, b'blob1')),
+            (OFS_DELTA, (1, b'blob3')),
+            (OFS_DELTA, (0, b'bob')),
+        ])
         self.assertEntriesMatch([0, 2, 1, 3, 4], entries,
                                 self.make_pack_iter(f))
 
     def test_long_chain(self):
         n = 100
-        objects_spec = [(Blob.type_num, 'blob')]
+        objects_spec = [(Blob.type_num, b'blob')]
         for i in range(n):
-            objects_spec.append((OFS_DELTA, (i, 'blob%i' % i)))
+            objects_spec.append((OFS_DELTA, (i, b'blob' + str(i).encode('ascii'))))
         f = BytesIO()
         entries = build_pack(f, objects_spec)
         self.assertEntriesMatch(range(n + 1), entries, self.make_pack_iter(f))
 
     def test_branchy_chain(self):
         n = 100
-        objects_spec = [(Blob.type_num, 'blob')]
+        objects_spec = [(Blob.type_num, b'blob')]
         for i in range(n):
-            objects_spec.append((OFS_DELTA, (0, 'blob%i' % i)))
+            objects_spec.append((OFS_DELTA, (0, b'blob' + str(i).encode('ascii'))))
         f = BytesIO()
         entries = build_pack(f, objects_spec)
         self.assertEntriesMatch(range(n + 1), entries, self.make_pack_iter(f))
 
     def test_ext_ref(self):
-        blob, = self.store_blobs(['blob'])
+        blob, = self.store_blobs([b'blob'])
         f = BytesIO()
-        entries = build_pack(f, [(REF_DELTA, (blob.id, 'blob1'))],
+        entries = build_pack(f, [(REF_DELTA, (blob.id, b'blob1'))],
                              store=self.store)
         pack_iter = self.make_pack_iter(f)
         self.assertEntriesMatch([0], entries, pack_iter)
         self.assertEqual([hex_to_sha(blob.id)], pack_iter.ext_refs())
 
     def test_ext_ref_chain(self):
-        blob, = self.store_blobs(['blob'])
+        blob, = self.store_blobs([b'blob'])
         f = BytesIO()
         entries = build_pack(f, [
-          (REF_DELTA, (1, 'blob2')),
-          (REF_DELTA, (blob.id, 'blob1')),
-          ], store=self.store)
+            (REF_DELTA, (1, b'blob2')),
+            (REF_DELTA, (blob.id, b'blob1')),
+        ], store=self.store)
         pack_iter = self.make_pack_iter(f)
         self.assertEntriesMatch([1, 0], entries, pack_iter)
         self.assertEqual([hex_to_sha(blob.id)], pack_iter.ext_refs())
@@ -983,46 +976,46 @@ class DeltaChainIteratorTests(TestCase):
     def test_ext_ref_chain_degenerate(self):
         # Test a degenerate case where the sender is sending a REF_DELTA
         # object that expands to an object already in the repository.
-        blob, = self.store_blobs(['blob'])
-        blob2, = self.store_blobs(['blob2'])
+        blob, = self.store_blobs([b'blob'])
+        blob2, = self.store_blobs([b'blob2'])
         assert blob.id < blob2.id
 
         f = BytesIO()
         entries = build_pack(f, [
-          (REF_DELTA, (blob.id, 'blob2')),
-          (REF_DELTA, (0, 'blob3')),
+          (REF_DELTA, (blob.id, b'blob2')),
+          (REF_DELTA, (0, b'blob3')),
           ], store=self.store)
         pack_iter = self.make_pack_iter(f)
         self.assertEntriesMatch([0, 1], entries, pack_iter)
         self.assertEqual([hex_to_sha(blob.id)], pack_iter.ext_refs())
 
     def test_ext_ref_multiple_times(self):
-        blob, = self.store_blobs(['blob'])
+        blob, = self.store_blobs([b'blob'])
         f = BytesIO()
         entries = build_pack(f, [
-          (REF_DELTA, (blob.id, 'blob1')),
-          (REF_DELTA, (blob.id, 'blob2')),
-          ], store=self.store)
+            (REF_DELTA, (blob.id, b'blob1')),
+            (REF_DELTA, (blob.id, b'blob2')),
+        ], store=self.store)
         pack_iter = self.make_pack_iter(f)
         self.assertEntriesMatch([0, 1], entries, pack_iter)
         self.assertEqual([hex_to_sha(blob.id)], pack_iter.ext_refs())
 
     def test_multiple_ext_refs(self):
-        b1, b2 = self.store_blobs(['foo', 'bar'])
+        b1, b2 = self.store_blobs([b'foo', b'bar'])
         f = BytesIO()
         entries = build_pack(f, [
-          (REF_DELTA, (b1.id, 'foo1')),
-          (REF_DELTA, (b2.id, 'bar2')),
-          ], store=self.store)
+            (REF_DELTA, (b1.id, b'foo1')),
+            (REF_DELTA, (b2.id, b'bar2')),
+        ], store=self.store)
         pack_iter = self.make_pack_iter(f)
         self.assertEntriesMatch([0, 1], entries, pack_iter)
         self.assertEqual([hex_to_sha(b1.id), hex_to_sha(b2.id)],
                          pack_iter.ext_refs())
 
     def test_bad_ext_ref_non_thin_pack(self):
-        blob, = self.store_blobs(['blob'])
+        blob, = self.store_blobs([b'blob'])
         f = BytesIO()
-        build_pack(f, [(REF_DELTA, (blob.id, 'blob1'))],
+        entries = build_pack(f, [(REF_DELTA, (blob.id, b'blob1'))],
                              store=self.store)
         pack_iter = self.make_pack_iter(f, thin=False)
         try:
@@ -1032,13 +1025,13 @@ class DeltaChainIteratorTests(TestCase):
             self.assertEqual(([blob.id],), e.args)
 
     def test_bad_ext_ref_thin_pack(self):
-        b1, b2, b3 = self.store_blobs(['foo', 'bar', 'baz'])
+        b1, b2, b3 = self.store_blobs([b'foo', b'bar', b'baz'])
         f = BytesIO()
         build_pack(f, [
-          (REF_DELTA, (1, 'foo99')),
-          (REF_DELTA, (b1.id, 'foo1')),
-          (REF_DELTA, (b2.id, 'bar2')),
-          (REF_DELTA, (b3.id, 'baz3')),
+          (REF_DELTA, (1, b'foo99')),
+          (REF_DELTA, (b1.id, b'foo1')),
+          (REF_DELTA, (b2.id, b'bar2')),
+          (REF_DELTA, (b3.id, b'baz3')),
           ], store=self.store)
         del self.store[b2.id]
         del self.store[b3.id]
@@ -1053,17 +1046,17 @@ class DeltaChainIteratorTests(TestCase):
 class DeltaEncodeSizeTests(TestCase):
 
     def test_basic(self):
-        self.assertEqual('\x00', _delta_encode_size(0))
-        self.assertEqual('\x01', _delta_encode_size(1))
-        self.assertEqual('\xfa\x01', _delta_encode_size(250))
-        self.assertEqual('\xe8\x07', _delta_encode_size(1000))
-        self.assertEqual('\xa0\x8d\x06', _delta_encode_size(100000))
+        self.assertEqual(b'\x00', _delta_encode_size(0))
+        self.assertEqual(b'\x01', _delta_encode_size(1))
+        self.assertEqual(b'\xfa\x01', _delta_encode_size(250))
+        self.assertEqual(b'\xe8\x07', _delta_encode_size(1000))
+        self.assertEqual(b'\xa0\x8d\x06', _delta_encode_size(100000))
 
 
 class EncodeCopyOperationTests(TestCase):
 
     def test_basic(self):
-        self.assertEqual('\x80', _encode_copy_operation(0, 0))
-        self.assertEqual('\x91\x01\x0a', _encode_copy_operation(1, 10))
-        self.assertEqual('\xb1\x64\xe8\x03', _encode_copy_operation(100, 1000))
-        self.assertEqual('\x93\xe8\x03\x01', _encode_copy_operation(1000, 1))
+        self.assertEqual(b'\x80', _encode_copy_operation(0, 0))
+        self.assertEqual(b'\x91\x01\x0a', _encode_copy_operation(1, 10))
+        self.assertEqual(b'\xb1\x64\xe8\x03', _encode_copy_operation(100, 1000))
+        self.assertEqual(b'\x93\xe8\x03\x01', _encode_copy_operation(1000, 1))

+ 167 - 192
dulwich/tests/test_porcelain.py

@@ -39,7 +39,6 @@ from dulwich.tests.compat.utils import require_git_version
 from dulwich.tests.utils import (
     build_commit_graph,
     make_object,
-    skipIfPY3,
     )
 
 
@@ -52,7 +51,6 @@ class PorcelainTestCase(TestCase):
         self.repo = Repo.init(repo_dir)
 
 
-@skipIfPY3
 class ArchiveTests(PorcelainTestCase):
     """Tests for the archive command."""
 
@@ -60,120 +58,116 @@ class ArchiveTests(PorcelainTestCase):
         # TODO(jelmer): Remove this once dulwich has its own implementation of archive.
         require_git_version((1, 5, 0))
         c1, c2, c3 = build_commit_graph(self.repo.object_store, [[1], [2, 1], [3, 1, 2]])
-        self.repo.refs["refs/heads/master"] = c3.id
+        self.repo.refs[b"refs/heads/master"] = c3.id
         out = BytesIO()
         err = BytesIO()
-        porcelain.archive(self.repo.path, "refs/heads/master", outstream=out,
+        porcelain.archive(self.repo.path, b"refs/heads/master", outstream=out,
             errstream=err)
-        self.assertEqual("", err.getvalue())
+        self.assertEqual(b"", err.getvalue())
         tf = tarfile.TarFile(fileobj=out)
         self.addCleanup(tf.close)
         self.assertEqual([], tf.getnames())
 
 
-@skipIfPY3
 class UpdateServerInfoTests(PorcelainTestCase):
 
     def test_simple(self):
         c1, c2, c3 = build_commit_graph(self.repo.object_store, [[1], [2, 1],
             [3, 1, 2]])
-        self.repo.refs["refs/heads/foo"] = c3.id
+        self.repo.refs[b"refs/heads/foo"] = c3.id
         porcelain.update_server_info(self.repo.path)
         self.assertTrue(os.path.exists(os.path.join(self.repo.controldir(),
-            'info', 'refs')))
+            b'info', b'refs')))
 
 
-@skipIfPY3
 class CommitTests(PorcelainTestCase):
 
     def test_custom_author(self):
         c1, c2, c3 = build_commit_graph(self.repo.object_store, [[1], [2, 1],
             [3, 1, 2]])
-        self.repo.refs["refs/heads/foo"] = c3.id
-        sha = porcelain.commit(self.repo.path, message="Some message",
-                author="Joe <joe@example.com>", committer="Bob <bob@example.com>")
-        self.assertTrue(isinstance(sha, str))
+        self.repo.refs[b"refs/heads/foo"] = c3.id
+        sha = porcelain.commit(self.repo.path, message=b"Some message",
+                author=b"Joe <joe@example.com>", committer=b"Bob <bob@example.com>")
+        self.assertTrue(isinstance(sha, bytes))
         self.assertEqual(len(sha), 40)
 
 
-@skipIfPY3
 class CloneTests(PorcelainTestCase):
 
     def test_simple_local(self):
-        f1_1 = make_object(Blob, data='f1')
+        f1_1 = make_object(Blob, data=b'f1')
         commit_spec = [[1], [2, 1], [3, 1, 2]]
-        trees = {1: [('f1', f1_1), ('f2', f1_1)],
-                 2: [('f1', f1_1), ('f2', f1_1)],
-                 3: [('f1', f1_1), ('f2', f1_1)], }
+        trees = {1: [(b'f1', f1_1), (b'f2', f1_1)],
+                 2: [(b'f1', f1_1), (b'f2', f1_1)],
+                 3: [(b'f1', f1_1), (b'f2', f1_1)], }
 
         c1, c2, c3 = build_commit_graph(self.repo.object_store,
                                         commit_spec, trees)
-        self.repo.refs["refs/heads/master"] = c3.id
+        self.repo.refs[b"refs/heads/master"] = c3.id
         target_path = tempfile.mkdtemp()
-        outstream = BytesIO()
+        errstream = BytesIO()
         self.addCleanup(shutil.rmtree, target_path)
         r = porcelain.clone(self.repo.path, target_path,
-                            checkout=False, outstream=outstream)
+                            checkout=False, errstream=errstream)
         self.assertEqual(r.path, target_path)
         self.assertEqual(Repo(target_path).head(), c3.id)
-        self.assertTrue('f1' not in os.listdir(target_path))
-        self.assertTrue('f2' not in os.listdir(target_path))
+        self.assertTrue(b'f1' not in os.listdir(target_path))
+        self.assertTrue(b'f2' not in os.listdir(target_path))
 
     def test_simple_local_with_checkout(self):
-        f1_1 = make_object(Blob, data='f1')
+        f1_1 = make_object(Blob, data=b'f1')
         commit_spec = [[1], [2, 1], [3, 1, 2]]
-        trees = {1: [('f1', f1_1), ('f2', f1_1)],
-                 2: [('f1', f1_1), ('f2', f1_1)],
-                 3: [('f1', f1_1), ('f2', f1_1)], }
+        trees = {1: [(b'f1', f1_1), (b'f2', f1_1)],
+                 2: [(b'f1', f1_1), (b'f2', f1_1)],
+                 3: [(b'f1', f1_1), (b'f2', f1_1)], }
 
         c1, c2, c3 = build_commit_graph(self.repo.object_store,
                                         commit_spec, trees)
-        self.repo.refs["refs/heads/master"] = c3.id
+        self.repo.refs[b"refs/heads/master"] = c3.id
         target_path = tempfile.mkdtemp()
-        outstream = BytesIO()
+        errstream = BytesIO()
         self.addCleanup(shutil.rmtree, target_path)
         r = porcelain.clone(self.repo.path, target_path,
-                            checkout=True, outstream=outstream)
+                            checkout=True, errstream=errstream)
         self.assertEqual(r.path, target_path)
         self.assertEqual(Repo(target_path).head(), c3.id)
         self.assertTrue('f1' in os.listdir(target_path))
         self.assertTrue('f2' in os.listdir(target_path))
 
     def test_bare_local_with_checkout(self):
-        f1_1 = make_object(Blob, data='f1')
+        f1_1 = make_object(Blob, data=b'f1')
         commit_spec = [[1], [2, 1], [3, 1, 2]]
-        trees = {1: [('f1', f1_1), ('f2', f1_1)],
-                 2: [('f1', f1_1), ('f2', f1_1)],
-                 3: [('f1', f1_1), ('f2', f1_1)], }
+        trees = {1: [(b'f1', f1_1), (b'f2', f1_1)],
+                 2: [(b'f1', f1_1), (b'f2', f1_1)],
+                 3: [(b'f1', f1_1), (b'f2', f1_1)], }
 
         c1, c2, c3 = build_commit_graph(self.repo.object_store,
                                         commit_spec, trees)
-        self.repo.refs["refs/heads/master"] = c3.id
+        self.repo.refs[b"refs/heads/master"] = c3.id
         target_path = tempfile.mkdtemp()
-        outstream = BytesIO()
+        errstream = BytesIO()
         self.addCleanup(shutil.rmtree, target_path)
         r = porcelain.clone(self.repo.path, target_path,
-                            bare=True, outstream=outstream)
+                            bare=True, errstream=errstream)
         self.assertEqual(r.path, target_path)
         self.assertEqual(Repo(target_path).head(), c3.id)
-        self.assertFalse('f1' in os.listdir(target_path))
-        self.assertFalse('f2' in os.listdir(target_path))
+        self.assertFalse(b'f1' in os.listdir(target_path))
+        self.assertFalse(b'f2' in os.listdir(target_path))
 
     def test_no_checkout_with_bare(self):
-        f1_1 = make_object(Blob, data='f1')
+        f1_1 = make_object(Blob, data=b'f1')
         commit_spec = [[1]]
-        trees = {1: [('f1', f1_1), ('f2', f1_1)]}
+        trees = {1: [(b'f1', f1_1), (b'f2', f1_1)]}
 
         (c1, ) = build_commit_graph(self.repo.object_store, commit_spec, trees)
-        self.repo.refs["refs/heads/master"] = c1.id
+        self.repo.refs[b"refs/heads/master"] = c1.id
         target_path = tempfile.mkdtemp()
-        outstream = BytesIO()
+        errstream = BytesIO()
         self.addCleanup(shutil.rmtree, target_path)
         self.assertRaises(ValueError, porcelain.clone, self.repo.path,
-            target_path, checkout=True, bare=True, outstream=outstream)
+            target_path, checkout=True, bare=True, errstream=errstream)
 
 
-@skipIfPY3
 class InitTests(TestCase):
 
     def test_non_bare(self):
@@ -187,7 +181,6 @@ class InitTests(TestCase):
         porcelain.init(repo_dir, bare=True)
 
 
-@skipIfPY3
 class AddTests(PorcelainTestCase):
 
     def test_add_default_paths(self):
@@ -196,8 +189,8 @@ class AddTests(PorcelainTestCase):
         with open(os.path.join(self.repo.path, 'blah'), 'w') as f:
             f.write("\n")
         porcelain.add(repo=self.repo.path, paths=['blah'])
-        porcelain.commit(repo=self.repo.path, message='test',
-            author='test', committer='test')
+        porcelain.commit(repo=self.repo.path, message=b'test',
+            author=b'test', committer=b'test')
 
         # Add a second test file and a file in a directory
         with open(os.path.join(self.repo.path, 'foo'), 'w') as f:
@@ -209,7 +202,7 @@ class AddTests(PorcelainTestCase):
 
         # Check that foo was added and nothing in .git was modified
         index = self.repo.open_index()
-        self.assertEqual(sorted(index), ['adir/afile', 'blah', 'foo'])
+        self.assertEqual(sorted(index), [b'adir/afile', b'blah', b'foo'])
 
     def test_add_file(self):
         with open(os.path.join(self.repo.path, 'foo'), 'w') as f:
@@ -217,7 +210,6 @@ class AddTests(PorcelainTestCase):
         porcelain.add(self.repo.path, paths=["foo"])
 
 
-@skipIfPY3
 class RemoveTests(PorcelainTestCase):
 
     def test_remove_file(self):
@@ -227,129 +219,123 @@ class RemoveTests(PorcelainTestCase):
         porcelain.rm(self.repo.path, paths=["foo"])
 
 
-@skipIfPY3
 class LogTests(PorcelainTestCase):
 
     def test_simple(self):
         c1, c2, c3 = build_commit_graph(self.repo.object_store, [[1], [2, 1],
             [3, 1, 2]])
-        self.repo.refs["HEAD"] = c3.id
+        self.repo.refs[b"HEAD"] = c3.id
         outstream = BytesIO()
         porcelain.log(self.repo.path, outstream=outstream)
-        self.assertEqual(3, outstream.getvalue().count("-" * 50))
+        self.assertEqual(3, outstream.getvalue().count(b"-" * 50))
 
     def test_max_entries(self):
         c1, c2, c3 = build_commit_graph(self.repo.object_store, [[1], [2, 1],
             [3, 1, 2]])
-        self.repo.refs["HEAD"] = c3.id
+        self.repo.refs[b"HEAD"] = c3.id
         outstream = BytesIO()
         porcelain.log(self.repo.path, outstream=outstream, max_entries=1)
-        self.assertEqual(1, outstream.getvalue().count("-" * 50))
+        self.assertEqual(1, outstream.getvalue().count(b"-" * 50))
 
 
-@skipIfPY3
 class ShowTests(PorcelainTestCase):
 
     def test_nolist(self):
         c1, c2, c3 = build_commit_graph(self.repo.object_store, [[1], [2, 1],
             [3, 1, 2]])
-        self.repo.refs["HEAD"] = c3.id
+        self.repo.refs[b"HEAD"] = c3.id
         outstream = BytesIO()
         porcelain.show(self.repo.path, objects=c3.id, outstream=outstream)
-        self.assertTrue(outstream.getvalue().startswith("-" * 50))
+        self.assertTrue(outstream.getvalue().startswith(b"-" * 50))
 
     def test_simple(self):
         c1, c2, c3 = build_commit_graph(self.repo.object_store, [[1], [2, 1],
             [3, 1, 2]])
-        self.repo.refs["HEAD"] = c3.id
+        self.repo.refs[b"HEAD"] = c3.id
         outstream = BytesIO()
         porcelain.show(self.repo.path, objects=[c3.id], outstream=outstream)
-        self.assertTrue(outstream.getvalue().startswith("-" * 50))
+        self.assertTrue(outstream.getvalue().startswith(b"-" * 50))
 
     def test_blob(self):
-        b = Blob.from_string("The Foo\n")
+        b = Blob.from_string(b"The Foo\n")
         self.repo.object_store.add_object(b)
         outstream = BytesIO()
         porcelain.show(self.repo.path, objects=[b.id], outstream=outstream)
-        self.assertEqual(outstream.getvalue(), "The Foo\n")
+        self.assertEqual(outstream.getvalue(), b"The Foo\n")
 
 
-@skipIfPY3
 class SymbolicRefTests(PorcelainTestCase):
 
     def test_set_wrong_symbolic_ref(self):
         c1, c2, c3 = build_commit_graph(self.repo.object_store, [[1], [2, 1],
             [3, 1, 2]])
-        self.repo.refs["HEAD"] = c3.id
+        self.repo.refs[b"HEAD"] = c3.id
 
-        self.assertRaises(ValueError, porcelain.symbolic_ref, self.repo.path, 'foobar')
+        self.assertRaises(ValueError, porcelain.symbolic_ref, self.repo.path, b'foobar')
 
     def test_set_force_wrong_symbolic_ref(self):
         c1, c2, c3 = build_commit_graph(self.repo.object_store, [[1], [2, 1],
             [3, 1, 2]])
-        self.repo.refs["HEAD"] = c3.id
+        self.repo.refs[b"HEAD"] = c3.id
 
-        porcelain.symbolic_ref(self.repo.path, 'force_foobar', force=True)
+        porcelain.symbolic_ref(self.repo.path, b'force_foobar', force=True)
 
         #test if we actually changed the file
-        with self.repo.get_named_file('HEAD') as f:
+        with self.repo.get_named_file(b'HEAD') as f:
             new_ref = f.read()
         self.assertEqual(new_ref, b'ref: refs/heads/force_foobar\n')
 
     def test_set_symbolic_ref(self):
         c1, c2, c3 = build_commit_graph(self.repo.object_store, [[1], [2, 1],
             [3, 1, 2]])
-        self.repo.refs["HEAD"] = c3.id
+        self.repo.refs[b"HEAD"] = c3.id
 
-        porcelain.symbolic_ref(self.repo.path, 'master')
+        porcelain.symbolic_ref(self.repo.path, b'master')
 
     def test_set_symbolic_ref_other_than_master(self):
         c1, c2, c3 = build_commit_graph(self.repo.object_store, [[1], [2, 1],
             [3, 1, 2]], attrs=dict(refs='develop'))
-        self.repo.refs["HEAD"] = c3.id
-        self.repo.refs["refs/heads/develop"] = c3.id
+        self.repo.refs[b"HEAD"] = c3.id
+        self.repo.refs[b"refs/heads/develop"] = c3.id
 
-        porcelain.symbolic_ref(self.repo.path, 'develop')
+        porcelain.symbolic_ref(self.repo.path, b'develop')
 
         #test if we actually changed the file
-        with self.repo.get_named_file('HEAD') as f:
+        with self.repo.get_named_file(b'HEAD') as f:
             new_ref = f.read()
         self.assertEqual(new_ref, b'ref: refs/heads/develop\n')
 
 
-@skipIfPY3
 class DiffTreeTests(PorcelainTestCase):
 
     def test_empty(self):
         c1, c2, c3 = build_commit_graph(self.repo.object_store, [[1], [2, 1],
             [3, 1, 2]])
-        self.repo.refs["HEAD"] = c3.id
+        self.repo.refs[b"HEAD"] = c3.id
         outstream = BytesIO()
         porcelain.diff_tree(self.repo.path, c2.tree, c3.tree, outstream=outstream)
-        self.assertEqual(outstream.getvalue(), "")
+        self.assertEqual(outstream.getvalue(), b"")
 
 
-@skipIfPY3
 class CommitTreeTests(PorcelainTestCase):
 
     def test_simple(self):
         c1, c2, c3 = build_commit_graph(self.repo.object_store, [[1], [2, 1],
             [3, 1, 2]])
         b = Blob()
-        b.data = "foo the bar"
+        b.data = b"foo the bar"
         t = Tree()
-        t.add("somename", 0o100644, b.id)
+        t.add(b"somename", 0o100644, b.id)
         self.repo.object_store.add_object(t)
         self.repo.object_store.add_object(b)
         sha = porcelain.commit_tree(
-            self.repo.path, t.id, message="Withcommit.",
-            author="Joe <joe@example.com>",
-            committer="Jane <jane@example.com>")
-        self.assertTrue(isinstance(sha, str))
+            self.repo.path, t.id, message=b"Withcommit.",
+            author=b"Joe <joe@example.com>",
+            committer=b"Jane <jane@example.com>")
+        self.assertTrue(isinstance(sha, bytes))
         self.assertEqual(len(sha), 40)
 
 
-@skipIfPY3
 class RevListTests(PorcelainTestCase):
 
     def test_simple(self):
@@ -359,42 +345,42 @@ class RevListTests(PorcelainTestCase):
         porcelain.rev_list(
             self.repo.path, [c3.id], outstream=outstream)
         self.assertEqual(
-            "%s\n%s\n%s\n" % (c3.id, c2.id, c1.id),
+            c3.id + b"\n" +
+            c2.id + b"\n" +
+            c1.id + b"\n",
             outstream.getvalue())
 
 
-@skipIfPY3
 class TagCreateTests(PorcelainTestCase):
 
     def test_annotated(self):
         c1, c2, c3 = build_commit_graph(self.repo.object_store, [[1], [2, 1],
             [3, 1, 2]])
-        self.repo.refs["HEAD"] = c3.id
+        self.repo.refs[b"HEAD"] = c3.id
 
-        porcelain.tag_create(self.repo.path, "tryme", 'foo <foo@bar.com>',
-                'bar', annotated=True)
+        porcelain.tag_create(self.repo.path, b"tryme", b'foo <foo@bar.com>',
+                b'bar', annotated=True)
 
-        tags = self.repo.refs.as_dict("refs/tags")
-        self.assertEqual(tags.keys(), ["tryme"])
-        tag = self.repo['refs/tags/tryme']
+        tags = self.repo.refs.as_dict(b"refs/tags")
+        self.assertEqual(list(tags.keys()), [b"tryme"])
+        tag = self.repo[b'refs/tags/tryme']
         self.assertTrue(isinstance(tag, Tag))
-        self.assertEqual("foo <foo@bar.com>", tag.tagger)
-        self.assertEqual("bar", tag.message)
+        self.assertEqual(b"foo <foo@bar.com>", tag.tagger)
+        self.assertEqual(b"bar", tag.message)
 
     def test_unannotated(self):
         c1, c2, c3 = build_commit_graph(self.repo.object_store, [[1], [2, 1],
             [3, 1, 2]])
-        self.repo.refs["HEAD"] = c3.id
+        self.repo.refs[b"HEAD"] = c3.id
 
-        porcelain.tag_create(self.repo.path, "tryme", annotated=False)
+        porcelain.tag_create(self.repo.path, b"tryme", annotated=False)
 
-        tags = self.repo.refs.as_dict("refs/tags")
-        self.assertEqual(tags.keys(), ["tryme"])
-        self.repo['refs/tags/tryme']
-        self.assertEqual(tags.values(), [self.repo.head()])
+        tags = self.repo.refs.as_dict(b"refs/tags")
+        self.assertEqual(list(tags.keys()), [b"tryme"])
+        self.repo[b'refs/tags/tryme']
+        self.assertEqual(list(tags.values()), [self.repo.head()])
 
 
-@skipIfPY3
 class TagListTests(PorcelainTestCase):
 
     def test_empty(self):
@@ -402,50 +388,47 @@ class TagListTests(PorcelainTestCase):
         self.assertEqual([], tags)
 
     def test_simple(self):
-        self.repo.refs["refs/tags/foo"] = "aa" * 20
-        self.repo.refs["refs/tags/bar/bla"] = "bb" * 20
+        self.repo.refs[b"refs/tags/foo"] = b"aa" * 20
+        self.repo.refs[b"refs/tags/bar/bla"] = b"bb" * 20
         tags = porcelain.tag_list(self.repo.path)
 
-        self.assertEqual(["bar/bla", "foo"], tags)
+        self.assertEqual([b"bar/bla", b"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))
+        self.repo[b"HEAD"] = c1.id
+        porcelain.tag_create(self.repo, b'foo')
+        self.assertTrue(b"foo" in porcelain.tag_list(self.repo))
+        porcelain.tag_delete(self.repo, b'foo')
+        self.assertFalse(b"foo" in porcelain.tag_list(self.repo))
 
 
-@skipIfPY3
 class ResetTests(PorcelainTestCase):
 
     def test_hard_head(self):
         with open(os.path.join(self.repo.path, 'foo'), 'w') as f:
             f.write("BAR")
         porcelain.add(self.repo.path, paths=["foo"])
-        porcelain.commit(self.repo.path, message="Some message",
-                committer="Jane <jane@example.com>",
-                author="John <john@example.com>")
+        porcelain.commit(self.repo.path, message=b"Some message",
+                committer=b"Jane <jane@example.com>",
+                author=b"John <john@example.com>")
 
-        with open(os.path.join(self.repo.path, 'foo'), 'w') as f:
-            f.write("OOH")
+        with open(os.path.join(self.repo.path, 'foo'), 'wb') as f:
+            f.write(b"OOH")
 
-        porcelain.reset(self.repo, "hard", "HEAD")
+        porcelain.reset(self.repo, "hard", b"HEAD")
 
         index = self.repo.open_index()
         changes = list(tree_changes(self.repo,
                        index.commit(self.repo.object_store),
-                       self.repo['HEAD'].tree))
+                       self.repo[b'HEAD'].tree))
 
         self.assertEqual([], changes)
 
 
-@skipIfPY3
 class PushTests(PorcelainTestCase):
 
     def test_simple(self):
@@ -457,22 +440,22 @@ class PushTests(PorcelainTestCase):
         outstream = BytesIO()
         errstream = BytesIO()
 
-        porcelain.commit(repo=self.repo.path, message='init',
-            author='', committer='')
+        porcelain.commit(repo=self.repo.path, message=b'init',
+            author=b'', committer=b'')
 
         # Setup target repo cloned from temp test repo
         clone_path = tempfile.mkdtemp()
-        porcelain.clone(self.repo.path, target=clone_path, outstream=outstream)
+        porcelain.clone(self.repo.path, target=clone_path, errstream=errstream)
 
         # create a second file to be pushed back to origin
         handle, fullpath = tempfile.mkstemp(dir=clone_path)
         porcelain.add(repo=clone_path, paths=[os.path.basename(fullpath)])
-        porcelain.commit(repo=clone_path, message='push',
-            author='', committer='')
+        porcelain.commit(repo=clone_path, message=b'push',
+            author=b'', committer=b'')
 
         # Setup a non-checked out branch in the remote
-        refs_path = os.path.join('refs', 'heads', 'foo')
-        self.repo[refs_path] = self.repo['HEAD']
+        refs_path = b"refs/heads/foo"
+        self.repo[refs_path] = self.repo[b'HEAD']
 
         # Push to the remote
         porcelain.push(clone_path, self.repo.path, refs_path, outstream=outstream,
@@ -483,14 +466,13 @@ class PushTests(PorcelainTestCase):
 
         # Get the change in the target repo corresponding to the add
         # this will be in the foo branch.
-        change = list(tree_changes(self.repo, self.repo['HEAD'].tree,
-                                   self.repo['refs/heads/foo'].tree))[0]
+        change = list(tree_changes(self.repo, self.repo[b'HEAD'].tree,
+                                   self.repo[b'refs/heads/foo'].tree))[0]
 
-        self.assertEqual(r_clone['HEAD'].id, self.repo[refs_path].id)
-        self.assertEqual(os.path.basename(fullpath), change.new.path)
+        self.assertEqual(r_clone[b'HEAD'].id, self.repo[refs_path].id)
+        self.assertEqual(os.path.basename(fullpath), change.new.path.decode('ascii'))
 
 
-@skipIfPY3
 class PullTests(PorcelainTestCase):
 
     def test_simple(self):
@@ -501,30 +483,29 @@ class PullTests(PorcelainTestCase):
         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')
+        porcelain.commit(repo=self.repo.path, message=b'test',
+                         author=b'test', committer=b'test')
 
         # Setup target repo
         target_path = tempfile.mkdtemp()
-        porcelain.clone(self.repo.path, target=target_path, outstream=outstream)
+        porcelain.clone(self.repo.path, target=target_path, errstream=errstream)
 
         # 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')
+        porcelain.commit(repo=self.repo.path, message=b'test2',
+            author=b'test2', committer=b'test2')
 
         # Pull changes into the cloned repo
-        porcelain.pull(target_path, self.repo.path, 'refs/heads/master',
+        porcelain.pull(target_path, self.repo.path, b'refs/heads/master',
             outstream=outstream, errstream=errstream)
 
         # Check the target repo for pushed changes
         r = Repo(target_path)
-        self.assertEqual(r['HEAD'].id, self.repo['HEAD'].id)
+        self.assertEqual(r[b'HEAD'].id, self.repo[b'HEAD'].id)
 
 
-@skipIfPY3
 class StatusTests(PorcelainTestCase):
 
     def test_status(self):
@@ -536,14 +517,14 @@ class StatusTests(PorcelainTestCase):
             f.write('origstuff')
 
         porcelain.add(repo=self.repo.path, paths=['foo'])
-        porcelain.commit(repo=self.repo.path, message='test status',
-            author='', committer='')
+        porcelain.commit(repo=self.repo.path, message=b'test status',
+            author=b'', committer=b'')
 
         # modify access and modify time of path
         os.utime(fullpath, (0, 0))
 
-        with open(fullpath, 'w') as f:
-            f.write('stuff')
+        with open(fullpath, 'wb') as f:
+            f.write(b'stuff')
 
         # Make a dummy file and stage it
         filename_add = 'bar'
@@ -554,8 +535,8 @@ class StatusTests(PorcelainTestCase):
 
         results = porcelain.status(self.repo)
 
-        self.assertEqual(results.staged['add'][0], filename_add)
-        self.assertEqual(results.unstaged, ['foo'])
+        self.assertEqual(results.staged['add'][0], filename_add.encode('ascii'))
+        self.assertEqual(results.unstaged, [b'foo'])
 
     def test_get_tree_changes_add(self):
         """Unit test for get_tree_changes add."""
@@ -565,8 +546,8 @@ class StatusTests(PorcelainTestCase):
         with open(os.path.join(self.repo.path, filename), 'w') as f:
             f.write('stuff')
         porcelain.add(repo=self.repo.path, paths=filename)
-        porcelain.commit(repo=self.repo.path, message='test status',
-            author='', committer='')
+        porcelain.commit(repo=self.repo.path, message=b'test status',
+            author=b'', committer=b'')
 
         filename = 'foo'
         with open(os.path.join(self.repo.path, filename), 'w') as f:
@@ -574,7 +555,7 @@ class StatusTests(PorcelainTestCase):
         porcelain.add(repo=self.repo.path, paths=filename)
         changes = porcelain.get_tree_changes(self.repo.path)
 
-        self.assertEqual(changes['add'][0], filename)
+        self.assertEqual(changes['add'][0], filename.encode('ascii'))
         self.assertEqual(len(changes['add']), 1)
         self.assertEqual(len(changes['modify']), 0)
         self.assertEqual(len(changes['delete']), 0)
@@ -588,14 +569,14 @@ class StatusTests(PorcelainTestCase):
         with open(fullpath, 'w') as f:
             f.write('stuff')
         porcelain.add(repo=self.repo.path, paths=filename)
-        porcelain.commit(repo=self.repo.path, message='test status',
-            author='', committer='')
+        porcelain.commit(repo=self.repo.path, message=b'test status',
+            author=b'', committer=b'')
         with open(fullpath, 'w') as f:
             f.write('otherstuff')
         porcelain.add(repo=self.repo.path, paths=filename)
         changes = porcelain.get_tree_changes(self.repo.path)
 
-        self.assertEqual(changes['modify'][0], filename)
+        self.assertEqual(changes['modify'][0], filename.encode('ascii'))
         self.assertEqual(len(changes['add']), 0)
         self.assertEqual(len(changes['modify']), 1)
         self.assertEqual(len(changes['delete']), 0)
@@ -608,12 +589,12 @@ class StatusTests(PorcelainTestCase):
         with open(os.path.join(self.repo.path, filename), 'w') as f:
             f.write('stuff')
         porcelain.add(repo=self.repo.path, paths=filename)
-        porcelain.commit(repo=self.repo.path, message='test status',
-            author='', committer='')
+        porcelain.commit(repo=self.repo.path, message=b'test status',
+            author=b'', committer=b'')
         porcelain.rm(repo=self.repo.path, paths=[filename])
         changes = porcelain.get_tree_changes(self.repo.path)
 
-        self.assertEqual(changes['delete'][0], filename)
+        self.assertEqual(changes['delete'][0], filename.encode('ascii'))
         self.assertEqual(len(changes['add']), 0)
         self.assertEqual(len(changes['modify']), 0)
         self.assertEqual(len(changes['delete']), 1)
@@ -622,19 +603,17 @@ class StatusTests(PorcelainTestCase):
 # TODO(jelmer): Add test for dulwich.porcelain.daemon
 
 
-@skipIfPY3
 class UploadPackTests(PorcelainTestCase):
     """Tests for upload_pack."""
 
     def test_upload_pack(self):
         outf = BytesIO()
-        exitcode = porcelain.upload_pack(self.repo.path, BytesIO("0000"), outf)
+        exitcode = porcelain.upload_pack(self.repo.path, BytesIO(b"0000"), outf)
         outlines = outf.getvalue().splitlines()
-        self.assertEqual(["0000"], outlines)
+        self.assertEqual([b"0000"], outlines)
         self.assertEqual(0, exitcode)
 
 
-@skipIfPY3
 class ReceivePackTests(PorcelainTestCase):
     """Tests for receive_pack."""
 
@@ -643,21 +622,20 @@ class ReceivePackTests(PorcelainTestCase):
         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,
+        self.repo.do_commit(message=b'test status',
+            author=b'', committer=b'', author_timestamp=1402354300,
             commit_timestamp=1402354300, author_timezone=0, commit_timezone=0)
         outf = BytesIO()
-        exitcode = porcelain.receive_pack(self.repo.path, BytesIO("0000"), outf)
+        exitcode = porcelain.receive_pack(self.repo.path, BytesIO(b"0000"), outf)
         outlines = outf.getvalue().splitlines()
         self.assertEqual([
-            '005a9e65bdcf4a22cdd4f3700604a275cd2aaf146b23 HEAD\x00report-status '
-            'delete-refs side-band-64k',
-            '003f9e65bdcf4a22cdd4f3700604a275cd2aaf146b23 refs/heads/master',
-            '0000'], outlines)
+            b'005a9e65bdcf4a22cdd4f3700604a275cd2aaf146b23 HEAD\x00report-status '
+            b'delete-refs side-band-64k',
+            b'003f9e65bdcf4a22cdd4f3700604a275cd2aaf146b23 refs/heads/master',
+            b'0000'], outlines)
         self.assertEqual(0, exitcode)
 
 
-@skipIfPY3
 class BranchListTests(PorcelainTestCase):
 
     def test_standard(self):
@@ -665,45 +643,42 @@ class BranchListTests(PorcelainTestCase):
 
     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.repo[b"HEAD"] = c1.id
+        porcelain.branch_create(self.repo, b"foo")
         self.assertEqual(
-            set(["master", "foo"]),
+            set([b"master", b"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)
+        self.repo[b"HEAD"] = c1.id
+        porcelain.branch_create(self.repo, b"foo")
+        self.assertRaises(KeyError, porcelain.branch_create, self.repo, b"foo")
+        porcelain.branch_create(self.repo, b"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.repo[b"HEAD"] = c1.id
+        porcelain.branch_create(self.repo, b"foo")
         self.assertEqual(
-            set(["master", "foo"]),
+            set([b"master", b"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))
+        self.repo[b"HEAD"] = c1.id
+        porcelain.branch_create(self.repo, b'foo')
+        self.assertTrue(b"foo" in porcelain.branch_list(self.repo))
+        porcelain.branch_delete(self.repo, b'foo')
+        self.assertFalse(b"foo" in porcelain.branch_list(self.repo))
 
 
-@skipIfPY3
 class FetchTests(PorcelainTestCase):
 
     def test_simple(self):
@@ -714,22 +689,22 @@ class FetchTests(PorcelainTestCase):
         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')
+        porcelain.commit(repo=self.repo.path, message=b'test',
+                         author=b'test', committer=b'test')
 
         # Setup target repo
         target_path = tempfile.mkdtemp()
         target_repo = porcelain.clone(self.repo.path, target=target_path,
-            outstream=outstream)
+            errstream=errstream)
 
         # 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')
+        porcelain.commit(repo=self.repo.path, message=b'test2',
+            author=b'test2', committer=b'test2')
 
-        self.assertFalse(self.repo['HEAD'].id in target_repo)
+        self.assertFalse(self.repo[b'HEAD'].id in target_repo)
 
         # Fetch changes into the cloned repo
         porcelain.fetch(target_path, self.repo.path, outstream=outstream,
@@ -737,4 +712,4 @@ class FetchTests(PorcelainTestCase):
 
         # Check the target repo for pushed changes
         r = Repo(target_path)
-        self.assertTrue(self.repo['HEAD'].id in r)
+        self.assertTrue(self.repo[b'HEAD'].id in r)

+ 85 - 87
dulwich/tests/test_protocol.py

@@ -25,6 +25,7 @@ from dulwich.errors import (
     HangupException,
     )
 from dulwich.protocol import (
+    GitProtocolError,
     PktLineParser,
     Protocol,
     ReceivableProtocol,
@@ -37,26 +38,25 @@ from dulwich.protocol import (
     BufferedPktLineWriter,
     )
 from dulwich.tests import TestCase
-from dulwich.tests.utils import skipIfPY3
 
 
 class BaseProtocolTests(object):
 
     def test_write_pkt_line_none(self):
         self.proto.write_pkt_line(None)
-        self.assertEqual(self.rout.getvalue(), '0000')
+        self.assertEqual(self.rout.getvalue(), b'0000')
 
     def test_write_pkt_line(self):
-        self.proto.write_pkt_line('bla')
-        self.assertEqual(self.rout.getvalue(), '0007bla')
+        self.proto.write_pkt_line(b'bla')
+        self.assertEqual(self.rout.getvalue(), b'0007bla')
 
     def test_read_pkt_line(self):
-        self.rin.write('0008cmd ')
+        self.rin.write(b'0008cmd ')
         self.rin.seek(0)
-        self.assertEqual('cmd ', self.proto.read_pkt_line())
+        self.assertEqual(b'cmd ', self.proto.read_pkt_line())
 
     def test_eof(self):
-        self.rin.write('0000')
+        self.rin.write(b'0000')
         self.rin.seek(0)
         self.assertFalse(self.proto.eof())
         self.assertEqual(None, self.proto.read_pkt_line())
@@ -64,50 +64,49 @@ class BaseProtocolTests(object):
         self.assertRaises(HangupException, self.proto.read_pkt_line)
 
     def test_unread_pkt_line(self):
-        self.rin.write('0007foo0000')
+        self.rin.write(b'0007foo0000')
         self.rin.seek(0)
-        self.assertEqual('foo', self.proto.read_pkt_line())
-        self.proto.unread_pkt_line('bar')
-        self.assertEqual('bar', self.proto.read_pkt_line())
+        self.assertEqual(b'foo', self.proto.read_pkt_line())
+        self.proto.unread_pkt_line(b'bar')
+        self.assertEqual(b'bar', self.proto.read_pkt_line())
         self.assertEqual(None, self.proto.read_pkt_line())
-        self.proto.unread_pkt_line('baz1')
-        self.assertRaises(ValueError, self.proto.unread_pkt_line, 'baz2')
+        self.proto.unread_pkt_line(b'baz1')
+        self.assertRaises(ValueError, self.proto.unread_pkt_line, b'baz2')
 
     def test_read_pkt_seq(self):
-        self.rin.write('0008cmd 0005l0000')
+        self.rin.write(b'0008cmd 0005l0000')
         self.rin.seek(0)
-        self.assertEqual(['cmd ', 'l'], list(self.proto.read_pkt_seq()))
+        self.assertEqual([b'cmd ', b'l'], list(self.proto.read_pkt_seq()))
 
     def test_read_pkt_line_none(self):
-        self.rin.write('0000')
+        self.rin.write(b'0000')
         self.rin.seek(0)
         self.assertEqual(None, self.proto.read_pkt_line())
 
     def test_read_pkt_line_wrong_size(self):
-        self.rin.write('0100too short')
+        self.rin.write(b'0100too short')
         self.rin.seek(0)
-        self.assertRaises(AssertionError, self.proto.read_pkt_line)
+        self.assertRaises(GitProtocolError, self.proto.read_pkt_line)
 
     def test_write_sideband(self):
-        self.proto.write_sideband(3, 'bloe')
-        self.assertEqual(self.rout.getvalue(), '0009\x03bloe')
+        self.proto.write_sideband(3, b'bloe')
+        self.assertEqual(self.rout.getvalue(), b'0009\x03bloe')
 
     def test_send_cmd(self):
-        self.proto.send_cmd('fetch', 'a', 'b')
-        self.assertEqual(self.rout.getvalue(), '000efetch a\x00b\x00')
+        self.proto.send_cmd(b'fetch', b'a', b'b')
+        self.assertEqual(self.rout.getvalue(), b'000efetch a\x00b\x00')
 
     def test_read_cmd(self):
-        self.rin.write('0012cmd arg1\x00arg2\x00')
+        self.rin.write(b'0012cmd arg1\x00arg2\x00')
         self.rin.seek(0)
-        self.assertEqual(('cmd', ['arg1', 'arg2']), self.proto.read_cmd())
+        self.assertEqual((b'cmd', [b'arg1', b'arg2']), self.proto.read_cmd())
 
     def test_read_cmd_noend0(self):
-        self.rin.write('0011cmd arg1\x00arg2')
+        self.rin.write(b'0011cmd arg1\x00arg2')
         self.rin.seek(0)
         self.assertRaises(AssertionError, self.proto.read_cmd)
 
 
-@skipIfPY3
 class ProtocolTests(BaseProtocolTests, TestCase):
 
     def setUp(self):
@@ -128,14 +127,13 @@ class ReceivableBytesIO(BytesIO):
         # fail fast if no bytes are available; in a real socket, this would
         # block forever
         if self.tell() == len(self.getvalue()) and not self.allow_read_past_eof:
-            raise AssertionError('Blocking read past end of socket')
+            raise GitProtocolError('Blocking read past end of socket')
         if size == 1:
             return self.read(1)
         # calls shouldn't return quite as much as asked for
         return self.read(size - 1)
 
 
-@skipIfPY3
 class ReceivableProtocolTests(BaseProtocolTests, TestCase):
 
     def setUp(self):
@@ -153,41 +151,41 @@ class ReceivableProtocolTests(BaseProtocolTests, TestCase):
         BaseProtocolTests.test_eof(self)
 
     def test_recv(self):
-        all_data = '1234567' * 10  # not a multiple of bufsize
+        all_data = b'1234567' * 10  # not a multiple of bufsize
         self.rin.write(all_data)
         self.rin.seek(0)
-        data = ''
+        data = b''
         # We ask for 8 bytes each time and actually read 7, so it should take
         # exactly 10 iterations.
         for _ in range(10):
             data += self.proto.recv(10)
         # any more reads would block
-        self.assertRaises(AssertionError, self.proto.recv, 10)
+        self.assertRaises(GitProtocolError, self.proto.recv, 10)
         self.assertEqual(all_data, data)
 
     def test_recv_read(self):
-        all_data = '1234567'  # recv exactly in one call
+        all_data = b'1234567'  # recv exactly in one call
         self.rin.write(all_data)
         self.rin.seek(0)
-        self.assertEqual('1234', self.proto.recv(4))
-        self.assertEqual('567', self.proto.read(3))
-        self.assertRaises(AssertionError, self.proto.recv, 10)
+        self.assertEqual(b'1234', self.proto.recv(4))
+        self.assertEqual(b'567', self.proto.read(3))
+        self.assertRaises(GitProtocolError, self.proto.recv, 10)
 
     def test_read_recv(self):
-        all_data = '12345678abcdefg'
+        all_data = b'12345678abcdefg'
         self.rin.write(all_data)
         self.rin.seek(0)
-        self.assertEqual('1234', self.proto.read(4))
-        self.assertEqual('5678abc', self.proto.recv(8))
-        self.assertEqual('defg', self.proto.read(4))
-        self.assertRaises(AssertionError, self.proto.recv, 10)
+        self.assertEqual(b'1234', self.proto.read(4))
+        self.assertEqual(b'5678abc', self.proto.recv(8))
+        self.assertEqual(b'defg', self.proto.read(4))
+        self.assertRaises(GitProtocolError, self.proto.recv, 10)
 
     def test_mixed(self):
         # arbitrary non-repeating string
-        all_data = ','.join(str(i) for i in range(100))
+        all_data = b','.join(str(i).encode('ascii') for i in range(100))
         self.rin.write(all_data)
         self.rin.seek(0)
-        data = ''
+        data = b''
 
         for i in range(1, 100):
             data += self.proto.recv(i)
@@ -207,37 +205,38 @@ class ReceivableProtocolTests(BaseProtocolTests, TestCase):
         self.assertEqual(all_data, data)
 
 
-@skipIfPY3
 class CapabilitiesTestCase(TestCase):
 
     def test_plain(self):
-        self.assertEqual(('bla', []), extract_capabilities('bla'))
+        self.assertEqual((b'bla', []), extract_capabilities(b'bla'))
 
     def test_caps(self):
-        self.assertEqual(('bla', ['la']), extract_capabilities('bla\0la'))
-        self.assertEqual(('bla', ['la']), extract_capabilities('bla\0la\n'))
-        self.assertEqual(('bla', ['la', 'la']), extract_capabilities('bla\0la la'))
+        self.assertEqual((b'bla', [b'la']), extract_capabilities(b'bla\0la'))
+        self.assertEqual((b'bla', [b'la']), extract_capabilities(b'bla\0la\n'))
+        self.assertEqual((b'bla', [b'la', b'la']), extract_capabilities(b'bla\0la la'))
 
     def test_plain_want_line(self):
-        self.assertEqual(('want bla', []), extract_want_line_capabilities('want bla'))
+        self.assertEqual((b'want bla', []), extract_want_line_capabilities(b'want bla'))
 
     def test_caps_want_line(self):
-        self.assertEqual(('want bla', ['la']), extract_want_line_capabilities('want bla la'))
-        self.assertEqual(('want bla', ['la']), extract_want_line_capabilities('want bla la\n'))
-        self.assertEqual(('want bla', ['la', 'la']), extract_want_line_capabilities('want bla la la'))
+        self.assertEqual((b'want bla', [b'la']),
+                extract_want_line_capabilities(b'want bla la'))
+        self.assertEqual((b'want bla', [b'la']),
+                extract_want_line_capabilities(b'want bla la\n'))
+        self.assertEqual((b'want bla', [b'la', b'la']),
+                extract_want_line_capabilities(b'want bla la la'))
 
     def test_ack_type(self):
-        self.assertEqual(SINGLE_ACK, ack_type(['foo', 'bar']))
-        self.assertEqual(MULTI_ACK, ack_type(['foo', 'bar', 'multi_ack']))
+        self.assertEqual(SINGLE_ACK, ack_type([b'foo', b'bar']))
+        self.assertEqual(MULTI_ACK, ack_type([b'foo', b'bar', b'multi_ack']))
         self.assertEqual(MULTI_ACK_DETAILED,
-                          ack_type(['foo', 'bar', 'multi_ack_detailed']))
+                          ack_type([b'foo', b'bar', b'multi_ack_detailed']))
         # choose detailed when both present
         self.assertEqual(MULTI_ACK_DETAILED,
-                          ack_type(['foo', 'bar', 'multi_ack',
-                                    'multi_ack_detailed']))
+                          ack_type([b'foo', b'bar', b'multi_ack',
+                                    b'multi_ack_detailed']))
 
 
-@skipIfPY3
 class BufferedPktLineWriterTests(TestCase):
 
     def setUp(self):
@@ -253,68 +252,67 @@ class BufferedPktLineWriterTests(TestCase):
         self._output.truncate()
 
     def test_write(self):
-        self._writer.write('foo')
-        self.assertOutputEquals('')
+        self._writer.write(b'foo')
+        self.assertOutputEquals(b'')
         self._writer.flush()
-        self.assertOutputEquals('0007foo')
+        self.assertOutputEquals(b'0007foo')
 
     def test_write_none(self):
         self._writer.write(None)
-        self.assertOutputEquals('')
+        self.assertOutputEquals(b'')
         self._writer.flush()
-        self.assertOutputEquals('0000')
+        self.assertOutputEquals(b'0000')
 
     def test_flush_empty(self):
         self._writer.flush()
-        self.assertOutputEquals('')
+        self.assertOutputEquals(b'')
 
     def test_write_multiple(self):
-        self._writer.write('foo')
-        self._writer.write('bar')
-        self.assertOutputEquals('')
+        self._writer.write(b'foo')
+        self._writer.write(b'bar')
+        self.assertOutputEquals(b'')
         self._writer.flush()
-        self.assertOutputEquals('0007foo0007bar')
+        self.assertOutputEquals(b'0007foo0007bar')
 
     def test_write_across_boundary(self):
-        self._writer.write('foo')
-        self._writer.write('barbaz')
-        self.assertOutputEquals('0007foo000abarba')
+        self._writer.write(b'foo')
+        self._writer.write(b'barbaz')
+        self.assertOutputEquals(b'0007foo000abarba')
         self._truncate()
         self._writer.flush()
-        self.assertOutputEquals('z')
+        self.assertOutputEquals(b'z')
 
     def test_write_to_boundary(self):
-        self._writer.write('foo')
-        self._writer.write('barba')
-        self.assertOutputEquals('0007foo0009barba')
+        self._writer.write(b'foo')
+        self._writer.write(b'barba')
+        self.assertOutputEquals(b'0007foo0009barba')
         self._truncate()
-        self._writer.write('z')
+        self._writer.write(b'z')
         self._writer.flush()
-        self.assertOutputEquals('0005z')
+        self.assertOutputEquals(b'0005z')
 
 
-@skipIfPY3
 class PktLineParserTests(TestCase):
 
     def test_none(self):
         pktlines = []
         parser = PktLineParser(pktlines.append)
-        parser.parse("0000")
+        parser.parse(b"0000")
         self.assertEqual(pktlines, [None])
-        self.assertEqual("", parser.get_tail())
+        self.assertEqual(b"", parser.get_tail())
 
     def test_small_fragments(self):
         pktlines = []
         parser = PktLineParser(pktlines.append)
-        parser.parse("00")
-        parser.parse("05")
-        parser.parse("z0000")
-        self.assertEqual(pktlines, ["z", None])
-        self.assertEqual("", parser.get_tail())
+        parser.parse(b"00")
+        parser.parse(b"05")
+        parser.parse(b"z0000")
+        self.assertEqual(pktlines, [b"z", None])
+        self.assertEqual(b"", parser.get_tail())
 
     def test_multiple_packets(self):
         pktlines = []
         parser = PktLineParser(pktlines.append)
-        parser.parse("0005z0006aba")
-        self.assertEqual(pktlines, ["z", "ab"])
-        self.assertEqual("a", parser.get_tail())
+        parser.parse(b"0005z0006aba")
+        self.assertEqual(pktlines, [b"z", b"ab"])
+        self.assertEqual(b"a", parser.get_tail())

+ 10 - 12
dulwich/tests/test_refs.py

@@ -45,7 +45,6 @@ from dulwich.tests import (
 from dulwich.tests.utils import (
     open_repo,
     tear_down_repo,
-    skipIfPY3,
     )
 
 
@@ -309,7 +308,7 @@ class DiskRefsContainerTests(RefsContainerTests, TestCase):
 
     def test_setitem(self):
         RefsContainerTests.test_setitem(self)
-        f = open(os.path.join(self._refs.path, 'refs', 'some', 'ref'), 'rb')
+        f = open(os.path.join(self._refs.path, b'refs', b'some', b'ref'), 'rb')
         self.assertEqual(b'42d06bd4b77fed026b154d16493e5deab78f02ec',
                          f.read()[:40])
         f.close()
@@ -320,12 +319,12 @@ class DiskRefsContainerTests(RefsContainerTests, TestCase):
         self.assertEqual(ones, self._refs[b'HEAD'])
 
         # ensure HEAD was not modified
-        f = open(os.path.join(self._refs.path, 'HEAD'), 'rb')
+        f = open(os.path.join(self._refs.path, b'HEAD'), 'rb')
         self.assertEqual(b'ref: refs/heads/master', next(iter(f)).rstrip(b'\n'))
         f.close()
 
         # ensure the symbolic link was written through
-        f = open(os.path.join(self._refs.path, 'refs', 'heads', 'master'), 'rb')
+        f = open(os.path.join(self._refs.path, b'refs', b'heads', b'master'), 'rb')
         self.assertEqual(ones, f.read()[:40])
         f.close()
 
@@ -337,9 +336,9 @@ class DiskRefsContainerTests(RefsContainerTests, TestCase):
 
         # ensure lockfile was deleted
         self.assertFalse(os.path.exists(
-            os.path.join(self._refs.path, 'refs', 'heads', 'master.lock')))
+            os.path.join(self._refs.path, b'refs', b'heads', b'master.lock')))
         self.assertFalse(os.path.exists(
-            os.path.join(self._refs.path, 'HEAD.lock')))
+            os.path.join(self._refs.path, b'HEAD.lock')))
 
     def test_add_if_new_packed(self):
         # don't overwrite packed ref
@@ -348,7 +347,6 @@ class DiskRefsContainerTests(RefsContainerTests, TestCase):
         self.assertEqual(b'df6800012397fb85c56e7418dd4eb9405dee075c',
                          self._refs[b'refs/tags/refs-0.1'])
 
-    @skipIfPY3
     def test_add_if_new_symbolic(self):
         # Use an empty repo instead of the default.
         tear_down_repo(self._repo)
@@ -379,7 +377,7 @@ class DiskRefsContainerTests(RefsContainerTests, TestCase):
 
     def test_delitem(self):
         RefsContainerTests.test_delitem(self)
-        ref_file = os.path.join(self._refs.path, 'refs', 'heads', 'master')
+        ref_file = os.path.join(self._refs.path, b'refs', b'heads', b'master')
         self.assertFalse(os.path.exists(ref_file))
         self.assertFalse(b'refs/heads/master' in self._refs.get_packed_refs())
 
@@ -390,7 +388,7 @@ class DiskRefsContainerTests(RefsContainerTests, TestCase):
         self.assertRaises(KeyError, lambda: self._refs[b'HEAD'])
         self.assertEqual(b'42d06bd4b77fed026b154d16493e5deab78f02ec',
                          self._refs[b'refs/heads/master'])
-        self.assertFalse(os.path.exists(os.path.join(self._refs.path, 'HEAD')))
+        self.assertFalse(os.path.exists(os.path.join(self._refs.path, b'HEAD')))
 
     def test_remove_if_equals_symref(self):
         # HEAD is a symref, so shouldn't equal its dereferenced value
@@ -406,12 +404,12 @@ class DiskRefsContainerTests(RefsContainerTests, TestCase):
                          self._refs.read_loose_ref(b'HEAD'))
 
         self.assertFalse(os.path.exists(
-            os.path.join(self._refs.path, 'refs', 'heads', 'master.lock')))
+            os.path.join(self._refs.path, b'refs', b'heads', b'master.lock')))
         self.assertFalse(os.path.exists(
-            os.path.join(self._refs.path, 'HEAD.lock')))
+            os.path.join(self._refs.path, b'HEAD.lock')))
 
     def test_remove_packed_without_peeled(self):
-        refs_file = os.path.join(self._repo.path, 'packed-refs')
+        refs_file = os.path.join(self._repo._controldir, b'packed-refs')
         f = GitFile(refs_file)
         refs_data = f.read()
         f.close()

+ 312 - 235
dulwich/tests/test_repository.py

@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
 # test_repository.py -- tests for repository.py
 # Copyright (C) 2007 James Westby <jw+debian@jameswestby.net>
 #
@@ -22,8 +23,10 @@
 import os
 import stat
 import shutil
+import sys
 import tempfile
 import warnings
+import sys
 
 from dulwich import errors
 from dulwich.object_store import (
@@ -42,13 +45,28 @@ from dulwich.tests.utils import (
     open_repo,
     tear_down_repo,
     setup_warning_catcher,
-    skipIfPY3,
     )
 
-missing_sha = 'b91fa4d900e17e99b433218e988c4eb4a3e9a097'
+missing_sha = b'b91fa4d900e17e99b433218e988c4eb4a3e9a097'
+
+
+def mkdtemp_bytes():
+    tmp_dir = tempfile.mkdtemp()
+    if sys.version_info[0] > 2:
+        tmp_dir = tmp_dir.encode(sys.getfilesystemencoding())
+    return tmp_dir
+
+def mkdtemp_unicode():
+    suffix = u'déłwíçh'
+    if sys.version_info[0] == 2:
+        suffix = suffix.encode(sys.getfilesystemencoding())
+    tmp_dir = tempfile.mkdtemp(suffix=suffix)
+    if sys.version_info[0] == 2:
+        tmp_dir = tmp_dir.decode(sys.getfilesystemencoding())
+    return tmp_dir
+
 
 
-@skipIfPY3
 class CreateRepositoryTests(TestCase):
 
     def assertFileContentsEqual(self, expected, repo, path):
@@ -61,61 +79,86 @@ class CreateRepositoryTests(TestCase):
 
     def _check_repo_contents(self, repo, expect_bare):
         self.assertEqual(expect_bare, repo.bare)
-        self.assertFileContentsEqual('Unnamed repository', repo, 'description')
-        self.assertFileContentsEqual('', repo, os.path.join('info', 'exclude'))
-        self.assertFileContentsEqual(None, repo, 'nonexistent file')
-        barestr = 'bare = %s' % str(expect_bare).lower()
-        config_text = repo.get_named_file('config').read()
-        self.assertTrue(barestr in config_text, "%r" % config_text)
+        self.assertFileContentsEqual(b'Unnamed repository', repo, b'description')
+        self.assertFileContentsEqual(b'', repo, os.path.join(b'info', b'exclude'))
+        self.assertFileContentsEqual(None, repo, b'nonexistent file')
+        barestr = b'bare = ' + str(expect_bare).lower().encode('ascii')
+        with repo.get_named_file(b'config') as f:
+            config_text = f.read()
+            self.assertTrue(barestr in config_text, "%r" % config_text)
+
+
+class CreateMemoryRepositoryTests(CreateRepositoryTests):
+
+    def test_create_memory(self):
+        repo = MemoryRepo.init_bare([], {})
+        self._check_repo_contents(repo, True)
+
+
+class CreateRepositoryBytesRootTests(CreateRepositoryTests):
+
+    def mkdtemp(self):
+        tmp_dir = mkdtemp_bytes()
+        return tmp_dir, tmp_dir
 
     def test_create_disk_bare(self):
-        tmp_dir = tempfile.mkdtemp()
+        tmp_dir, tmp_dir_bytes = self.mkdtemp()
         self.addCleanup(shutil.rmtree, tmp_dir)
         repo = Repo.init_bare(tmp_dir)
-        self.assertEqual(tmp_dir, repo._controldir)
+        self.assertEqual(tmp_dir_bytes, repo._controldir)
         self._check_repo_contents(repo, True)
 
     def test_create_disk_non_bare(self):
-        tmp_dir = tempfile.mkdtemp()
+        tmp_dir, tmp_dir_bytes = self.mkdtemp()
         self.addCleanup(shutil.rmtree, tmp_dir)
         repo = Repo.init(tmp_dir)
-        self.assertEqual(os.path.join(tmp_dir, '.git'), repo._controldir)
+        self.assertEqual(os.path.join(tmp_dir_bytes, b'.git'), repo._controldir)
         self._check_repo_contents(repo, False)
 
-    def test_create_memory(self):
-        repo = MemoryRepo.init_bare([], {})
-        self._check_repo_contents(repo, True)
 
+class CreateRepositoryUnicodeRootTests(CreateRepositoryBytesRootTests):
+
+    def mktemp(self):
+        tmp_dir = mkdtemp_unicode()
+        tmp_dir_bytes = tmp_dir.encode(sys.getfilesystemencoding())
+        return tmp_dir, tmp_dir_bytes
 
-@skipIfPY3
-class RepositoryTests(TestCase):
+
+class RepositoryBytesRootTests(TestCase):
 
     def setUp(self):
-        super(RepositoryTests, self).setUp()
+        super(RepositoryBytesRootTests, self).setUp()
         self._repo = None
 
     def tearDown(self):
         if self._repo is not None:
             tear_down_repo(self._repo)
-        super(RepositoryTests, self).tearDown()
+        super(RepositoryBytesRootTests, self).tearDown()
+
+    def mkdtemp(self):
+        return mkdtemp_bytes()
+
+    def open_repo(self, name):
+        temp_dir = self.mkdtemp()
+        return open_repo(name, temp_dir)
 
     def test_simple_props(self):
-        r = self._repo = open_repo('a.git')
-        self.assertEqual(r.controldir(), r.path)
+        r = self._repo = self.open_repo('a.git')
+        self.assertEqual(r.controldir(), r._path_bytes)
 
     def test_setitem(self):
-        r = self._repo = open_repo('a.git')
-        r["refs/tags/foo"] = 'a90fa2d900a17e99b433217e988c4eb4a2e9a097'
-        self.assertEqual('a90fa2d900a17e99b433217e988c4eb4a2e9a097',
-                          r["refs/tags/foo"].id)
+        r = self._repo = self.open_repo('a.git')
+        r[b"refs/tags/foo"] = b'a90fa2d900a17e99b433217e988c4eb4a2e9a097'
+        self.assertEqual(b'a90fa2d900a17e99b433217e988c4eb4a2e9a097',
+                          r[b"refs/tags/foo"].id)
 
     def test_getitem_unicode(self):
-        r = self._repo = open_repo('a.git')
+        r = self._repo = self.open_repo('a.git')
 
         test_keys = [
-            ('refs/heads/master', True),
-            ('a90fa2d900a17e99b433217e988c4eb4a2e9a097', True),
-            ('11' * 19 + '--', False),
+            (b'refs/heads/master', True),
+            (b'a90fa2d900a17e99b433217e988c4eb4a2e9a097', True),
+            (b'11' * 19 + b'--', False),
         ]
 
         for k, contained in test_keys:
@@ -123,135 +166,139 @@ class RepositoryTests(TestCase):
 
         for k, _ in test_keys:
             self.assertRaisesRegexp(
-                TypeError, "'name' must be bytestring, not unicode",
-                r.__getitem__, unicode(k)
+                TypeError, "'name' must be bytestring, not int",
+                r.__getitem__, 12
             )
 
     def test_delitem(self):
-        r = self._repo = open_repo('a.git')
+        r = self._repo = self.open_repo('a.git')
 
-        del r['refs/heads/master']
-        self.assertRaises(KeyError, lambda: r['refs/heads/master'])
+        del r[b'refs/heads/master']
+        self.assertRaises(KeyError, lambda: r[b'refs/heads/master'])
 
-        del r['HEAD']
-        self.assertRaises(KeyError, lambda: r['HEAD'])
+        del r[b'HEAD']
+        self.assertRaises(KeyError, lambda: r[b'HEAD'])
 
-        self.assertRaises(ValueError, r.__delitem__, 'notrefs/foo')
+        self.assertRaises(ValueError, r.__delitem__, b'notrefs/foo')
 
     def test_get_refs(self):
-        r = self._repo = open_repo('a.git')
+        r = self._repo = self.open_repo('a.git')
         self.assertEqual({
-            'HEAD': 'a90fa2d900a17e99b433217e988c4eb4a2e9a097',
-            'refs/heads/master': 'a90fa2d900a17e99b433217e988c4eb4a2e9a097',
-            'refs/tags/mytag': '28237f4dc30d0d462658d6b937b08a0f0b6ef55a',
-            'refs/tags/mytag-packed': 'b0931cadc54336e78a1d980420e3268903b57a50',
+            b'HEAD': b'a90fa2d900a17e99b433217e988c4eb4a2e9a097',
+            b'refs/heads/master': b'a90fa2d900a17e99b433217e988c4eb4a2e9a097',
+            b'refs/tags/mytag': b'28237f4dc30d0d462658d6b937b08a0f0b6ef55a',
+            b'refs/tags/mytag-packed': b'b0931cadc54336e78a1d980420e3268903b57a50',
             }, r.get_refs())
 
     def test_head(self):
-        r = self._repo = open_repo('a.git')
-        self.assertEqual(r.head(), 'a90fa2d900a17e99b433217e988c4eb4a2e9a097')
+        r = self._repo = self.open_repo('a.git')
+        self.assertEqual(r.head(), b'a90fa2d900a17e99b433217e988c4eb4a2e9a097')
 
     def test_get_object(self):
-        r = self._repo = open_repo('a.git')
+        r = self._repo = self.open_repo('a.git')
         obj = r.get_object(r.head())
-        self.assertEqual(obj.type_name, 'commit')
+        self.assertEqual(obj.type_name, b'commit')
 
     def test_get_object_non_existant(self):
-        r = self._repo = open_repo('a.git')
+        r = self._repo = self.open_repo('a.git')
         self.assertRaises(KeyError, r.get_object, missing_sha)
 
     def test_contains_object(self):
-        r = self._repo = open_repo('a.git')
+        r = self._repo = self.open_repo('a.git')
         self.assertTrue(r.head() in r)
 
     def test_contains_ref(self):
-        r = self._repo = open_repo('a.git')
-        self.assertTrue("HEAD" in r)
+        r = self._repo = self.open_repo('a.git')
+        self.assertTrue(b"HEAD" in r)
 
     def test_get_no_description(self):
-        r = self._repo = open_repo('a.git')
+        r = self._repo = self.open_repo('a.git')
         self.assertIs(None, r.get_description())
 
     def test_get_description(self):
-        r = self._repo = open_repo('a.git')
-        with open(os.path.join(r.path, 'description'), 'w') as f:
-            f.write("Some description")
-        self.assertEqual("Some description", r.get_description())
+        r = self._repo = self.open_repo('a.git')
+        with open(os.path.join(r.path, 'description'), 'wb') as f:
+            f.write(b"Some description")
+        self.assertEqual(b"Some description", r.get_description())
 
     def test_set_description(self):
-        r = self._repo = open_repo('a.git')
-        description = "Some description"
+        r = self._repo = self.open_repo('a.git')
+        description = b"Some description"
         r.set_description(description)
         self.assertEqual(description, r.get_description())
 
     def test_contains_missing(self):
-        r = self._repo = open_repo('a.git')
-        self.assertFalse("bar" in r)
+        r = self._repo = self.open_repo('a.git')
+        self.assertFalse(b"bar" in r)
 
     def test_get_peeled(self):
         # unpacked ref
-        r = self._repo = open_repo('a.git')
-        tag_sha = '28237f4dc30d0d462658d6b937b08a0f0b6ef55a'
+        r = self._repo = self.open_repo('a.git')
+        tag_sha = b'28237f4dc30d0d462658d6b937b08a0f0b6ef55a'
         self.assertNotEqual(r[tag_sha].sha().hexdigest(), r.head())
-        self.assertEqual(r.get_peeled('refs/tags/mytag'), r.head())
+        self.assertEqual(r.get_peeled(b'refs/tags/mytag'), r.head())
 
         # packed ref with cached peeled value
-        packed_tag_sha = 'b0931cadc54336e78a1d980420e3268903b57a50'
+        packed_tag_sha = b'b0931cadc54336e78a1d980420e3268903b57a50'
         parent_sha = r[r.head()].parents[0]
         self.assertNotEqual(r[packed_tag_sha].sha().hexdigest(), parent_sha)
-        self.assertEqual(r.get_peeled('refs/tags/mytag-packed'), parent_sha)
+        self.assertEqual(r.get_peeled(b'refs/tags/mytag-packed'), parent_sha)
 
         # TODO: add more corner cases to test repo
 
     def test_get_peeled_not_tag(self):
-        r = self._repo = open_repo('a.git')
-        self.assertEqual(r.get_peeled('HEAD'), r.head())
+        r = self._repo = self.open_repo('a.git')
+        self.assertEqual(r.get_peeled(b'HEAD'), r.head())
 
     def test_get_walker(self):
-        r = self._repo = open_repo('a.git')
+        r = self._repo = self.open_repo('a.git')
         # include defaults to [r.head()]
         self.assertEqual([e.commit.id for e in r.get_walker()],
-                         [r.head(), '2a72d929692c41d8554c07f6301757ba18a65d91'])
+                         [r.head(), b'2a72d929692c41d8554c07f6301757ba18a65d91'])
         self.assertEqual(
-            [e.commit.id for e in r.get_walker(['2a72d929692c41d8554c07f6301757ba18a65d91'])],
-            ['2a72d929692c41d8554c07f6301757ba18a65d91'])
+            [e.commit.id for e in r.get_walker([b'2a72d929692c41d8554c07f6301757ba18a65d91'])],
+            [b'2a72d929692c41d8554c07f6301757ba18a65d91'])
         self.assertEqual(
-            [e.commit.id for e in r.get_walker('2a72d929692c41d8554c07f6301757ba18a65d91')],
-            ['2a72d929692c41d8554c07f6301757ba18a65d91'])
+            [e.commit.id for e in r.get_walker(b'2a72d929692c41d8554c07f6301757ba18a65d91')],
+            [b'2a72d929692c41d8554c07f6301757ba18a65d91'])
 
     def test_clone(self):
-        r = self._repo = open_repo('a.git')
-        tmp_dir = tempfile.mkdtemp()
+        r = self._repo = self.open_repo('a.git')
+        tmp_dir = self.mkdtemp()
         self.addCleanup(shutil.rmtree, tmp_dir)
         t = r.clone(tmp_dir, mkdir=False)
         self.assertEqual({
-            'HEAD': 'a90fa2d900a17e99b433217e988c4eb4a2e9a097',
-            'refs/remotes/origin/master':
-                'a90fa2d900a17e99b433217e988c4eb4a2e9a097',
-            'refs/heads/master': 'a90fa2d900a17e99b433217e988c4eb4a2e9a097',
-            'refs/tags/mytag': '28237f4dc30d0d462658d6b937b08a0f0b6ef55a',
-            'refs/tags/mytag-packed':
-                'b0931cadc54336e78a1d980420e3268903b57a50',
+            b'HEAD': b'a90fa2d900a17e99b433217e988c4eb4a2e9a097',
+            b'refs/remotes/origin/master':
+                b'a90fa2d900a17e99b433217e988c4eb4a2e9a097',
+            b'refs/heads/master': b'a90fa2d900a17e99b433217e988c4eb4a2e9a097',
+            b'refs/tags/mytag': b'28237f4dc30d0d462658d6b937b08a0f0b6ef55a',
+            b'refs/tags/mytag-packed':
+                b'b0931cadc54336e78a1d980420e3268903b57a50',
             }, t.refs.as_dict())
         shas = [e.commit.id for e in r.get_walker()]
         self.assertEqual(shas, [t.head(),
-                         '2a72d929692c41d8554c07f6301757ba18a65d91'])
+                         b'2a72d929692c41d8554c07f6301757ba18a65d91'])
 
     def test_clone_no_head(self):
-        temp_dir = tempfile.mkdtemp()
+        temp_dir = self.mkdtemp()
+        if isinstance(temp_dir, bytes):
+            temp_dir_str = temp_dir.decode(sys.getfilesystemencoding())
+        else:
+            temp_dir_str = temp_dir
         self.addCleanup(shutil.rmtree, temp_dir)
         repo_dir = os.path.join(os.path.dirname(__file__), 'data', 'repos')
-        dest_dir = os.path.join(temp_dir, 'a.git')
+        dest_dir = os.path.join(temp_dir_str, 'a.git')
         shutil.copytree(os.path.join(repo_dir, 'a.git'),
                         dest_dir, symlinks=True)
         r = Repo(dest_dir)
-        del r.refs["refs/heads/master"]
-        del r.refs["HEAD"]
-        t = r.clone(os.path.join(temp_dir, 'b.git'), mkdir=True)
+        del r.refs[b"refs/heads/master"]
+        del r.refs[b"HEAD"]
+        t = r.clone(os.path.join(temp_dir_str, 'b.git'), mkdir=True)
         self.assertEqual({
-            'refs/tags/mytag': '28237f4dc30d0d462658d6b937b08a0f0b6ef55a',
-            'refs/tags/mytag-packed':
-                'b0931cadc54336e78a1d980420e3268903b57a50',
+            b'refs/tags/mytag': b'28237f4dc30d0d462658d6b937b08a0f0b6ef55a',
+            b'refs/tags/mytag-packed':
+                b'b0931cadc54336e78a1d980420e3268903b57a50',
             }, t.refs.as_dict())
 
     def test_clone_empty(self):
@@ -262,50 +309,54 @@ class RepositoryTests(TestCase):
         to the server.
         Non-bare repo HEAD always points to an existing ref.
         """
-        r = self._repo = open_repo('empty.git')
-        tmp_dir = tempfile.mkdtemp()
+        r = self._repo = self.open_repo('empty.git')
+        tmp_dir = self.mkdtemp()
         self.addCleanup(shutil.rmtree, tmp_dir)
         r.clone(tmp_dir, mkdir=False, bare=True)
 
     def test_merge_history(self):
-        r = self._repo = open_repo('simple_merge.git')
+        r = self._repo = self.open_repo('simple_merge.git')
         shas = [e.commit.id for e in r.get_walker()]
-        self.assertEqual(shas, ['5dac377bdded4c9aeb8dff595f0faeebcc8498cc',
-                                'ab64bbdcc51b170d21588e5c5d391ee5c0c96dfd',
-                                '4cffe90e0a41ad3f5190079d7c8f036bde29cbe6',
-                                '60dacdc733de308bb77bb76ce0fb0f9b44c9769e',
-                                '0d89f20333fbb1d2f3a94da77f4981373d8f4310'])
+        self.assertEqual(shas, [b'5dac377bdded4c9aeb8dff595f0faeebcc8498cc',
+                                b'ab64bbdcc51b170d21588e5c5d391ee5c0c96dfd',
+                                b'4cffe90e0a41ad3f5190079d7c8f036bde29cbe6',
+                                b'60dacdc733de308bb77bb76ce0fb0f9b44c9769e',
+                                b'0d89f20333fbb1d2f3a94da77f4981373d8f4310'])
 
     def test_out_of_order_merge(self):
         """Test that revision history is ordered by date, not parent order."""
-        r = self._repo = open_repo('ooo_merge.git')
+        r = self._repo = self.open_repo('ooo_merge.git')
         shas = [e.commit.id for e in r.get_walker()]
-        self.assertEqual(shas, ['7601d7f6231db6a57f7bbb79ee52e4d462fd44d1',
-                                'f507291b64138b875c28e03469025b1ea20bc614',
-                                'fb5b0425c7ce46959bec94d54b9a157645e114f5',
-                                'f9e39b120c68182a4ba35349f832d0e4e61f485c'])
+        self.assertEqual(shas, [b'7601d7f6231db6a57f7bbb79ee52e4d462fd44d1',
+                                b'f507291b64138b875c28e03469025b1ea20bc614',
+                                b'fb5b0425c7ce46959bec94d54b9a157645e114f5',
+                                b'f9e39b120c68182a4ba35349f832d0e4e61f485c'])
 
     def test_get_tags_empty(self):
-        r = self._repo = open_repo('ooo_merge.git')
-        self.assertEqual({}, r.refs.as_dict('refs/tags'))
+        r = self._repo = self.open_repo('ooo_merge.git')
+        self.assertEqual({}, r.refs.as_dict(b'refs/tags'))
 
     def test_get_config(self):
-        r = self._repo = open_repo('ooo_merge.git')
+        r = self._repo = self.open_repo('ooo_merge.git')
         self.assertIsInstance(r.get_config(), Config)
 
     def test_get_config_stack(self):
-        r = self._repo = open_repo('ooo_merge.git')
+        r = self._repo = self.open_repo('ooo_merge.git')
         self.assertIsInstance(r.get_config_stack(), Config)
 
     def test_submodule(self):
-        temp_dir = tempfile.mkdtemp()
+        temp_dir = self.mkdtemp()
         repo_dir = os.path.join(os.path.dirname(__file__), 'data', 'repos')
+        if isinstance(temp_dir, bytes):
+            temp_dir_str = temp_dir.decode(sys.getfilesystemencoding())
+        else:
+            temp_dir_str = temp_dir
         shutil.copytree(os.path.join(repo_dir, 'a.git'),
-                        os.path.join(temp_dir, 'a.git'), symlinks=True)
-        rel = os.path.relpath(os.path.join(repo_dir, 'submodule'), temp_dir)
-        os.symlink(os.path.join(rel, 'dotgit'), os.path.join(temp_dir, '.git'))
+                        os.path.join(temp_dir_str, 'a.git'), symlinks=True)
+        rel = os.path.relpath(os.path.join(repo_dir, 'submodule'), temp_dir_str)
+        os.symlink(os.path.join(rel, 'dotgit'), os.path.join(temp_dir_str, '.git'))
         r = Repo(temp_dir)
-        self.assertEqual(r.head(), 'a90fa2d900a17e99b433217e988c4eb4a2e9a097')
+        self.assertEqual(r.head(), b'a90fa2d900a17e99b433217e988c4eb4a2e9a097')
 
     def test_common_revisions(self):
         """
@@ -315,36 +366,36 @@ class RepositoryTests(TestCase):
         ``Repo.fetch_objects()``).
         """
 
-        expected_shas = set(['60dacdc733de308bb77bb76ce0fb0f9b44c9769e'])
+        expected_shas = set([b'60dacdc733de308bb77bb76ce0fb0f9b44c9769e'])
 
         # Source for objects.
-        r_base = open_repo('simple_merge.git')
+        r_base = self.open_repo('simple_merge.git')
 
         # Re-create each-side of the merge in simple_merge.git.
         #
         # Since the trees and blobs are missing, the repository created is
         # corrupted, but we're only checking for commits for the purpose of this
         # test, so it's immaterial.
-        r1_dir = tempfile.mkdtemp()
-        r1_commits = ['ab64bbdcc51b170d21588e5c5d391ee5c0c96dfd', # HEAD
-                      '60dacdc733de308bb77bb76ce0fb0f9b44c9769e',
-                      '0d89f20333fbb1d2f3a94da77f4981373d8f4310']
+        r1_dir = self.mkdtemp()
+        r1_commits = [b'ab64bbdcc51b170d21588e5c5d391ee5c0c96dfd', # HEAD
+                      b'60dacdc733de308bb77bb76ce0fb0f9b44c9769e',
+                      b'0d89f20333fbb1d2f3a94da77f4981373d8f4310']
 
-        r2_dir = tempfile.mkdtemp()
-        r2_commits = ['4cffe90e0a41ad3f5190079d7c8f036bde29cbe6', # HEAD
-                      '60dacdc733de308bb77bb76ce0fb0f9b44c9769e',
-                      '0d89f20333fbb1d2f3a94da77f4981373d8f4310']
+        r2_dir = self.mkdtemp()
+        r2_commits = [b'4cffe90e0a41ad3f5190079d7c8f036bde29cbe6', # HEAD
+                      b'60dacdc733de308bb77bb76ce0fb0f9b44c9769e',
+                      b'0d89f20333fbb1d2f3a94da77f4981373d8f4310']
 
         try:
             r1 = Repo.init_bare(r1_dir)
             for c in r1_commits:
                 r1.object_store.add_object(r_base.get_object(c))
-            r1.refs['HEAD'] = r1_commits[0]
+            r1.refs[b'HEAD'] = r1_commits[0]
 
             r2 = Repo.init_bare(r2_dir)
             for c in r2_commits:
                 r2.object_store.add_object(r_base.get_object(c))
-            r2.refs['HEAD'] = r2_commits[0]
+            r2.refs[b'HEAD'] = r2_commits[0]
 
             # Finally, the 'real' testing!
             shas = r2.object_store.find_common_revisions(r1.get_graph_walker())
@@ -360,19 +411,19 @@ class RepositoryTests(TestCase):
         if os.name != 'posix':
             self.skipTest('shell hook tests requires POSIX shell')
 
-        pre_commit_fail = """#!/bin/sh
+        pre_commit_fail = b"""#!/bin/sh
 exit 1
 """
 
-        pre_commit_success = """#!/bin/sh
+        pre_commit_success = b"""#!/bin/sh
 exit 0
 """
 
-        repo_dir = os.path.join(tempfile.mkdtemp())
+        repo_dir = os.path.join(self.mkdtemp())
         r = Repo.init(repo_dir)
         self.addCleanup(shutil.rmtree, repo_dir)
 
-        pre_commit = os.path.join(r.controldir(), 'hooks', 'pre-commit')
+        pre_commit = os.path.join(r.controldir(), b'hooks', b'pre-commit')
 
         with open(pre_commit, 'wb') as f:
             f.write(pre_commit_fail)
@@ -389,9 +440,9 @@ exit 0
         os.chmod(pre_commit, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
 
         commit_sha = r.do_commit(
-            'empty commit',
-            committer='Test Committer <test@nodomain.com>',
-            author='Test Author <test@nodomain.com>',
+            b'empty commit',
+            committer=b'Test Committer <test@nodomain.com>',
+            author=b'Test Author <test@nodomain.com>',
             commit_timestamp=12395, commit_timezone=0,
             author_timestamp=12395, author_timezone=0)
         self.assertEqual([], r[commit_sha].parents)
@@ -400,27 +451,27 @@ exit 0
         if os.name != 'posix':
             self.skipTest('shell hook tests requires POSIX shell')
 
-        commit_msg_fail = """#!/bin/sh
+        commit_msg_fail = b"""#!/bin/sh
 exit 1
 """
 
-        commit_msg_success = """#!/bin/sh
+        commit_msg_success = b"""#!/bin/sh
 exit 0
 """
 
-        repo_dir = os.path.join(tempfile.mkdtemp())
+        repo_dir = os.path.join(self.mkdtemp())
         r = Repo.init(repo_dir)
         self.addCleanup(shutil.rmtree, repo_dir)
 
-        commit_msg = os.path.join(r.controldir(), 'hooks', 'commit-msg')
+        commit_msg = os.path.join(r.controldir(), b'hooks', b'commit-msg')
 
         with open(commit_msg, 'wb') as f:
             f.write(commit_msg_fail)
         os.chmod(commit_msg, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
 
-        self.assertRaises(errors.CommitError, r.do_commit, 'failed commit',
-                          committer='Test Committer <test@nodomain.com>',
-                          author='Test Author <test@nodomain.com>',
+        self.assertRaises(errors.CommitError, r.do_commit, b'failed commit',
+                          committer=b'Test Committer <test@nodomain.com>',
+                          author=b'Test Author <test@nodomain.com>',
                           commit_timestamp=12345, commit_timezone=0,
                           author_timestamp=12345, author_timezone=0)
 
@@ -429,9 +480,9 @@ exit 0
         os.chmod(commit_msg, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
 
         commit_sha = r.do_commit(
-            'empty commit',
-            committer='Test Committer <test@nodomain.com>',
-            author='Test Author <test@nodomain.com>',
+            b'empty commit',
+            committer=b'Test Committer <test@nodomain.com>',
+            author=b'Test Author <test@nodomain.com>',
             commit_timestamp=12395, commit_timezone=0,
             author_timestamp=12395, author_timezone=0)
         self.assertEqual([], r[commit_sha].parents)
@@ -440,33 +491,38 @@ exit 0
         if os.name != 'posix':
             self.skipTest('shell hook tests requires POSIX shell')
 
-        repo_dir = os.path.join(tempfile.mkdtemp())
+        repo_dir = self.mkdtemp()
+        if isinstance(repo_dir, bytes):
+            repo_dir_str = repo_dir.decode(sys.getfilesystemencoding())
+        else:
+            repo_dir_str = repo_dir
+
         r = Repo.init(repo_dir)
         self.addCleanup(shutil.rmtree, repo_dir)
 
-        (fd, path) = tempfile.mkstemp(dir=repo_dir)
+        (fd, path) = tempfile.mkstemp(dir=repo_dir_str)
         post_commit_msg = """#!/bin/sh
-rm %(file)s
-""" % {'file': path}
+rm """ + path + """
+"""
 
         root_sha = r.do_commit(
-            'empty commit',
-            committer='Test Committer <test@nodomain.com>',
-            author='Test Author <test@nodomain.com>',
+            b'empty commit',
+            committer=b'Test Committer <test@nodomain.com>',
+            author=b'Test Author <test@nodomain.com>',
             commit_timestamp=12345, commit_timezone=0,
             author_timestamp=12345, author_timezone=0)
         self.assertEqual([], r[root_sha].parents)
 
-        post_commit = os.path.join(r.controldir(), 'hooks', 'post-commit')
+        post_commit = os.path.join(r.controldir(), b'hooks', b'post-commit')
 
-        with open(post_commit, 'wb') as f:
+        with open(post_commit, 'w') as f:
             f.write(post_commit_msg)
         os.chmod(post_commit, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
 
         commit_sha = r.do_commit(
-            'empty commit',
-            committer='Test Committer <test@nodomain.com>',
-            author='Test Author <test@nodomain.com>',
+            b'empty commit',
+            committer=b'Test Committer <test@nodomain.com>',
+            author=b'Test Author <test@nodomain.com>',
             commit_timestamp=12345, commit_timezone=0,
             author_timestamp=12345, author_timezone=0)
         self.assertEqual([root_sha], r[commit_sha].parents)
@@ -476,7 +532,7 @@ rm %(file)s
         post_commit_msg_fail = """#!/bin/sh
 exit 1
 """
-        with open(post_commit, 'wb') as f:
+        with open(post_commit, 'w') as f:
             f.write(post_commit_msg_fail)
         os.chmod(post_commit, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
 
@@ -486,40 +542,51 @@ exit 1
         self.addCleanup(restore_warnings)
 
         commit_sha2 = r.do_commit(
-            'empty commit',
-            committer='Test Committer <test@nodomain.com>',
-            author='Test Author <test@nodomain.com>',
+            b'empty commit',
+            committer=b'Test Committer <test@nodomain.com>',
+            author=b'Test Author <test@nodomain.com>',
             commit_timestamp=12345, commit_timezone=0,
             author_timestamp=12345, author_timezone=0)
-        self.assertEqual(len(warnings_list), 1)
+        self.assertEqual(len(warnings_list), 1, warnings_list)
         self.assertIsInstance(warnings_list[-1], UserWarning)
         self.assertTrue("post-commit hook failed: " in str(warnings_list[-1]))
         self.assertEqual([commit_sha], r[commit_sha2].parents)
 
 
-@skipIfPY3
-class BuildRepoTests(TestCase):
+class RepositoryUnicodeRootTests(RepositoryBytesRootTests):
+
+    def mktemp(self):
+        return mkdtemp_unicode()
+
+
+class BuildRepoBytesRootTests(TestCase):
     """Tests that build on-disk repos from scratch.
 
     Repos live in a temp dir and are torn down after each test. They start with
     a single commit in master having single file named 'a'.
     """
 
+    def get_repo_dir(self):
+        return os.path.join(mkdtemp_bytes(), b'test')
+
+    def get_a_filename(self):
+        return b'a'
+
     def setUp(self):
-        super(BuildRepoTests, self).setUp()
-        self._repo_dir = os.path.join(tempfile.mkdtemp(), 'test')
+        super(BuildRepoBytesRootTests, self).setUp()
+        self._repo_dir = self.get_repo_dir()
         os.makedirs(self._repo_dir)
         r = self._repo = Repo.init(self._repo_dir)
         self.assertFalse(r.bare)
-        self.assertEqual('ref: refs/heads/master', r.refs.read_ref('HEAD'))
-        self.assertRaises(KeyError, lambda: r.refs['refs/heads/master'])
+        self.assertEqual(b'ref: refs/heads/master', r.refs.read_ref(b'HEAD'))
+        self.assertRaises(KeyError, lambda: r.refs[b'refs/heads/master'])
 
-        with open(os.path.join(r.path, 'a'), 'wb') as f:
-            f.write('file contents')
+        with open(os.path.join(r._path_bytes, b'a'), 'wb') as f:
+            f.write(b'file contents')
         r.stage(['a'])
-        commit_sha = r.do_commit('msg',
-                                 committer='Test Committer <test@nodomain.com>',
-                                 author='Test Author <test@nodomain.com>',
+        commit_sha = r.do_commit(b'msg',
+                                 committer=b'Test Committer <test@nodomain.com>',
+                                 author=b'Test Author <test@nodomain.com>',
                                  commit_timestamp=12345, commit_timezone=0,
                                  author_timestamp=12345, author_timezone=0)
         self.assertEqual([], r[commit_sha].parents)
@@ -527,43 +594,43 @@ class BuildRepoTests(TestCase):
 
     def tearDown(self):
         tear_down_repo(self._repo)
-        super(BuildRepoTests, self).tearDown()
+        super(BuildRepoBytesRootTests, self).tearDown()
 
     def test_build_repo(self):
         r = self._repo
-        self.assertEqual('ref: refs/heads/master', r.refs.read_ref('HEAD'))
-        self.assertEqual(self._root_commit, r.refs['refs/heads/master'])
-        expected_blob = objects.Blob.from_string('file contents')
+        self.assertEqual(b'ref: refs/heads/master', r.refs.read_ref(b'HEAD'))
+        self.assertEqual(self._root_commit, r.refs[b'refs/heads/master'])
+        expected_blob = objects.Blob.from_string(b'file contents')
         self.assertEqual(expected_blob.data, r[expected_blob.id].data)
         actual_commit = r[self._root_commit]
-        self.assertEqual('msg', actual_commit.message)
+        self.assertEqual(b'msg', actual_commit.message)
 
     def test_commit_modified(self):
         r = self._repo
-        with open(os.path.join(r.path, 'a'), 'wb') as f:
-            f.write('new contents')
-        os.symlink('a', os.path.join(self._repo_dir, 'b'))
+        with open(os.path.join(r._path_bytes, b'a'), 'wb') as f:
+            f.write(b'new contents')
+        os.symlink('a', os.path.join(r._path_bytes, b'b'))
         r.stage(['a', 'b'])
-        commit_sha = r.do_commit('modified a',
-                                 committer='Test Committer <test@nodomain.com>',
-                                 author='Test Author <test@nodomain.com>',
+        commit_sha = r.do_commit(b'modified a',
+                                 committer=b'Test Committer <test@nodomain.com>',
+                                 author=b'Test Author <test@nodomain.com>',
                                  commit_timestamp=12395, commit_timezone=0,
                                  author_timestamp=12395, author_timezone=0)
         self.assertEqual([self._root_commit], r[commit_sha].parents)
-        a_mode, a_id = tree_lookup_path(r.get_object, r[commit_sha].tree, 'a')
+        a_mode, a_id = tree_lookup_path(r.get_object, r[commit_sha].tree, b'a')
         self.assertEqual(stat.S_IFREG | 0o644, a_mode)
-        self.assertEqual('new contents', r[a_id].data)
-        b_mode, b_id = tree_lookup_path(r.get_object, r[commit_sha].tree, 'b')
+        self.assertEqual(b'new contents', r[a_id].data)
+        b_mode, b_id = tree_lookup_path(r.get_object, r[commit_sha].tree, b'b')
         self.assertTrue(stat.S_ISLNK(b_mode))
-        self.assertEqual('a', r[b_id].data)
+        self.assertEqual(b'a', r[b_id].data)
 
     def test_commit_deleted(self):
         r = self._repo
-        os.remove(os.path.join(r.path, 'a'))
+        os.remove(os.path.join(r._path_bytes, b'a'))
         r.stage(['a'])
-        commit_sha = r.do_commit('deleted a',
-                                 committer='Test Committer <test@nodomain.com>',
-                                 author='Test Author <test@nodomain.com>',
+        commit_sha = r.do_commit(b'deleted a',
+                                 committer=b'Test Committer <test@nodomain.com>',
+                                 author=b'Test Author <test@nodomain.com>',
                                  commit_timestamp=12395, commit_timezone=0,
                                  author_timestamp=12395, author_timezone=0)
         self.assertEqual([self._root_commit], r[commit_sha].parents)
@@ -573,42 +640,42 @@ class BuildRepoTests(TestCase):
 
     def test_commit_encoding(self):
         r = self._repo
-        commit_sha = r.do_commit('commit with strange character \xee',
-             committer='Test Committer <test@nodomain.com>',
-             author='Test Author <test@nodomain.com>',
+        commit_sha = r.do_commit(b'commit with strange character \xee',
+             committer=b'Test Committer <test@nodomain.com>',
+             author=b'Test Author <test@nodomain.com>',
              commit_timestamp=12395, commit_timezone=0,
              author_timestamp=12395, author_timezone=0,
-             encoding="iso8859-1")
-        self.assertEqual("iso8859-1", r[commit_sha].encoding)
+             encoding=b"iso8859-1")
+        self.assertEqual(b"iso8859-1", r[commit_sha].encoding)
 
     def test_commit_config_identity(self):
         # commit falls back to the users' identity if it wasn't specified
         r = self._repo
         c = r.get_config()
-        c.set(("user", ), "name", "Jelmer")
-        c.set(("user", ), "email", "jelmer@apache.org")
+        c.set((b"user", ), b"name", b"Jelmer")
+        c.set((b"user", ), b"email", b"jelmer@apache.org")
         c.write_to_path()
-        commit_sha = r.do_commit('message')
+        commit_sha = r.do_commit(b'message')
         self.assertEqual(
-            "Jelmer <jelmer@apache.org>",
+            b"Jelmer <jelmer@apache.org>",
             r[commit_sha].author)
         self.assertEqual(
-            "Jelmer <jelmer@apache.org>",
+            b"Jelmer <jelmer@apache.org>",
             r[commit_sha].committer)
 
     def test_commit_config_identity_in_memoryrepo(self):
         # commit falls back to the users' identity if it wasn't specified
         r = MemoryRepo.init_bare([], {})
         c = r.get_config()
-        c.set(("user", ), "name", "Jelmer")
-        c.set(("user", ), "email", "jelmer@apache.org")
+        c.set((b"user", ), b"name", b"Jelmer")
+        c.set((b"user", ), b"email", b"jelmer@apache.org")
 
-        commit_sha = r.do_commit('message', tree=objects.Tree().id)
+        commit_sha = r.do_commit(b'message', tree=objects.Tree().id)
         self.assertEqual(
-            "Jelmer <jelmer@apache.org>",
+            b"Jelmer <jelmer@apache.org>",
             r[commit_sha].author)
         self.assertEqual(
-            "Jelmer <jelmer@apache.org>",
+            b"Jelmer <jelmer@apache.org>",
             r[commit_sha].committer)
 
     def test_commit_fail_ref(self):
@@ -623,9 +690,9 @@ class BuildRepoTests(TestCase):
         r.refs.add_if_new = add_if_new
 
         old_shas = set(r.object_store)
-        self.assertRaises(errors.CommitError, r.do_commit, 'failed commit',
-                          committer='Test Committer <test@nodomain.com>',
-                          author='Test Author <test@nodomain.com>',
+        self.assertRaises(errors.CommitError, r.do_commit, b'failed commit',
+                          committer=b'Test Committer <test@nodomain.com>',
+                          author=b'Test Author <test@nodomain.com>',
                           commit_timestamp=12345, commit_timezone=0,
                           author_timestamp=12345, author_timezone=0)
         new_shas = set(r.object_store) - old_shas
@@ -633,45 +700,45 @@ class BuildRepoTests(TestCase):
         # Check that the new commit (now garbage) was added.
         new_commit = r[new_shas.pop()]
         self.assertEqual(r[self._root_commit].tree, new_commit.tree)
-        self.assertEqual('failed commit', new_commit.message)
+        self.assertEqual(b'failed commit', new_commit.message)
 
     def test_commit_branch(self):
         r = self._repo
 
-        commit_sha = r.do_commit('commit to branch',
-             committer='Test Committer <test@nodomain.com>',
-             author='Test Author <test@nodomain.com>',
+        commit_sha = r.do_commit(b'commit to branch',
+             committer=b'Test Committer <test@nodomain.com>',
+             author=b'Test Author <test@nodomain.com>',
              commit_timestamp=12395, commit_timezone=0,
              author_timestamp=12395, author_timezone=0,
-             ref="refs/heads/new_branch")
-        self.assertEqual(self._root_commit, r["HEAD"].id)
-        self.assertEqual(commit_sha, r["refs/heads/new_branch"].id)
+             ref=b"refs/heads/new_branch")
+        self.assertEqual(self._root_commit, r[b"HEAD"].id)
+        self.assertEqual(commit_sha, r[b"refs/heads/new_branch"].id)
         self.assertEqual([], r[commit_sha].parents)
-        self.assertTrue("refs/heads/new_branch" in r)
+        self.assertTrue(b"refs/heads/new_branch" in r)
 
         new_branch_head = commit_sha
 
-        commit_sha = r.do_commit('commit to branch 2',
-             committer='Test Committer <test@nodomain.com>',
-             author='Test Author <test@nodomain.com>',
+        commit_sha = r.do_commit(b'commit to branch 2',
+             committer=b'Test Committer <test@nodomain.com>',
+             author=b'Test Author <test@nodomain.com>',
              commit_timestamp=12395, commit_timezone=0,
              author_timestamp=12395, author_timezone=0,
-             ref="refs/heads/new_branch")
-        self.assertEqual(self._root_commit, r["HEAD"].id)
-        self.assertEqual(commit_sha, r["refs/heads/new_branch"].id)
+             ref=b"refs/heads/new_branch")
+        self.assertEqual(self._root_commit, r[b"HEAD"].id)
+        self.assertEqual(commit_sha, r[b"refs/heads/new_branch"].id)
         self.assertEqual([new_branch_head], r[commit_sha].parents)
 
     def test_commit_merge_heads(self):
         r = self._repo
-        merge_1 = r.do_commit('commit to branch 2',
-             committer='Test Committer <test@nodomain.com>',
-             author='Test Author <test@nodomain.com>',
+        merge_1 = r.do_commit(b'commit to branch 2',
+             committer=b'Test Committer <test@nodomain.com>',
+             author=b'Test Author <test@nodomain.com>',
              commit_timestamp=12395, commit_timezone=0,
              author_timestamp=12395, author_timezone=0,
-             ref="refs/heads/new_branch")
-        commit_sha = r.do_commit('commit with merge',
-             committer='Test Committer <test@nodomain.com>',
-             author='Test Author <test@nodomain.com>',
+             ref=b"refs/heads/new_branch")
+        commit_sha = r.do_commit(b'commit with merge',
+             committer=b'Test Committer <test@nodomain.com>',
+             author=b'Test Author <test@nodomain.com>',
              commit_timestamp=12395, commit_timezone=0,
              author_timestamp=12395, author_timezone=0,
              merge_heads=[merge_1])
@@ -684,9 +751,9 @@ class BuildRepoTests(TestCase):
 
         old_shas = set(r.object_store)
         old_refs = r.get_refs()
-        commit_sha = r.do_commit('commit with no ref',
-             committer='Test Committer <test@nodomain.com>',
-             author='Test Author <test@nodomain.com>',
+        commit_sha = r.do_commit(b'commit with no ref',
+             committer=b'Test Committer <test@nodomain.com>',
+             author=b'Test Author <test@nodomain.com>',
              commit_timestamp=12395, commit_timezone=0,
              author_timestamp=12395, author_timezone=0,
              ref=None)
@@ -704,9 +771,9 @@ class BuildRepoTests(TestCase):
 
         old_shas = set(r.object_store)
         old_refs = r.get_refs()
-        commit_sha = r.do_commit('commit with no ref',
-             committer='Test Committer <test@nodomain.com>',
-             author='Test Author <test@nodomain.com>',
+        commit_sha = r.do_commit(b'commit with no ref',
+             committer=b'Test Committer <test@nodomain.com>',
+             author=b'Test Author <test@nodomain.com>',
              commit_timestamp=12395, commit_timezone=0,
              author_timestamp=12395, author_timezone=0,
              ref=None, merge_heads=[self._root_commit])
@@ -721,7 +788,17 @@ class BuildRepoTests(TestCase):
 
     def test_stage_deleted(self):
         r = self._repo
-        os.remove(os.path.join(r.path, 'a'))
+        os.remove(os.path.join(r._path_bytes, b'a'))
         r.stage(['a'])
         r.stage(['a'])  # double-stage a deleted path
 
+
+class BuildRepoUnicodeRootTests(TestCase):
+    """Tests that build on-disk repos from scratch.
+
+    Repos live in a temp dir and are torn down after each test. They start with
+    a single commit in master having single file named 'a'.
+    """
+
+    def get_repo_dir(self):
+        return os.path.join(mkdtemp_unicode(), 'test')

+ 138 - 158
dulwich/tests/test_server.py

@@ -28,10 +28,6 @@ from dulwich.errors import (
     UnexpectedCommandError,
     HangupException,
     )
-from dulwich.objects import (
-    Commit,
-    Tag,
-    )
 from dulwich.object_store import (
     MemoryObjectStore,
     )
@@ -58,20 +54,18 @@ from dulwich.server import (
 from dulwich.tests import TestCase
 from dulwich.tests.utils import (
     make_commit,
-    make_object,
     make_tag,
-    skipIfPY3,
     )
 from dulwich.protocol import (
     ZERO_SHA,
     )
 
-ONE = '1' * 40
-TWO = '2' * 40
-THREE = '3' * 40
-FOUR = '4' * 40
-FIVE = '5' * 40
-SIX = '6' * 40
+ONE = b'1' * 40
+TWO = b'2' * 40
+THREE = b'3' * 40
+FOUR = b'4' * 40
+FIVE = b'5' * 40
+SIX = b'6' * 40
 
 
 class TestProto(object):
@@ -87,7 +81,7 @@ class TestProto(object):
         if self._output:
             data = self._output.pop(0)
             if data is not None:
-                return '%s\n' % data.rstrip()
+                return data.rstrip() + b'\n'
             else:
                 # flush-pkt ('0000').
                 return None
@@ -112,11 +106,11 @@ class TestGenericHandler(Handler):
 
     @classmethod
     def capabilities(cls):
-        return ('cap1', 'cap2', 'cap3')
+        return (b'cap1', b'cap2', b'cap3')
 
     @classmethod
     def required_capabilities(cls):
-        return ('cap2',)
+        return (b'cap2',)
 
 
 class HandlerTestCase(TestCase):
@@ -132,81 +126,80 @@ class HandlerTestCase(TestCase):
             self.fail(e)
 
     def test_capability_line(self):
-        self.assertEqual('cap1 cap2 cap3', self._handler.capability_line())
+        self.assertEqual(b'cap1 cap2 cap3', self._handler.capability_line())
 
     def test_set_client_capabilities(self):
         set_caps = self._handler.set_client_capabilities
-        self.assertSucceeds(set_caps, ['cap2'])
-        self.assertSucceeds(set_caps, ['cap1', 'cap2'])
+        self.assertSucceeds(set_caps, [b'cap2'])
+        self.assertSucceeds(set_caps, [b'cap1', b'cap2'])
 
         # different order
-        self.assertSucceeds(set_caps, ['cap3', 'cap1', 'cap2'])
+        self.assertSucceeds(set_caps, [b'cap3', b'cap1', b'cap2'])
 
         # error cases
-        self.assertRaises(GitProtocolError, set_caps, ['capxxx', 'cap2'])
-        self.assertRaises(GitProtocolError, set_caps, ['cap1', 'cap3'])
+        self.assertRaises(GitProtocolError, set_caps, [b'capxxx', b'cap2'])
+        self.assertRaises(GitProtocolError, set_caps, [b'cap1', b'cap3'])
 
         # ignore innocuous but unknown capabilities
-        self.assertRaises(GitProtocolError, set_caps, ['cap2', 'ignoreme'])
-        self.assertFalse('ignoreme' in self._handler.capabilities())
-        self._handler.innocuous_capabilities = lambda: ('ignoreme',)
-        self.assertSucceeds(set_caps, ['cap2', 'ignoreme'])
+        self.assertRaises(GitProtocolError, set_caps, [b'cap2', b'ignoreme'])
+        self.assertFalse(b'ignoreme' in self._handler.capabilities())
+        self._handler.innocuous_capabilities = lambda: (b'ignoreme',)
+        self.assertSucceeds(set_caps, [b'cap2', b'ignoreme'])
 
     def test_has_capability(self):
-        self.assertRaises(GitProtocolError, self._handler.has_capability, 'cap')
+        self.assertRaises(GitProtocolError, self._handler.has_capability, b'cap')
         caps = self._handler.capabilities()
         self._handler.set_client_capabilities(caps)
         for cap in caps:
             self.assertTrue(self._handler.has_capability(cap))
-        self.assertFalse(self._handler.has_capability('capxxx'))
+        self.assertFalse(self._handler.has_capability(b'capxxx'))
 
 
-@skipIfPY3
 class UploadPackHandlerTestCase(TestCase):
 
     def setUp(self):
         super(UploadPackHandlerTestCase, self).setUp()
         self._repo = MemoryRepo.init_bare([], {})
-        backend = DictBackend({'/': self._repo})
+        backend = DictBackend({b'/': self._repo})
         self._handler = UploadPackHandler(
-          backend, ['/', 'host=lolcathost'], TestProto())
+          backend, [b'/', b'host=lolcathost'], TestProto())
 
     def test_progress(self):
         caps = self._handler.required_capabilities()
         self._handler.set_client_capabilities(caps)
-        self._handler.progress('first message')
-        self._handler.progress('second message')
-        self.assertEqual('first message',
+        self._handler.progress(b'first message')
+        self._handler.progress(b'second message')
+        self.assertEqual(b'first message',
                          self._handler.proto.get_received_line(2))
-        self.assertEqual('second message',
+        self.assertEqual(b'second message',
                          self._handler.proto.get_received_line(2))
         self.assertRaises(IndexError, self._handler.proto.get_received_line, 2)
 
     def test_no_progress(self):
-        caps = list(self._handler.required_capabilities()) + ['no-progress']
+        caps = list(self._handler.required_capabilities()) + [b'no-progress']
         self._handler.set_client_capabilities(caps)
-        self._handler.progress('first message')
-        self._handler.progress('second message')
+        self._handler.progress(b'first message')
+        self._handler.progress(b'second message')
         self.assertRaises(IndexError, self._handler.proto.get_received_line, 2)
 
     def test_get_tagged(self):
         refs = {
-            'refs/tags/tag1': ONE,
-            'refs/tags/tag2': TWO,
-            'refs/heads/master': FOUR,  # not a tag, no peeled value
+            b'refs/tags/tag1': ONE,
+            b'refs/tags/tag2': TWO,
+            b'refs/heads/master': FOUR,  # not a tag, no peeled value
             }
         # repo needs to peel this object
         self._repo.object_store.add_object(make_commit(id=FOUR))
         self._repo.refs._update(refs)
         peeled = {
-            'refs/tags/tag1': '1234' * 10,
-            'refs/tags/tag2': '5678' * 10,
+            b'refs/tags/tag1': b'1234' * 10,
+            b'refs/tags/tag2': b'5678' * 10,
             }
         self._repo.refs._update_peeled(peeled)
 
-        caps = list(self._handler.required_capabilities()) + ['include-tag']
+        caps = list(self._handler.required_capabilities()) + [b'include-tag']
         self._handler.set_client_capabilities(caps)
-        self.assertEqual({'1234' * 10: ONE, '5678' * 10: TWO},
+        self.assertEqual({b'1234' * 10: ONE, b'5678' * 10: TWO},
                           self._handler.get_tagged(refs, repo=self._repo))
 
         # non-include-tag case
@@ -215,7 +208,6 @@ class UploadPackHandlerTestCase(TestCase):
         self.assertEqual({}, self._handler.get_tagged(refs, repo=self._repo))
 
 
-@skipIfPY3
 class FindShallowTests(TestCase):
 
     def setUp(self):
@@ -226,7 +218,7 @@ class FindShallowTests(TestCase):
         self._store.add_object(commit)
         return commit
 
-    def make_linear_commits(self, n, message=''):
+    def make_linear_commits(self, n, message=b''):
         commits = []
         parents = []
         for _ in range(n):
@@ -250,9 +242,9 @@ class FindShallowTests(TestCase):
                          _find_shallow(self._store, [c3.id], 3))
 
     def test_multiple_independent(self):
-        a = self.make_linear_commits(2, message='a')
-        b = self.make_linear_commits(2, message='b')
-        c = self.make_linear_commits(2, message='c')
+        a = self.make_linear_commits(2, message=b'a')
+        b = self.make_linear_commits(2, message=b'b')
+        c = self.make_linear_commits(2, message=b'c')
         heads = [a[1].id, b[1].id, c[1].id]
 
         self.assertEqual((set([a[0].id, b[0].id, c[0].id]), set(heads)),
@@ -281,7 +273,7 @@ class FindShallowTests(TestCase):
 
     def test_tag(self):
         c1, c2 = self.make_linear_commits(2)
-        tag = make_tag(c2, name='tag')
+        tag = make_tag(c2, name=b'tag')
         self._store.add_object(tag)
 
         self.assertEqual((set([c1.id]), set([c2.id])),
@@ -293,37 +285,35 @@ class TestUploadPackHandler(UploadPackHandler):
     def required_capabilities(self):
         return ()
 
-@skipIfPY3
 class ReceivePackHandlerTestCase(TestCase):
 
     def setUp(self):
         super(ReceivePackHandlerTestCase, self).setUp()
         self._repo = MemoryRepo.init_bare([], {})
-        backend = DictBackend({'/': self._repo})
+        backend = DictBackend({b'/': self._repo})
         self._handler = ReceivePackHandler(
-          backend, ['/', 'host=lolcathost'], TestProto())
+          backend, [b'/', b'host=lolcathost'], TestProto())
 
     def test_apply_pack_del_ref(self):
         refs = {
-            'refs/heads/master': TWO,
-            'refs/heads/fake-branch': ONE}
+            b'refs/heads/master': TWO,
+            b'refs/heads/fake-branch': ONE}
         self._repo.refs._update(refs)
-        update_refs = [[ONE, ZERO_SHA, 'refs/heads/fake-branch'], ]
+        update_refs = [[ONE, ZERO_SHA, b'refs/heads/fake-branch'], ]
         status = self._handler._apply_pack(update_refs)
-        self.assertEqual(status[0][0], 'unpack')
-        self.assertEqual(status[0][1], 'ok')
-        self.assertEqual(status[1][0], 'refs/heads/fake-branch')
-        self.assertEqual(status[1][1], 'ok')
+        self.assertEqual(status[0][0], b'unpack')
+        self.assertEqual(status[0][1], b'ok')
+        self.assertEqual(status[1][0], b'refs/heads/fake-branch')
+        self.assertEqual(status[1][1], b'ok')
 
 
-@skipIfPY3
 class ProtocolGraphWalkerEmptyTestCase(TestCase):
     def setUp(self):
         super(ProtocolGraphWalkerEmptyTestCase, self).setUp()
         self._repo = MemoryRepo.init_bare([], {})
-        backend = DictBackend({'/': self._repo})
+        backend = DictBackend({b'/': self._repo})
         self._walker = ProtocolGraphWalker(
-            TestUploadPackHandler(backend, ['/', 'host=lolcats'], TestProto()),
+            TestUploadPackHandler(backend, [b'/', b'host=lolcats'], TestProto()),
             self._repo.object_store, self._repo.get_peeled)
 
     def test_empty_repository(self):
@@ -338,7 +328,6 @@ class ProtocolGraphWalkerEmptyTestCase(TestCase):
 
 
 
-@skipIfPY3
 class ProtocolGraphWalkerTestCase(TestCase):
 
     def setUp(self):
@@ -355,9 +344,9 @@ class ProtocolGraphWalkerTestCase(TestCase):
           make_commit(id=FIVE, parents=[THREE], commit_time=555),
           ]
         self._repo = MemoryRepo.init_bare(commits, {})
-        backend = DictBackend({'/': self._repo})
+        backend = DictBackend({b'/': self._repo})
         self._walker = ProtocolGraphWalker(
-            TestUploadPackHandler(backend, ['/', 'host=lolcats'], TestProto()),
+            TestUploadPackHandler(backend, [b'/', b'host=lolcats'], TestProto()),
             self._repo.object_store, self._repo.get_peeled)
 
     def test_all_wants_satisfied_no_haves(self):
@@ -394,20 +383,20 @@ class ProtocolGraphWalkerTestCase(TestCase):
         self.assertTrue(self._walker.all_wants_satisfied([TWO, THREE]))
 
     def test_split_proto_line(self):
-        allowed = ('want', 'done', None)
-        self.assertEqual(('want', ONE),
-                          _split_proto_line('want %s\n' % ONE, allowed))
-        self.assertEqual(('want', TWO),
-                          _split_proto_line('want %s\n' % TWO, allowed))
+        allowed = (b'want', b'done', None)
+        self.assertEqual((b'want', ONE),
+                          _split_proto_line(b'want ' + ONE + b'\n', allowed))
+        self.assertEqual((b'want', TWO),
+                          _split_proto_line(b'want ' + TWO + b'\n', allowed))
         self.assertRaises(GitProtocolError, _split_proto_line,
-                          'want xxxx\n', allowed)
+                          b'want xxxx\n', allowed)
         self.assertRaises(UnexpectedCommandError, _split_proto_line,
-                          'have %s\n' % THREE, allowed)
+                          b'have ' + THREE + b'\n', allowed)
         self.assertRaises(GitProtocolError, _split_proto_line,
-                          'foo %s\n' % FOUR, allowed)
-        self.assertRaises(GitProtocolError, _split_proto_line, 'bar', allowed)
-        self.assertEqual(('done', None), _split_proto_line('done\n', allowed))
-        self.assertEqual((None, None), _split_proto_line('', allowed))
+                          b'foo ' + FOUR + b'\n', allowed)
+        self.assertRaises(GitProtocolError, _split_proto_line, b'bar', allowed)
+        self.assertEqual((b'done', None), _split_proto_line(b'done\n', allowed))
+        self.assertEqual((None, None), _split_proto_line(b'', allowed))
 
     def test_determine_wants(self):
         self._walker.proto.set_output([None])
@@ -415,14 +404,14 @@ class ProtocolGraphWalkerTestCase(TestCase):
         self.assertEqual(None, self._walker.proto.get_received_line())
 
         self._walker.proto.set_output([
-          'want %s multi_ack' % ONE,
-          'want %s' % TWO,
+          b'want ' + ONE + b' multi_ack',
+          b'want ' + TWO,
           None,
           ])
         heads = {
-          'refs/heads/ref1': ONE,
-          'refs/heads/ref2': TWO,
-          'refs/heads/ref3': THREE,
+          b'refs/heads/ref1': ONE,
+          b'refs/heads/ref2': TWO,
+          b'refs/heads/ref3': THREE,
           }
         self._repo.refs._update(heads)
         self.assertEqual([ONE, TWO], self._walker.determine_wants(heads))
@@ -431,29 +420,29 @@ class ProtocolGraphWalkerTestCase(TestCase):
         self.assertEqual([], self._walker.determine_wants(heads))
         self._walker.advertise_refs = False
 
-        self._walker.proto.set_output(['want %s multi_ack' % FOUR, None])
+        self._walker.proto.set_output([b'want ' + FOUR + b' multi_ack', None])
         self.assertRaises(GitProtocolError, self._walker.determine_wants, heads)
 
         self._walker.proto.set_output([None])
         self.assertEqual([], self._walker.determine_wants(heads))
 
-        self._walker.proto.set_output(['want %s multi_ack' % ONE, 'foo', None])
+        self._walker.proto.set_output([b'want ' + ONE + b' multi_ack', b'foo', None])
         self.assertRaises(GitProtocolError, self._walker.determine_wants, heads)
 
-        self._walker.proto.set_output(['want %s multi_ack' % FOUR, None])
+        self._walker.proto.set_output([b'want ' + FOUR + b' multi_ack', None])
         self.assertRaises(GitProtocolError, self._walker.determine_wants, heads)
 
     def test_determine_wants_advertisement(self):
         self._walker.proto.set_output([None])
         # advertise branch tips plus tag
         heads = {
-          'refs/heads/ref4': FOUR,
-          'refs/heads/ref5': FIVE,
-          'refs/heads/tag6': SIX,
+          b'refs/heads/ref4': FOUR,
+          b'refs/heads/ref5': FIVE,
+          b'refs/heads/tag6': SIX,
           }
         self._repo.refs._update(heads)
         self._repo.refs._update_peeled(heads)
-        self._repo.refs._update_peeled({'refs/heads/tag6': FIVE})
+        self._repo.refs._update_peeled({b'refs/heads/tag6': FIVE})
         self._walker.determine_wants(heads)
         lines = []
         while True:
@@ -461,21 +450,21 @@ class ProtocolGraphWalkerTestCase(TestCase):
             if line is None:
                 break
             # strip capabilities list if present
-            if '\x00' in line:
-                line = line[:line.index('\x00')]
+            if b'\x00' in line:
+                line = line[:line.index(b'\x00')]
             lines.append(line.rstrip())
 
         self.assertEqual([
-          '%s refs/heads/ref4' % FOUR,
-          '%s refs/heads/ref5' % FIVE,
-          '%s refs/heads/tag6^{}' % FIVE,
-          '%s refs/heads/tag6' % SIX,
+          FOUR + b' refs/heads/ref4',
+          FIVE + b' refs/heads/ref5',
+          FIVE + b' refs/heads/tag6^{}',
+          SIX + b' refs/heads/tag6',
           ], sorted(lines))
 
         # ensure peeled tag was advertised immediately following tag
         for i, line in enumerate(lines):
-            if line.endswith(' refs/heads/tag6'):
-                self.assertEqual('%s refs/heads/tag6^{}' % FIVE, lines[i+1])
+            if line.endswith(b' refs/heads/tag6'):
+                self.assertEqual(FIVE + b' refs/heads/tag6^{}', lines[i+1])
 
     # TODO: test commit time cutoff
 
@@ -488,18 +477,18 @@ class ProtocolGraphWalkerTestCase(TestCase):
           expected, list(iter(self._walker.proto.get_received_line, None)))
 
     def test_handle_shallow_request_no_client_shallows(self):
-        self._handle_shallow_request(['deepen 1\n'], [FOUR, FIVE])
+        self._handle_shallow_request([b'deepen 1\n'], [FOUR, FIVE])
         self.assertEqual(set([TWO, THREE]), self._walker.shallow)
         self.assertReceived([
-          'shallow %s' % TWO,
-          'shallow %s' % THREE,
+          b'shallow ' + TWO,
+          b'shallow ' + THREE,
           ])
 
     def test_handle_shallow_request_no_new_shallows(self):
         lines = [
-          'shallow %s\n' % TWO,
-          'shallow %s\n' % THREE,
-          'deepen 1\n',
+          b'shallow ' + TWO + b'\n',
+          b'shallow ' + THREE + b'\n',
+          b'deepen 1\n',
           ]
         self._handle_shallow_request(lines, [FOUR, FIVE])
         self.assertEqual(set([TWO, THREE]), self._walker.shallow)
@@ -507,19 +496,18 @@ class ProtocolGraphWalkerTestCase(TestCase):
 
     def test_handle_shallow_request_unshallows(self):
         lines = [
-          'shallow %s\n' % TWO,
-          'deepen 2\n',
+          b'shallow ' + TWO + b'\n',
+          b'deepen 2\n',
           ]
         self._handle_shallow_request(lines, [FOUR, FIVE])
         self.assertEqual(set([ONE]), self._walker.shallow)
         self.assertReceived([
-          'shallow %s' % ONE,
-          'unshallow %s' % TWO,
+          b'shallow ' + ONE,
+          b'unshallow ' + TWO,
           # THREE is unshallow but was is not shallow in the client
           ])
 
 
-@skipIfPY3
 class TestProtocolGraphWalker(object):
 
     def __init__(self):
@@ -535,11 +523,11 @@ class TestProtocolGraphWalker(object):
             assert command in allowed
         return command, sha
 
-    def send_ack(self, sha, ack_type=''):
+    def send_ack(self, sha, ack_type=b''):
         self.acks.append((sha, ack_type))
 
     def send_nak(self):
-        self.acks.append((None, 'nak'))
+        self.acks.append((None, b'nak'))
 
     def all_wants_satisfied(self, haves):
         return self.done
@@ -550,7 +538,6 @@ class TestProtocolGraphWalker(object):
         return self.acks.pop(0)
 
 
-@skipIfPY3
 class AckGraphWalkerImplTestCase(TestCase):
     """Base setup and asserts for AckGraphWalker tests."""
 
@@ -558,10 +545,10 @@ class AckGraphWalkerImplTestCase(TestCase):
         super(AckGraphWalkerImplTestCase, self).setUp()
         self._walker = TestProtocolGraphWalker()
         self._walker.lines = [
-          ('have', TWO),
-          ('have', ONE),
-          ('have', THREE),
-          ('done', None),
+          (b'have', TWO),
+          (b'have', ONE),
+          (b'have', THREE),
+          (b'done', None),
           ]
         self._impl = self.impl_cls(self._walker)
 
@@ -573,17 +560,16 @@ class AckGraphWalkerImplTestCase(TestCase):
             self.assertEqual((sha, ack_type), self._walker.pop_ack())
         self.assertNoAck()
 
-    def assertAck(self, sha, ack_type=''):
+    def assertAck(self, sha, ack_type=b''):
         self.assertAcks([(sha, ack_type)])
 
     def assertNak(self):
-        self.assertAck(None, 'nak')
+        self.assertAck(None, b'nak')
 
     def assertNextEquals(self, sha):
         self.assertEqual(sha, next(self._impl))
 
 
-@skipIfPY3
 class SingleAckGraphWalkerImplTestCase(AckGraphWalkerImplTestCase):
 
     impl_cls = SingleAckGraphWalkerImpl
@@ -652,7 +638,6 @@ class SingleAckGraphWalkerImplTestCase(AckGraphWalkerImplTestCase):
         self.assertNak()
 
 
-@skipIfPY3
 class MultiAckGraphWalkerImplTestCase(AckGraphWalkerImplTestCase):
 
     impl_cls = MultiAckGraphWalkerImpl
@@ -664,11 +649,11 @@ class MultiAckGraphWalkerImplTestCase(AckGraphWalkerImplTestCase):
         self.assertNextEquals(ONE)
         self._walker.done = True
         self._impl.ack(ONE)
-        self.assertAck(ONE, 'continue')
+        self.assertAck(ONE, b'continue')
 
         self.assertNextEquals(THREE)
         self._impl.ack(THREE)
-        self.assertAck(THREE, 'continue')
+        self.assertAck(THREE, b'continue')
 
         self.assertNextEquals(None)
         self.assertAck(THREE)
@@ -679,7 +664,7 @@ class MultiAckGraphWalkerImplTestCase(AckGraphWalkerImplTestCase):
 
         self.assertNextEquals(ONE)
         self._impl.ack(ONE)
-        self.assertAck(ONE, 'continue')
+        self.assertAck(ONE, b'continue')
 
         self.assertNextEquals(THREE)
         self.assertNoAck()
@@ -690,11 +675,11 @@ class MultiAckGraphWalkerImplTestCase(AckGraphWalkerImplTestCase):
 
     def test_multi_ack_flush(self):
         self._walker.lines = [
-          ('have', TWO),
+          (b'have', TWO),
           (None, None),
-          ('have', ONE),
-          ('have', THREE),
-          ('done', None),
+          (b'have', ONE),
+          (b'have', THREE),
+          (b'done', None),
           ]
         self.assertNextEquals(TWO)
         self.assertNoAck()
@@ -704,11 +689,11 @@ class MultiAckGraphWalkerImplTestCase(AckGraphWalkerImplTestCase):
 
         self._walker.done = True
         self._impl.ack(ONE)
-        self.assertAck(ONE, 'continue')
+        self.assertAck(ONE, b'continue')
 
         self.assertNextEquals(THREE)
         self._impl.ack(THREE)
-        self.assertAck(THREE, 'continue')
+        self.assertAck(THREE, b'continue')
 
         self.assertNextEquals(None)
         self.assertAck(THREE)
@@ -727,7 +712,6 @@ class MultiAckGraphWalkerImplTestCase(AckGraphWalkerImplTestCase):
         self.assertNak()
 
 
-@skipIfPY3
 class MultiAckDetailedGraphWalkerImplTestCase(AckGraphWalkerImplTestCase):
 
     impl_cls = MultiAckDetailedGraphWalkerImpl
@@ -739,11 +723,11 @@ class MultiAckDetailedGraphWalkerImplTestCase(AckGraphWalkerImplTestCase):
         self.assertNextEquals(ONE)
         self._walker.done = True
         self._impl.ack(ONE)
-        self.assertAcks([(ONE, 'common'), (ONE, 'ready')])
+        self.assertAcks([(ONE, b'common'), (ONE, b'ready')])
 
         self.assertNextEquals(THREE)
         self._impl.ack(THREE)
-        self.assertAck(THREE, 'ready')
+        self.assertAck(THREE, b'ready')
 
         self.assertNextEquals(None)
         self.assertAck(THREE)
@@ -754,7 +738,7 @@ class MultiAckDetailedGraphWalkerImplTestCase(AckGraphWalkerImplTestCase):
 
         self.assertNextEquals(ONE)
         self._impl.ack(ONE)
-        self.assertAck(ONE, 'common')
+        self.assertAck(ONE, b'common')
 
         self.assertNextEquals(THREE)
         self.assertNoAck()
@@ -766,11 +750,11 @@ class MultiAckDetailedGraphWalkerImplTestCase(AckGraphWalkerImplTestCase):
     def test_multi_ack_flush(self):
         # same as ack test but contains a flush-pkt in the middle
         self._walker.lines = [
-          ('have', TWO),
+          (b'have', TWO),
           (None, None),
-          ('have', ONE),
-          ('have', THREE),
-          ('done', None),
+          (b'have', ONE),
+          (b'have', THREE),
+          (b'done', None),
           ]
         self.assertNextEquals(TWO)
         self.assertNoAck()
@@ -780,11 +764,11 @@ class MultiAckDetailedGraphWalkerImplTestCase(AckGraphWalkerImplTestCase):
 
         self._walker.done = True
         self._impl.ack(ONE)
-        self.assertAcks([(ONE, 'common'), (ONE, 'ready')])
+        self.assertAcks([(ONE, b'common'), (ONE, b'ready')])
 
         self.assertNextEquals(THREE)
         self._impl.ack(THREE)
-        self.assertAck(THREE, 'ready')
+        self.assertAck(THREE, b'ready')
 
         self.assertNextEquals(None)
         self.assertAck(THREE)
@@ -805,11 +789,11 @@ class MultiAckDetailedGraphWalkerImplTestCase(AckGraphWalkerImplTestCase):
     def test_multi_ack_nak_flush(self):
         # same as nak test but contains a flush-pkt in the middle
         self._walker.lines = [
-          ('have', TWO),
+          (b'have', TWO),
           (None, None),
-          ('have', ONE),
-          ('have', THREE),
-          ('done', None),
+          (b'have', ONE),
+          (b'have', THREE),
+          (b'done', None),
           ]
         self.assertNextEquals(TWO)
         self.assertNoAck()
@@ -841,7 +825,6 @@ class MultiAckDetailedGraphWalkerImplTestCase(AckGraphWalkerImplTestCase):
         self.assertNak()
 
 
-@skipIfPY3
 class FileSystemBackendTests(TestCase):
     """Tests for FileSystemBackend."""
 
@@ -870,25 +853,23 @@ class FileSystemBackendTests(TestCase):
                           lambda: backend.open_repository('/ups'))
 
 
-@skipIfPY3
 class DictBackendTests(TestCase):
     """Tests for DictBackend."""
 
     def test_nonexistant(self):
         repo = MemoryRepo.init_bare([], {})
-        backend = DictBackend({'/': repo})
+        backend = DictBackend({b'/': repo})
         self.assertRaises(NotGitRepository,
             backend.open_repository, "/does/not/exist/unless/foo")
 
     def test_bad_repo_path(self):
         repo = MemoryRepo.init_bare([], {})
-        backend = DictBackend({'/': repo})
+        backend = DictBackend({b'/': repo})
 
         self.assertRaises(NotGitRepository,
                           lambda: backend.open_repository('/ups'))
 
 
-@skipIfPY3
 class ServeCommandTests(TestCase):
     """Tests for serve_command."""
 
@@ -897,24 +878,23 @@ class ServeCommandTests(TestCase):
         self.backend = DictBackend({})
 
     def serve_command(self, handler_cls, args, inf, outf):
-        return serve_command(handler_cls, ["test"] + args, backend=self.backend,
+        return serve_command(handler_cls, [b"test"] + args, backend=self.backend,
             inf=inf, outf=outf)
 
     def test_receive_pack(self):
         commit = make_commit(id=ONE, parents=[], commit_time=111)
-        self.backend.repos["/"] = MemoryRepo.init_bare(
-            [commit], {"refs/heads/master": commit.id})
+        self.backend.repos[b"/"] = MemoryRepo.init_bare(
+            [commit], {b"refs/heads/master": commit.id})
         outf = BytesIO()
-        exitcode = self.serve_command(ReceivePackHandler, ["/"], BytesIO("0000"), outf)
+        exitcode = self.serve_command(ReceivePackHandler, [b"/"], BytesIO(b"0000"), outf)
         outlines = outf.getvalue().splitlines()
         self.assertEqual(2, len(outlines))
-        self.assertEqual("1111111111111111111111111111111111111111 refs/heads/master",
-            outlines[0][4:].split("\x00")[0])
-        self.assertEqual("0000", outlines[-1])
+        self.assertEqual(b"1111111111111111111111111111111111111111 refs/heads/master",
+            outlines[0][4:].split(b"\x00")[0])
+        self.assertEqual(b"0000", outlines[-1])
         self.assertEqual(0, exitcode)
 
 
-@skipIfPY3
 class UpdateServerInfoTests(TestCase):
     """Tests for update_server_info."""
 
@@ -932,9 +912,9 @@ class UpdateServerInfoTests(TestCase):
 
     def test_simple(self):
         commit_id = self.repo.do_commit(
-            message="foo",
-            committer="Joe Example <joe@example.com>",
-            ref="refs/heads/foo")
+            message=b"foo",
+            committer=b"Joe Example <joe@example.com>",
+            ref=b"refs/heads/foo")
         update_server_info(self.repo)
         with open(os.path.join(self.path, ".git", "info", "refs"), 'rb') as f:
             self.assertEqual(f.read(), commit_id + b'\trefs/heads/foo\n')

+ 10 - 10
dulwich/tests/test_utils.py

@@ -47,7 +47,7 @@ class BuildCommitGraphTest(TestCase):
         self.assertEqual([], c1.parents)
         self.assertEqual([c1.id], c2.parents)
         self.assertEqual(c1.tree, c2.tree)
-        self.assertEqual([], list(self.store[c1.tree].iteritems()))
+        self.assertEqual([], self.store[c1.tree].items())
         self.assertTrue(c2.commit_time > c1.commit_time)
 
     def test_merge(self):
@@ -62,19 +62,19 @@ class BuildCommitGraphTest(TestCase):
                           [[1], [3, 2], [2, 1]])
 
     def test_trees(self):
-        a1 = make_object(Blob, data='aaa1')
-        a2 = make_object(Blob, data='aaa2')
+        a1 = make_object(Blob, data=b'aaa1')
+        a2 = make_object(Blob, data=b'aaa2')
         c1, c2 = build_commit_graph(self.store, [[1], [2, 1]],
-                                    trees={1: [('a', a1)],
-                                           2: [('a', a2, 0o100644)]})
-        self.assertEqual((0o100644, a1.id), self.store[c1.tree]['a'])
-        self.assertEqual((0o100644, a2.id), self.store[c2.tree]['a'])
+                                    trees={1: [(b'a', a1)],
+                                           2: [(b'a', a2, 0o100644)]})
+        self.assertEqual((0o100644, a1.id), self.store[c1.tree][b'a'])
+        self.assertEqual((0o100644, a2.id), self.store[c2.tree][b'a'])
 
     def test_attrs(self):
         c1, c2 = build_commit_graph(self.store, [[1], [2, 1]],
-                                    attrs={1: {'message': 'Hooray!'}})
-        self.assertEqual('Hooray!', c1.message)
-        self.assertEqual('Commit 2', c2.message)
+                                    attrs={1: {'message': b'Hooray!'}})
+        self.assertEqual(b'Hooray!', c1.message)
+        self.assertEqual(b'Commit 2', c2.message)
 
     def test_commit_time(self):
         c1, c2, c3 = build_commit_graph(self.store, [[1], [2, 1], [3, 2]],

+ 0 - 2
dulwich/tests/test_walk.py

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

+ 54 - 60
dulwich/tests/test_web.py

@@ -28,7 +28,6 @@ from dulwich.object_store import (
     )
 from dulwich.objects import (
     Blob,
-    Tag,
     )
 from dulwich.repo import (
     BaseRepo,
@@ -62,7 +61,6 @@ from dulwich.web import (
 from dulwich.tests.utils import (
     make_object,
     make_tag,
-    skipIfPY3,
     )
 
 
@@ -112,12 +110,11 @@ def _test_backend(objects, refs=None, named_files=None):
     if not named_files:
         named_files = {}
     repo = MemoryRepo.init_bare(objects, refs)
-    for path, contents in named_files.iteritems():
+    for path, contents in named_files.items():
         repo._put_named_file(path, contents)
     return DictBackend({'/': repo})
 
 
-@skipIfPY3
 class DumbHandlersTestCase(WebTestCase):
 
     def test_send_file_not_found(self):
@@ -125,16 +122,16 @@ class DumbHandlersTestCase(WebTestCase):
         self.assertEqual(HTTP_NOT_FOUND, self._status)
 
     def test_send_file(self):
-        f = BytesIO('foobar')
-        output = ''.join(send_file(self._req, f, 'some/thing'))
-        self.assertEqual('foobar', output)
+        f = BytesIO(b'foobar')
+        output = b''.join(send_file(self._req, f, 'some/thing'))
+        self.assertEqual(b'foobar', output)
         self.assertEqual(HTTP_OK, self._status)
         self.assertContentTypeEquals('some/thing')
         self.assertTrue(f.closed)
 
     def test_send_file_buffered(self):
         bufsize = 10240
-        xs = 'x' * bufsize
+        xs = b'x' * bufsize
         f = BytesIO(2 * xs)
         self.assertEqual([xs, xs],
                           list(send_file(self._req, f, 'some/thing')))
@@ -168,19 +165,19 @@ class DumbHandlersTestCase(WebTestCase):
         self.assertFalse(self._req.cached)
 
     def test_get_text_file(self):
-        backend = _test_backend([], named_files={'description': 'foo'})
+        backend = _test_backend([], named_files={'description': b'foo'})
         mat = re.search('.*', 'description')
-        output = ''.join(get_text_file(self._req, backend, mat))
-        self.assertEqual('foo', output)
+        output = b''.join(get_text_file(self._req, backend, mat))
+        self.assertEqual(b'foo', output)
         self.assertEqual(HTTP_OK, self._status)
         self.assertContentTypeEquals('text/plain')
         self.assertFalse(self._req.cached)
 
     def test_get_loose_object(self):
-        blob = make_object(Blob, data='foo')
+        blob = make_object(Blob, data=b'foo')
         backend = _test_backend([blob])
-        mat = re.search('^(..)(.{38})$', blob.id)
-        output = ''.join(get_loose_object(self._req, backend, mat))
+        mat = re.search('^(..)(.{38})$', blob.id.decode('ascii'))
+        output = b''.join(get_loose_object(self._req, backend, mat))
         self.assertEqual(blob.as_legacy_object(), output)
         self.assertEqual(HTTP_OK, self._status)
         self.assertContentTypeEquals('application/x-git-loose-object')
@@ -192,9 +189,9 @@ class DumbHandlersTestCase(WebTestCase):
         self.assertEqual(HTTP_NOT_FOUND, self._status)
 
     def test_get_loose_object_error(self):
-        blob = make_object(Blob, data='foo')
+        blob = make_object(Blob, data=b'foo')
         backend = _test_backend([blob])
-        mat = re.search('^(..)(.{38})$', blob.id)
+        mat = re.search('^(..)(.{38})$', blob.id.decode('ascii'))
 
         def as_legacy_object_error():
             raise IOError
@@ -205,20 +202,20 @@ class DumbHandlersTestCase(WebTestCase):
 
     def test_get_pack_file(self):
         pack_name = os.path.join('objects', 'pack', 'pack-%s.pack' % ('1' * 40))
-        backend = _test_backend([], named_files={pack_name: 'pack contents'})
+        backend = _test_backend([], named_files={pack_name: b'pack contents'})
         mat = re.search('.*', pack_name)
-        output = ''.join(get_pack_file(self._req, backend, mat))
-        self.assertEqual('pack contents', output)
+        output = b''.join(get_pack_file(self._req, backend, mat))
+        self.assertEqual(b'pack contents', output)
         self.assertEqual(HTTP_OK, self._status)
         self.assertContentTypeEquals('application/x-git-packed-objects')
         self.assertTrue(self._req.cached)
 
     def test_get_idx_file(self):
         idx_name = os.path.join('objects', 'pack', 'pack-%s.idx' % ('1' * 40))
-        backend = _test_backend([], named_files={idx_name: 'idx contents'})
+        backend = _test_backend([], named_files={idx_name: b'idx contents'})
         mat = re.search('.*', idx_name)
-        output = ''.join(get_idx_file(self._req, backend, mat))
-        self.assertEqual('idx contents', output)
+        output = b''.join(get_idx_file(self._req, backend, mat))
+        self.assertEqual(b'idx contents', output)
         self.assertEqual(HTTP_OK, self._status)
         self.assertContentTypeEquals('application/x-git-packed-objects-toc')
         self.assertTrue(self._req.cached)
@@ -226,26 +223,26 @@ class DumbHandlersTestCase(WebTestCase):
     def test_get_info_refs(self):
         self._environ['QUERY_STRING'] = ''
 
-        blob1 = make_object(Blob, data='1')
-        blob2 = make_object(Blob, data='2')
-        blob3 = make_object(Blob, data='3')
+        blob1 = make_object(Blob, data=b'1')
+        blob2 = make_object(Blob, data=b'2')
+        blob3 = make_object(Blob, data=b'3')
 
-        tag1 = make_tag(blob2, name='tag-tag')
+        tag1 = make_tag(blob2, name=b'tag-tag')
 
         objects = [blob1, blob2, blob3, tag1]
         refs = {
-          'HEAD': '000',
-          'refs/heads/master': blob1.id,
-          'refs/tags/tag-tag': tag1.id,
-          'refs/tags/blob-tag': blob3.id,
+          b'HEAD': b'000',
+          b'refs/heads/master': blob1.id,
+          b'refs/tags/tag-tag': tag1.id,
+          b'refs/tags/blob-tag': blob3.id,
           }
         backend = _test_backend(objects, refs=refs)
 
         mat = re.search('.*', '//info/refs')
-        self.assertEqual(['%s\trefs/heads/master\n' % blob1.id,
-                           '%s\trefs/tags/blob-tag\n' % blob3.id,
-                           '%s\trefs/tags/tag-tag\n' % tag1.id,
-                           '%s\trefs/tags/tag-tag^{}\n' % blob2.id],
+        self.assertEqual([blob1.id + b'\trefs/heads/master\n',
+                           blob3.id + b'\trefs/tags/blob-tag\n',
+                           tag1.id + b'\trefs/tags/tag-tag\n',
+                           blob2.id + b'\trefs/tags/tag-tag^{}\n'],
                           list(get_info_refs(self._req, backend, mat)))
         self.assertEqual(HTTP_OK, self._status)
         self.assertContentTypeEquals('text/plain')
@@ -273,16 +270,15 @@ class DumbHandlersTestCase(WebTestCase):
         repo = BaseRepo(store, None)
         backend = DictBackend({'/': repo})
         mat = re.search('.*', '//info/packs')
-        output = ''.join(get_info_packs(self._req, backend, mat))
-        expected = 'P pack-%s.pack\n' * 3
-        expected %= ('1' * 40, '2' * 40, '3' * 40)
+        output = b''.join(get_info_packs(self._req, backend, mat))
+        expected = b''.join(
+            [(b'P pack-' + s + b'.pack\n') for s in [b'1' * 40, b'2' * 40, b'3' * 40]])
         self.assertEqual(expected, output)
         self.assertEqual(HTTP_OK, self._status)
         self.assertContentTypeEquals('text/plain')
         self.assertFalse(self._req.cached)
 
 
-@skipIfPY3
 class SmartHandlersTestCase(WebTestCase):
 
     class _TestUploadPackHandler(object):
@@ -294,7 +290,7 @@ class SmartHandlersTestCase(WebTestCase):
             self.advertise_refs = advertise_refs
 
         def handle(self):
-            self.proto.write('handled input: %s' % self.proto.recv(1024))
+            self.proto.write(b'handled input: ' + self.proto.recv(1024))
 
     def _make_handler(self, *args, **kwargs):
         self._handler = self._TestUploadPackHandler(*args, **kwargs)
@@ -311,7 +307,7 @@ class SmartHandlersTestCase(WebTestCase):
         self.assertFalse(self._req.cached)
 
     def _run_handle_service_request(self, content_length=None):
-        self._environ['wsgi.input'] = BytesIO('foo')
+        self._environ['wsgi.input'] = BytesIO(b'foo')
         if content_length is not None:
             self._environ['CONTENT_LENGTH'] = content_length
         mat = re.search('.*', '/git-upload-pack')
@@ -320,7 +316,7 @@ class SmartHandlersTestCase(WebTestCase):
         write_output = self._output.getvalue()
         # Ensure all output was written via the write callback.
         self.assertEqual('', handler_output)
-        self.assertEqual('handled input: foo', write_output)
+        self.assertEqual(b'handled input: foo', write_output)
         self.assertContentTypeEquals('application/x-git-upload-pack-result')
         self.assertFalse(self._handler.advertise_refs)
         self.assertTrue(self._handler.http_req)
@@ -337,45 +333,44 @@ class SmartHandlersTestCase(WebTestCase):
 
     def test_get_info_refs_unknown(self):
         self._environ['QUERY_STRING'] = 'service=git-evil-handler'
-        content = list(get_info_refs(self._req, 'backend', None))
+        content = list(get_info_refs(self._req, b'backend', None))
         self.assertFalse('git-evil-handler' in "".join(content))
         self.assertEqual(HTTP_FORBIDDEN, self._status)
         self.assertFalse(self._req.cached)
 
     def test_get_info_refs(self):
-        self._environ['wsgi.input'] = BytesIO('foo')
+        self._environ['wsgi.input'] = BytesIO(b'foo')
         self._environ['QUERY_STRING'] = 'service=git-upload-pack'
 
         mat = re.search('.*', '/git-upload-pack')
-        handler_output = ''.join(get_info_refs(self._req, 'backend', mat))
+        handler_output = b''.join(get_info_refs(self._req, b'backend', mat))
         write_output = self._output.getvalue()
-        self.assertEqual(('001e# service=git-upload-pack\n'
-                           '0000'
+        self.assertEqual((b'001e# service=git-upload-pack\n'
+                           b'0000'
                            # input is ignored by the handler
-                           'handled input: '), write_output)
+                           b'handled input: '), write_output)
         # Ensure all output was written via the write callback.
-        self.assertEqual('', handler_output)
+        self.assertEqual(b'', handler_output)
         self.assertTrue(self._handler.advertise_refs)
         self.assertTrue(self._handler.http_req)
         self.assertFalse(self._req.cached)
 
 
-@skipIfPY3
 class LengthLimitedFileTestCase(TestCase):
     def test_no_cutoff(self):
-        f = _LengthLimitedFile(BytesIO('foobar'), 1024)
-        self.assertEqual('foobar', f.read())
+        f = _LengthLimitedFile(BytesIO(b'foobar'), 1024)
+        self.assertEqual(b'foobar', f.read())
 
     def test_cutoff(self):
-        f = _LengthLimitedFile(BytesIO('foobar'), 3)
-        self.assertEqual('foo', f.read())
-        self.assertEqual('', f.read())
+        f = _LengthLimitedFile(BytesIO(b'foobar'), 3)
+        self.assertEqual(b'foo', f.read())
+        self.assertEqual(b'', f.read())
 
     def test_multiple_reads(self):
-        f = _LengthLimitedFile(BytesIO('foobar'), 3)
-        self.assertEqual('fo', f.read(2))
-        self.assertEqual('o', f.read(2))
-        self.assertEqual('', f.read())
+        f = _LengthLimitedFile(BytesIO(b'foobar'), 3)
+        self.assertEqual(b'fo', f.read(2))
+        self.assertEqual(b'o', f.read(2))
+        self.assertEqual(b'', f.read())
 
 
 class HTTPGitRequestTestCase(WebTestCase):
@@ -419,7 +414,6 @@ class HTTPGitRequestTestCase(WebTestCase):
         self.assertEqual(402, self._status)
 
 
-@skipIfPY3
 class HTTPGitApplicationTestCase(TestCase):
 
     def setUp(self):
@@ -460,7 +454,7 @@ class GunzipTestCase(HTTPGitApplicationTestCase):
     __doc__ = """TestCase for testing the GunzipFilter, ensuring the wsgi.input
     is correctly decompressed and headers are corrected.
     """
-    example_text = __doc__
+    example_text = __doc__.encode('ascii')
 
     def setUp(self):
         super(GunzipTestCase, self).setUp()

+ 8 - 6
dulwich/tests/utils.py

@@ -60,7 +60,7 @@ from dulwich.tests import (
 F = 0o100644  # Shorthand mode for Files.
 
 
-def open_repo(name):
+def open_repo(name, temp_dir=None):
     """Open a copy of a repo in a temporary directory.
 
     Use this function for accessing repos in dulwich/tests/data/repos to avoid
@@ -69,6 +69,8 @@ def open_repo(name):
 
     :param name: The name of the repository, relative to
         dulwich/tests/data/repos
+    :param temp_dir: temporary directory to initialize to. If not provided, a
+        temporary directory will be created.
     :returns: An initialized Repo object that lives in a temporary directory.
     """
     temp_dir = tempfile.mkdtemp()
@@ -80,7 +82,7 @@ def open_repo(name):
 
 def tear_down_repo(repo):
     """Tear down a test repository."""
-    temp_dir = os.path.dirname(repo.path.rstrip(os.sep))
+    temp_dir = os.path.dirname(repo._path_bytes.rstrip(os.sep.encode(sys.getfilesystemencoding())))
     shutil.rmtree(temp_dir)
 
 
@@ -145,12 +147,12 @@ def make_tag(target, **attrs):
     target_id = target.id
     target_type = object_class(target.type_name)
     default_time = int(time.mktime(datetime.datetime(2010, 1, 1).timetuple()))
-    all_attrs = {'tagger': 'Test Author <test@nodomain.com>',
+    all_attrs = {'tagger': b'Test Author <test@nodomain.com>',
                  'tag_time': default_time,
                  'tag_timezone': 0,
-                 'message': 'Test message.',
+                 'message': b'Test message.',
                  'object': (target_type, target_id),
-                 'name': 'Test Tag',
+                 'name': b'Test Tag',
                  }
     all_attrs.update(attrs)
     return make_object(Tag, **all_attrs)
@@ -323,7 +325,7 @@ def build_commit_graph(object_store, commit_spec, trees=None, attrs=None):
         tree_id = commit_tree(object_store, blobs)
 
         commit_attrs = {
-            'message': 'Commit %i' % commit_num,
+            'message': ('Commit %i' % commit_num).encode('ascii'),
             'parents': parent_ids,
             'tree': tree_id,
             'commit_time': commit_time,

+ 2 - 0
dulwich/walk.py

@@ -226,6 +226,8 @@ class Walker(object):
         if order not in ALL_ORDERS:
             raise ValueError('Unknown walk order %s' % order)
         self.store = store
+        if not isinstance(include, list):
+            include = [include]
         self.include = include
         self.excluded = set(exclude or [])
         self.order = order

+ 4 - 4
dulwich/web.py

@@ -139,7 +139,7 @@ def get_text_file(req, backend, mat):
 
 
 def get_loose_object(req, backend, mat):
-    sha = mat.group(1) + mat.group(2)
+    sha = (mat.group(1) + mat.group(2)).encode('ascii')
     logger.info('Sending loose object %s', sha)
     object_store = get_repo(backend, mat).object_store
     if not object_store.contains_loose(sha):
@@ -184,7 +184,7 @@ def get_info_refs(req, backend, mat):
         proto = ReceivableProtocol(BytesIO().read, write)
         handler = handler_cls(backend, [url_prefix(mat)], proto,
                               http_req=req, advertise_refs=True)
-        handler.proto.write_pkt_line('# service=%s\n' % service)
+        handler.proto.write_pkt_line(b'# service=' + service.encode('ascii') + b'\n')
         handler.proto.write_pkt_line(None)
         handler.handle()
     else:
@@ -219,7 +219,7 @@ class _LengthLimitedFile(object):
 
     def read(self, size=-1):
         if self._bytes_avail <= 0:
-            return ''
+            return b''
         if size == -1 or size > self._bytes_avail:
             size = self._bytes_avail
         self._bytes_avail -= size
@@ -344,7 +344,7 @@ class HTTPGitApplication(object):
                              handlers=self.handlers)
         # environ['QUERY_STRING'] has qs args
         handler = None
-        for smethod, spath in self.services.iterkeys():
+        for smethod, spath in self.services.keys():
             if smethod != method:
                 continue
             mat = spath.search(path)

+ 0 - 5
setup.cfg

@@ -1,5 +0,0 @@
-[egg_info]
-tag_build = 
-tag_date = 0
-tag_svn_revision = 0
-

+ 17 - 10
setup.py

@@ -8,7 +8,7 @@ except ImportError:
     from distutils.core import setup, Extension
 from distutils.core import Distribution
 
-dulwich_version_string = '0.10.1a'
+dulwich_version_string = '0.10.2'
 
 include_dirs = []
 # Windows MSVC support
@@ -48,7 +48,7 @@ if sys.platform == 'darwin' and os.path.exists('/usr/bin/xcodebuild'):
 
 if sys.version_info[0] == 2:
     tests_require = ['fastimport', 'mock']
-    if not '__pypy__' in sys.modules:
+    if not '__pypy__' in sys.modules and not sys.platform == 'win32':
         tests_require.extend(['gevent', 'geventhttpclient'])
 else:
     # fastimport, gevent, geventhttpclient are not available for PY3
@@ -57,6 +57,20 @@ else:
 if sys.version_info < (2, 7):
     tests_require.append('unittest2')
 
+if sys.version_info[0] > 2 and sys.platform == 'win32':
+    # C Modules don't build for python3 windows, and prevent tests from running
+    ext_modules = []
+else:
+    ext_modules = [
+        Extension('dulwich._objects', ['dulwich/_objects.c'],
+                  include_dirs=include_dirs),
+        Extension('dulwich._pack', ['dulwich/_pack.c'],
+                  include_dirs=include_dirs),
+        Extension('dulwich._diff_tree', ['dulwich/_diff_tree.c'],
+                  include_dirs=include_dirs),
+    ]
+
+
 setup(name='dulwich',
       description='Python Git Library',
       keywords='git',
@@ -86,14 +100,7 @@ setup(name='dulwich',
           'Operating System :: POSIX',
           'Topic :: Software Development :: Version Control',
       ],
-      ext_modules=[
-          Extension('dulwich._objects', ['dulwich/_objects.c'],
-                    include_dirs=include_dirs),
-          Extension('dulwich._pack', ['dulwich/_pack.c'],
-              include_dirs=include_dirs),
-          Extension('dulwich._diff_tree', ['dulwich/_diff_tree.c'],
-              include_dirs=include_dirs),
-      ],
+      ext_modules=ext_modules,
       test_suite='dulwich.tests.test_suite',
       tests_require=tests_require,
       distclass=DulwichDistribution,