Pārlūkot izejas kodu

Imported Upstream version 0.11.1

Jelmer Vernooij 9 gadi atpakaļ
vecāks
revīzija
7c78398c33
51 mainītis faili ar 1095 papildinājumiem un 556 dzēšanām
  1. 0 13
      .gitignore
  2. 0 4
      .testr.conf
  3. 0 16
      .travis.yml
  4. 1 1
      AUTHORS
  5. 4 0
      MANIFEST.in
  6. 4 0
      Makefile
  7. 21 1
      NEWS
  8. 29 0
      PKG-INFO
  9. 4 4
      README.md
  10. 0 133
      README.swift
  11. 11 2
      appveyor.yml
  12. 40 0
      bin/dulwich
  13. 1 1
      docs/index.txt
  14. 0 2
      docs/tutorial/.gitignore
  15. 1 1
      dulwich.cfg
  16. 29 0
      dulwich.egg-info/PKG-INFO
  17. 187 0
      dulwich.egg-info/SOURCES.txt
  18. 1 0
      dulwich.egg-info/dependency_links.txt
  19. 1 0
      dulwich.egg-info/pbr.json
  20. 1 0
      dulwich.egg-info/top_level.txt
  21. 1 1
      dulwich/__init__.py
  22. 2 2
      dulwich/_objects.c
  23. 74 21
      dulwich/client.py
  24. 42 26
      dulwich/config.py
  25. 7 5
      dulwich/file.py
  26. 15 1
      dulwich/objects.py
  27. 94 4
      dulwich/objectspec.py
  28. 54 35
      dulwich/pack.py
  29. 128 44
      dulwich/porcelain.py
  30. 5 4
      dulwich/refs.py
  31. 22 2
      dulwich/repo.py
  32. 3 2
      dulwich/server.py
  33. 31 10
      dulwich/tests/compat/server_utils.py
  34. 31 16
      dulwich/tests/compat/test_client.py
  35. 5 0
      dulwich/tests/compat/test_web.py
  36. 6 3
      dulwich/tests/test_blackbox.py
  37. 26 8
      dulwich/tests/test_client.py
  38. 24 17
      dulwich/tests/test_config.py
  39. 4 4
      dulwich/tests/test_file.py
  40. 1 0
      dulwich/tests/test_object_store.py
  41. 73 0
      dulwich/tests/test_objectspec.py
  42. 1 1
      dulwich/tests/test_pack.py
  43. 37 15
      dulwich/tests/test_porcelain.py
  44. 17 0
      dulwich/tests/test_refs.py
  45. 25 0
      dulwich/tests/test_repository.py
  46. 11 11
      dulwich/tests/test_server.py
  47. 2 18
      examples/clone.py
  48. 0 110
      relicensing-apachev2.txt
  49. 6 0
      setup.cfg
  50. 9 8
      setup.py
  51. 4 10
      tox.ini

+ 0 - 13
.gitignore

@@ -1,13 +0,0 @@
-_trial_temp
-build
-MANIFEST
-dist
-apidocs
-*,cover
-.testrepository
-*.pyc
-*.so
-*~
-*.swp
-docs/tutorial/index.html
-dulwich.egg-info/

+ 0 - 4
.testr.conf

@@ -1,4 +0,0 @@
-[DEFAULT]
-test_command=PYTHONPATH=. python -m subunit.run $IDOPTION $LISTOPT dulwich.tests.test_suite
-test_id_option=--load-list $IDFILE
-test_list_option=--list

+ 0 - 16
.travis.yml

@@ -1,16 +0,0 @@
-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

+ 1 - 1
AUTHORS

@@ -1,4 +1,4 @@
-Jelmer Vernooij <jelmer@samba.org>
+Jelmer Vernooij <jelmer@jelmer.uk>
 James Westby <jw+debian@jameswestby.net>
 James Westby <jw+debian@jameswestby.net>
 John Carr <john.carr@unrouted.co.uk>
 John Carr <john.carr@unrouted.co.uk>
 Dave Borowitz <dborowitz@google.com>
 Dave Borowitz <dborowitz@google.com>

+ 4 - 0
MANIFEST.in

@@ -4,8 +4,12 @@ include README.md
 include Makefile
 include Makefile
 include COPYING
 include COPYING
 include HACKING
 include HACKING
+include CONTRIBUTING
 include setup.cfg
 include setup.cfg
 include dulwich/stdint.h
 include dulwich/stdint.h
 recursive-include docs conf.py *.txt Makefile make.bat
 recursive-include docs conf.py *.txt Makefile make.bat
 recursive-include examples *.py
 recursive-include examples *.py
 graft dulwich/tests/data
 graft dulwich/tests/data
+include tox.ini
+include dulwich.cfg
+include appveyor.yml

+ 4 - 0
Makefile

@@ -15,6 +15,10 @@ DESTDIR=/
 all: build
 all: build
 
 
 doc:: pydoctor
 doc:: pydoctor
+doc:: sphinx
+
+sphinx::
+	$(MAKE) -C docs html
 
 
 pydoctor::
 pydoctor::
 	$(PYDOCTOR) --make-html -c dulwich.cfg
 	$(PYDOCTOR) --make-html -c dulwich.cfg

+ 21 - 1
NEWS

@@ -1,4 +1,8 @@
-0.10.2  UNRELEASED
+0.11.1	2015-09-13
+
+ Fix-up release to exclude broken blame.py file.
+
+0.11.0	2015-09-13
 
 
  IMPROVEMENTS
  IMPROVEMENTS
 
 
@@ -12,12 +16,28 @@
     capabilities. (Jelmer Vernooij)
     capabilities. (Jelmer Vernooij)
   * Switched `default_local_git_client_cls` to `LocalGitClient`.
   * Switched `default_local_git_client_cls` to `LocalGitClient`.
     (Gary van der Merwe)
     (Gary van der Merwe)
+  * Add `porcelain.ls_remote` and `GitClient.get_refs`.
+    (Michael Edgar)
+  * Add `Repo.discover` method. (B. M. Corser)
+  * Add `dulwich.objectspec.parse_refspec`. (Jelmer Vernooij)
+  * Add `porcelain.pack_objects` and `porcelain.repack`.
+    (Jelmer Vernooij)
 
 
  BUG FIXES
  BUG FIXES
 
 
   * Fix handling of 'done' in graph walker and implement the
   * Fix handling of 'done' in graph walker and implement the
     'no-done' capability. (Tommy Yu, #88)
     'no-done' capability. (Tommy Yu, #88)
 
 
+  * Avoid recursion limit issues resolving deltas. (William Grant, #81)
+
+  * Allow arguments in local client binary path overrides.
+    (Jelmer Vernooij)
+
+  * Fix handling of commands with arguments in paramiko SSH
+    client. (Andreas Klöckner, Jelmer Vernooij, #363)
+
+  * Fix parsing of quoted strings in configs. (Jelmer Vernooij, #305)
+
 0.10.1  2015-03-25
 0.10.1  2015-03-25
 
 
  BUG FIXES
  BUG FIXES

+ 29 - 0
PKG-INFO

@@ -0,0 +1,29 @@
+Metadata-Version: 1.1
+Name: dulwich
+Version: 0.11.1
+Summary: Python Git Library
+Home-page: https://www.dulwich.io/
+Author: Jelmer Vernooij
+Author-email: jelmer@jelmer.uk
+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: Programming Language :: Python :: 3.4
+Classifier: Programming Language :: Python :: Implementation :: CPython
+Classifier: Programming Language :: Python :: Implementation :: PyPy
+Classifier: Operating System :: POSIX
+Classifier: Topic :: Software Development :: Version Control

+ 4 - 4
README.md

@@ -5,8 +5,8 @@ This is the Dulwich project.
 It aims to provide an interface to git repos (both local and remote) that
 It aims to provide an interface to git repos (both local and remote) that
 doesn't call out to git directly but instead uses pure Python.
 doesn't call out to git directly but instead uses pure Python.
 
 
-Homepage: https://samba.org/~jelmer/dulwich/
-Author: Jelmer Vernooij <jelmer@samba.org>
+Homepage: https://www.dulwich.io/
+Author: Jelmer Vernooij <jelmer@jelmer.uk>
 
 
 The project is named after the part of London that Mr. and Mrs. Git live in
 The project is named after the part of London that Mr. and Mrs. Git live in
 in the particular Monty Python sketch.
 in the particular Monty Python sketch.
@@ -31,11 +31,11 @@ Further documentation
 
 
 The dulwich documentation can be found in doc/ and on the web:
 The dulwich documentation can be found in doc/ and on the web:
 
 
-http://www.samba.org/~jelmer/dulwich/docs/
+https://www.dulwich.io/docs/
 
 
 The API reference can be generated using pydoctor, by running "make pydoctor", or on the web:
 The API reference can be generated using pydoctor, by running "make pydoctor", or on the web:
 
 
-http://www.samba.org/~jelmer/dulwich/apidocs
+https://www.dulwich.io/apidocs
 
 
 Help
 Help
 ----
 ----

+ 0 - 133
README.swift

@@ -1,133 +0,0 @@
-Openstack Swift as backend for Dulwich
-======================================
-Fabien Boucher <fabien.boucher@enovance.com>
-
-The module dulwich/contrib/swift.py implements dulwich.repo.BaseRepo
-in order to being compatible with Openstack Swift.
-We can then use Dulwich as server (Git server) and instead of using
-a regular POSIX file system to store repository objects we use the
-object storage Swift via its own API.
-
-c Git client <---> Dulwich server <---> Openstack Swift API
-
-This implementation is still a work in progress and we can say that
-is a Beta version so you need to be prepared to find bugs.
-
-Configuration file
-------------------
-
-We need to provide some configuration values in order to let Dulwich
-talk and authenticate against Swift. The following config file must
-be used as template:
-
-    [swift]
-    # Authentication URL (Keystone or Swift)
-    auth_url = http://127.0.0.1:5000/v2.0
-    # Authentication version to use
-    auth_ver = 2
-    # The tenant and username separated by a semicolon
-    username = admin;admin
-    # The user password
-    password = pass
-    # The Object storage region to use (auth v2) (Default RegionOne)
-    region_name = RegionOne
-    # The Object storage endpoint URL to use (auth v2) (Default internalURL)
-    endpoint_type = internalURL
-    # Concurrency to use for parallel tasks (Default 10)
-    concurrency = 10
-    # Size of the HTTP pool (Default 10)
-    http_pool_length = 10
-    # Timeout delay for HTTP connections (Default 20)
-    http_timeout = 20
-    # Chunk size to read from pack (Bytes) (Default 12228)
-    chunk_length = 12228
-    # Cache size (MBytes) (Default 20)
-    cache_length = 20
-
-
-Note that for now we use the same tenant to perform the requests
-against Swift. Therefor there is only one Swift account used
-for storing repositories. Each repository will be contained in
-a Swift container.
-
-How to start unittest
----------------------
-
-There is no need to have a Swift cluster running to run the unitests.
-Just run the following command in the Dulwich source directory:
-
-    $ PYTHONPATH=. python -m dulwich.contrib.test_swift
-
-How to start functional tests
------------------------------
-
-We provide some basic tests to perform smoke tests against a real Swift
-cluster. To run those functional tests you need a properly configured
-configuration file. The tests can be run as follow:
-
-    $ DULWICH_SWIFT_CFG=/etc/swift-dul.conf PYTHONPATH=. python -m dulwich.contrib.test_swift_smoke
-
-How to install
---------------
-
-Install the Dulwich library via the setup.py. The dependencies will be
-automatically retrieved from pypi:
-
-    $ python ./setup.py install
-
-How to run the server
----------------------
-
-Start the server using the following command:
-
-    $ python -m dulwich.contrib.swift daemon -c /etc/swift-dul.conf -l 127.0.0.1
-
-Note that a lot of request will be performed against the Swift
-cluster so it is better to start the Dulwich server as close
-as possible of the Swift proxy. The best solution is to run
-the server on the Swift proxy node to reduce the latency.
-
-How to use
-----------
-
-Once you have validated that the functional tests is working as expected and
-the server is running we can init a bare repository. Run this
-command with the name of the repository to create:
-
-    $ python -m dulwich.contrib.swift init -c /etc/swift-dul.conf edeploy
-
-The repository name will be the container that will contain all the Git
-objects for the repository. Then standard c Git client can be used to
-perform operations againt this repository.
-
-As an example we can clone the previously empty bare repository:
-
-    $ git clone git://localhost/edeploy
-
-Then push an existing project in it:
-
-    $ git clone https://github.com/enovance/edeploy.git edeployclone
-    $ cd edeployclone
-    $ git remote add alt git://localhost/edeploy
-    $ git push alt master
-    $ git ls-remote alt
-    9dc50a9a9bff1e232a74e365707f22a62492183e        HEAD
-    9dc50a9a9bff1e232a74e365707f22a62492183e        refs/heads/master
-
-The other Git commands can be used the way you do usually against
-a regular repository.
-
-Note the daemon subcommands starts a Git server listening for the
-Git protocol. Therefor there is no authentication or encryption
-at all between the cGIT client and the GIT server (Dulwich).
-
-Note on the .info file for pack object
---------------------------------------
-
-The Swift interface of Dulwich relies only on the pack format
-to store Git objects. Instead of using only an index (pack-sha.idx)
-along with the pack, we add a second file (pack-sha.info). This file
-is automatically created when a client pushes some references on the
-repository. The purpose of this file is to speed up pack creation
-server side when a client fetches some references. Currently this
-.info format is not optimized and may change in future.

+ 11 - 2
appveyor.yml

@@ -1,20 +1,29 @@
 environment:
 environment:
   matrix:
   matrix:
     - PYTHON: "C:\\Python27"
     - PYTHON: "C:\\Python27"
-      PYTHON_ARCH: "32"
       PYWIN32_URL: "https://downloads.sourceforge.net/project/pywin32/pywin32/Build%20219/pywin32-219.win32-py2.7.exe"
       PYWIN32_URL: "https://downloads.sourceforge.net/project/pywin32/pywin32/Build%20219/pywin32-219.win32-py2.7.exe"
 
 
     - PYTHON: "C:\\Python34"
     - PYTHON: "C:\\Python34"
-      PYTHON_ARCH: "32"
       PYWIN32_URL: "https://downloads.sourceforge.net/project/pywin32/pywin32/Build%20219/pywin32-219.win32-py3.4.exe"
       PYWIN32_URL: "https://downloads.sourceforge.net/project/pywin32/pywin32/Build%20219/pywin32-219.win32-py3.4.exe"
 
 
+    - PYTHON: "C:\\Python27-x64"
+      PYWIN32_URL: "https://downloads.sourceforge.net/project/pywin32/pywin32/Build%20219/pywin32-219.win-amd64-py2.7.exe"
+
+    - PYTHON: "C:\\Python34-x64"
+      PYWIN32_URL: "https://downloads.sourceforge.net/project/pywin32/pywin32/Build%20219/pywin32-219.win-amd64-py3.4.exe"
+
 install:
 install:
   - ps: (new-object net.webclient).DownloadFile($env:PYWIN32_URL, 'c:\\pywin32.exe')
   - ps: (new-object net.webclient).DownloadFile($env:PYWIN32_URL, 'c:\\pywin32.exe')
   - "%PYTHON%/Scripts/easy_install.exe c:\\pywin32.exe"
   - "%PYTHON%/Scripts/easy_install.exe c:\\pywin32.exe"
+  - "%PYTHON%/Scripts/easy_install.exe wheel"
 
 
 build: off
 build: off
 
 
 test_script:
 test_script:
   - "%WITH_COMPILER% %PYTHON%/python setup.py test"
   - "%WITH_COMPILER% %PYTHON%/python setup.py test"
 
 
+after_test:
+  - "%WITH_COMPILER% %PYTHON%/python setup.py bdist_wheel"
 
 
+artifacts:
+  - path: dist\*

+ 40 - 0
bin/dulwich

@@ -239,6 +239,12 @@ def cmd_tag(args):
     porcelain.tag('.', args[0])
     porcelain.tag('.', args[0])
 
 
 
 
+def cmd_repack(args):
+    opts, args = getopt(args, "", [])
+    opts = dict(opts)
+    porcelain.repack('.')
+
+
 def cmd_reset(args):
 def cmd_reset(args):
     opts, args = getopt(args, "", ["hard", "soft", "mixed"])
     opts, args = getopt(args, "", ["hard", "soft", "mixed"])
     opts = dict(opts)
     opts = dict(opts)
@@ -342,6 +348,37 @@ def cmd_status(args):
         sys.stdout.write("\n")
         sys.stdout.write("\n")
 
 
 
 
+def cmd_ls_remote(args):
+    opts, args = getopt(args, '', [])
+    if len(args) < 1:
+        print('Usage: dulwich ls-remote URL')
+        sys.exit(1)
+    refs = porcelain.ls_remote(args[0])
+    for ref in sorted(refs):
+        sys.stdout.write("%s\t%s\n" % (ref, refs[ref]))
+
+
+def cmd_pack_objects(args):
+    opts, args = getopt(args, '', ['stdout'])
+    opts = dict(opts)
+    if len(args) < 1 and not '--stdout' in args:
+        print('Usage: dulwich pack-objects basename')
+        sys.exit(1)
+    object_ids = [l.strip() for l in sys.stdin.readlines()]
+    basename = args[0]
+    if '--stdout' in opts:
+        packf = getattr(sys.stdout, 'buffer', sys.stdout)
+        idxf = None
+        close = []
+    else:
+        packf = open(basename + '.pack', 'w')
+        idxf = open(basename + '.idx', 'w')
+        close = [packf, idxf]
+    porcelain.pack_objects('.', object_ids, packf, idxf)
+    for f in close:
+        f.close()
+
+
 commands = {
 commands = {
     "add": cmd_add,
     "add": cmd_add,
     "archive": cmd_archive,
     "archive": cmd_archive,
@@ -357,7 +394,10 @@ commands = {
     "fetch": cmd_fetch,
     "fetch": cmd_fetch,
     "init": cmd_init,
     "init": cmd_init,
     "log": cmd_log,
     "log": cmd_log,
+    "ls-remote": cmd_ls_remote,
+    "pack-objects": cmd_pack_objects,
     "receive-pack": cmd_receive_pack,
     "receive-pack": cmd_receive_pack,
+    "repack": cmd_repack,
     "reset": cmd_reset,
     "reset": cmd_reset,
     "rev-list": cmd_rev_list,
     "rev-list": cmd_rev_list,
     "rm": cmd_rm,
     "rm": cmd_rm,

+ 1 - 1
docs/index.txt

@@ -7,7 +7,7 @@ dulwich - Python implementation of Git
 Overview
 Overview
 ========
 ========
 
 
-.. include:: ../README
+.. include:: ../README.md
 
 
 Documentation
 Documentation
 =============
 =============

+ 0 - 2
docs/tutorial/.gitignore

@@ -1,2 +0,0 @@
-*.html
-myrepo

+ 1 - 1
dulwich.cfg

@@ -1,5 +1,5 @@
 packages: dulwich
 packages: dulwich
 docformat: restructuredtext
 docformat: restructuredtext
 projectname: Dulwich
 projectname: Dulwich
-projecturl: http://samba.org/~jelmer/dulwich/
+projecturl: https://www.dulwich.io/
 htmloutput: apidocs
 htmloutput: apidocs

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

@@ -0,0 +1,29 @@
+Metadata-Version: 1.1
+Name: dulwich
+Version: 0.11.1
+Summary: Python Git Library
+Home-page: https://www.dulwich.io/
+Author: Jelmer Vernooij
+Author-email: jelmer@jelmer.uk
+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: Programming Language :: Python :: 3.4
+Classifier: Programming Language :: Python :: Implementation :: CPython
+Classifier: Programming Language :: Python :: Implementation :: PyPy
+Classifier: Operating System :: POSIX
+Classifier: Topic :: Software Development :: Version Control

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

@@ -0,0 +1,187 @@
+AUTHORS
+CONTRIBUTING
+COPYING
+MANIFEST.in
+Makefile
+NEWS
+README.md
+appveyor.yml
+dulwich.cfg
+setup.cfg
+setup.py
+tox.ini
+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/pbr.json
+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/issue88_expect_ack_nak_client.export
+dulwich/tests/data/repos/issue88_expect_ack_nak_other.export
+dulwich/tests/data/repos/issue88_expect_ack_nak_server.export
+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

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

@@ -0,0 +1 @@
+

+ 1 - 0
dulwich.egg-info/pbr.json

@@ -0,0 +1 @@
+{"is_release": false, "git_version": "b732930"}

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

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

+ 1 - 1
dulwich/__init__.py

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

+ 2 - 2
dulwich/_objects.c

@@ -199,7 +199,7 @@ static PyObject *py_sorted_tree_items(PyObject *self, PyObject *args)
 		}
 		}
 
 
 		py_mode = PyTuple_GET_ITEM(value, 0);
 		py_mode = PyTuple_GET_ITEM(value, 0);
-		if (!PyInt_Check(py_mode)) {
+		if (!PyInt_Check(py_mode) && !PyLong_Check(py_mode)) {
 			PyErr_SetString(PyExc_TypeError, "Mode is not an integral type");
 			PyErr_SetString(PyExc_TypeError, "Mode is not an integral type");
 			goto error;
 			goto error;
 		}
 		}
@@ -210,7 +210,7 @@ static PyObject *py_sorted_tree_items(PyObject *self, PyObject *args)
 			goto error;
 			goto error;
 		}
 		}
 		qsort_entries[n].name = PyString_AS_STRING(key);
 		qsort_entries[n].name = PyString_AS_STRING(key);
-		qsort_entries[n].mode = PyInt_AS_LONG(py_mode);
+		qsort_entries[n].mode = PyInt_AsLong(py_mode);
 
 
 		qsort_entries[n].tuple = PyObject_CallFunctionObjArgs(
 		qsort_entries[n].tuple = PyObject_CallFunctionObjArgs(
 		                tree_entry_cls, key, py_mode, py_sha, NULL);
 		                tree_entry_cls, key, py_mode, py_sha, NULL);

+ 74 - 21
dulwich/client.py

@@ -42,6 +42,7 @@ from contextlib import closing
 from io import BytesIO, BufferedReader
 from io import BytesIO, BufferedReader
 import dulwich
 import dulwich
 import select
 import select
+import shlex
 import socket
 import socket
 import subprocess
 import subprocess
 import sys
 import sys
@@ -201,7 +202,7 @@ class GitClient(object):
                   progress=None, write_pack=write_pack_objects):
                   progress=None, write_pack=write_pack_objects):
         """Upload a pack to a remote repository.
         """Upload a pack to a remote repository.
 
 
-        :param path: Repository path
+        :param path: Repository path (as bytestring)
         :param generate_pack_contents: Function that can return a sequence of
         :param generate_pack_contents: Function that can return a sequence of
             the shas of the objects to upload.
             the shas of the objects to upload.
         :param progress: Optional progress function
         :param progress: Optional progress function
@@ -217,7 +218,7 @@ class GitClient(object):
     def fetch(self, path, target, determine_wants=None, progress=None):
     def fetch(self, path, target, determine_wants=None, progress=None):
         """Fetch into a target repository.
         """Fetch into a target repository.
 
 
-        :param path: Path to fetch from
+        :param path: Path to fetch from (as bytestring)
         :param target: Target repository to fetch into
         :param target: Target repository to fetch into
         :param determine_wants: Optional function to determine what refs
         :param determine_wants: Optional function to determine what refs
             to fetch
             to fetch
@@ -226,6 +227,7 @@ class GitClient(object):
         """
         """
         if determine_wants is None:
         if determine_wants is None:
             determine_wants = target.object_store.determine_wants_all
             determine_wants = target.object_store.determine_wants_all
+
         f, commit, abort = target.object_store.add_pack()
         f, commit, abort = target.object_store.add_pack()
         try:
         try:
             result = self.fetch_pack(
             result = self.fetch_pack(
@@ -249,6 +251,13 @@ class GitClient(object):
         """
         """
         raise NotImplementedError(self.fetch_pack)
         raise NotImplementedError(self.fetch_pack)
 
 
+    def get_refs(self, path):
+        """Retrieve the current refs from a git smart server.
+
+        :param path: Path to the repo to fetch from. (as bytestring)
+        """
+        raise NotImplementedError(self.get_refs)
+
     def _parse_status_report(self, proto):
     def _parse_status_report(self, proto):
         unpack = proto.read_pkt_line().strip()
         unpack = proto.read_pkt_line().strip()
         if unpack != b'unpack ok':
         if unpack != b'unpack ok':
@@ -445,7 +454,7 @@ class TraditionalGitClient(GitClient):
         reads would block.
         reads would block.
 
 
         :param cmd: The git service name to which we should connect.
         :param cmd: The git service name to which we should connect.
-        :param path: The path we should pass to the service.
+        :param path: The path we should pass to the service. (as bytestirng)
         """
         """
         raise NotImplementedError()
         raise NotImplementedError()
 
 
@@ -453,7 +462,7 @@ class TraditionalGitClient(GitClient):
                   progress=None, write_pack=write_pack_objects):
                   progress=None, write_pack=write_pack_objects):
         """Upload a pack to a remote repository.
         """Upload a pack to a remote repository.
 
 
-        :param path: Repository path
+        :param path: Repository path (as bytestring)
         :param generate_pack_contents: Function that can return a sequence of
         :param generate_pack_contents: Function that can return a sequence of
             the shas of the objects to upload.
             the shas of the objects to upload.
         :param progress: Optional callback called with progress updates
         :param progress: Optional callback called with progress updates
@@ -553,6 +562,14 @@ class TraditionalGitClient(GitClient):
                 proto, negotiated_capabilities, graph_walker, pack_data, progress)
                 proto, negotiated_capabilities, graph_walker, pack_data, progress)
             return refs
             return refs
 
 
+    def get_refs(self, path):
+        """Retrieve the current refs from a git smart server."""
+        # stock `git ls-remote` uses upload-pack
+        proto, _ = self._connect(b'upload-pack', path)
+        with proto:
+            refs, _ = read_pkt_refs(proto)
+            return refs
+
     def archive(self, path, committish, write_data, progress=None,
     def archive(self, path, committish, write_data, progress=None,
                 write_error=None):
                 write_error=None):
         proto, can_read = self._connect(b'upload-archive', path)
         proto, can_read = self._connect(b'upload-archive', path)
@@ -588,6 +605,10 @@ class TCPGitClient(TraditionalGitClient):
         TraditionalGitClient.__init__(self, *args, **kwargs)
         TraditionalGitClient.__init__(self, *args, **kwargs)
 
 
     def _connect(self, cmd, path):
     def _connect(self, cmd, path):
+        if type(cmd) is not bytes:
+            raise TypeError(path)
+        if type(path) is not bytes:
+            raise TypeError(path)
         sockaddrs = socket.getaddrinfo(
         sockaddrs = socket.getaddrinfo(
             self._host, self._port, socket.AF_UNSPEC, socket.SOCK_STREAM)
             self._host, self._port, socket.AF_UNSPEC, socket.SOCK_STREAM)
         s = None
         s = None
@@ -617,7 +638,8 @@ class TCPGitClient(TraditionalGitClient):
                          report_activity=self._report_activity)
                          report_activity=self._report_activity)
         if path.startswith(b"/~"):
         if path.startswith(b"/~"):
             path = path[1:]
             path = path[1:]
-        proto.send_cmd(b'git-' + cmd, path, b'host=' + self._host)
+        # TODO(jelmer): Alternative to ascii?
+        proto.send_cmd(b'git-' + cmd, path, b'host=' + self._host.encode('ascii'))
         return proto, lambda: _fileno_can_read(s)
         return proto, lambda: _fileno_can_read(s)
 
 
 
 
@@ -679,11 +701,14 @@ class SubprocessGitClient(TraditionalGitClient):
     git_command = None
     git_command = None
 
 
     def _connect(self, service, path):
     def _connect(self, service, path):
+        if type(service) is not bytes:
+            raise TypeError(path)
+        if type(path) is not bytes:
+            raise TypeError(path)
         import subprocess
         import subprocess
         if self.git_command is None:
         if self.git_command is None:
             git_command = find_git_command()
             git_command = find_git_command()
-        argv = git_command + [service, path]
-        argv = ['git', service.decode('ascii'), path]
+        argv = git_command + [service.decode('ascii'), path]
         p = SubprocessWrapper(
         p = SubprocessWrapper(
             subprocess.Popen(argv, bufsize=0, stdin=subprocess.PIPE,
             subprocess.Popen(argv, bufsize=0, stdin=subprocess.PIPE,
                              stdout=subprocess.PIPE,
                              stdout=subprocess.PIPE,
@@ -698,7 +723,6 @@ class LocalGitClient(GitClient):
     def __init__(self, thin_packs=True, report_activity=None):
     def __init__(self, thin_packs=True, report_activity=None):
         """Create a new LocalGitClient instance.
         """Create a new LocalGitClient instance.
 
 
-        :param path: Path to the local repository
         :param thin_packs: Whether or not thin packs should be retrieved
         :param thin_packs: Whether or not thin packs should be retrieved
         :param report_activity: Optional callback for reporting transport
         :param report_activity: Optional callback for reporting transport
             activity.
             activity.
@@ -710,7 +734,7 @@ class LocalGitClient(GitClient):
                   progress=None, write_pack=write_pack_objects):
                   progress=None, write_pack=write_pack_objects):
         """Upload a pack to a remote repository.
         """Upload a pack to a remote repository.
 
 
-        :param path: Repository path
+        :param path: Repository path (as bytestring)
         :param generate_pack_contents: Function that can return a sequence of
         :param generate_pack_contents: Function that can return a sequence of
             the shas of the objects to upload.
             the shas of the objects to upload.
         :param progress: Optional progress function
         :param progress: Optional progress function
@@ -749,7 +773,7 @@ class LocalGitClient(GitClient):
     def fetch(self, path, target, determine_wants=None, progress=None):
     def fetch(self, path, target, determine_wants=None, progress=None):
         """Fetch into a target repository.
         """Fetch into a target repository.
 
 
-        :param path: Path to fetch from
+        :param path: Path to fetch from (as bytestring)
         :param target: Target repository to fetch into
         :param target: Target repository to fetch into
         :param determine_wants: Optional function to determine what refs
         :param determine_wants: Optional function to determine what refs
             to fetch
             to fetch
@@ -780,6 +804,13 @@ class LocalGitClient(GitClient):
                 return
                 return
             write_pack_objects(ProtocolFile(None, pack_data), objects_iter)
             write_pack_objects(ProtocolFile(None, pack_data), objects_iter)
 
 
+    def get_refs(self, path):
+        """Retrieve the current refs from a git smart server."""
+        from dulwich.repo import Repo
+
+        with closing(Repo(path)) as target:
+            return target.get_refs()
+
 
 
 # What Git client to use for local access
 # What Git client to use for local access
 default_local_git_client_cls = LocalGitClient
 default_local_git_client_cls = LocalGitClient
@@ -802,7 +833,7 @@ class SSHVendor(object):
         with the remote command.
         with the remote command.
 
 
         :param host: Host name
         :param host: Host name
-        :param command: Command to run
+        :param command: Command to run (as argv array)
         :param username: Optional ame of user to log in as
         :param username: Optional ame of user to log in as
         :param port: Optional SSH port to use
         :param port: Optional SSH port to use
         """
         """
@@ -813,6 +844,10 @@ class SubprocessSSHVendor(SSHVendor):
     """SSH vendor that shells out to the local 'ssh' command."""
     """SSH vendor that shells out to the local 'ssh' command."""
 
 
     def run_command(self, host, command, username=None, port=None):
     def run_command(self, host, command, username=None, port=None):
+        if (type(command) is not list or
+            not all([isinstance(b, bytes) for b in command])):
+            raise TypeError(command)
+
         import subprocess
         import subprocess
         #FIXME: This has no way to deal with passwords..
         #FIXME: This has no way to deal with passwords..
         args = ['ssh', '-x']
         args = ['ssh', '-x']
@@ -915,7 +950,9 @@ else:
 
 
         def run_command(self, host, command, username=None, port=None,
         def run_command(self, host, command, username=None, port=None,
                         progress_stderr=None):
                         progress_stderr=None):
-
+            if (type(command) is not list or
+                not all([isinstance(b, bytes) for b in command])):
+                raise TypeError(command)
             # Paramiko needs an explicit port. None is not valid
             # Paramiko needs an explicit port. None is not valid
             if port is None:
             if port is None:
                 port = 22
                 port = 22
@@ -931,7 +968,7 @@ else:
             channel = client.get_transport().open_session()
             channel = client.get_transport().open_session()
 
 
             # Run commands
             # Run commands
-            channel.exec_command(*command)
+            channel.exec_command(subprocess.list2cmdline(command))
 
 
             return ParamikoWrapper(
             return ParamikoWrapper(
                 client, channel, progress_stderr=progress_stderr)
                 client, channel, progress_stderr=progress_stderr)
@@ -951,15 +988,24 @@ class SSHGitClient(TraditionalGitClient):
         self.alternative_paths = {}
         self.alternative_paths = {}
 
 
     def _get_cmd_path(self, cmd):
     def _get_cmd_path(self, cmd):
-        cmd = cmd.decode('ascii')
-        return self.alternative_paths.get(cmd, 'git-' + cmd)
+        cmd = self.alternative_paths.get(cmd, b'git-' + cmd)
+        assert isinstance(cmd, bytes)
+        if sys.version_info[:2] <= (2, 6):
+            return shlex.split(cmd)
+        else:
+            # TODO(jelmer): Don't decode/encode here
+            return [x.encode('ascii') for x in shlex.split(cmd.decode('ascii'))]
 
 
     def _connect(self, cmd, path):
     def _connect(self, cmd, path):
-        if path.startswith("/~"):
+        if type(cmd) is not bytes:
+            raise TypeError(path)
+        if type(path) is not bytes:
+            raise TypeError(path)
+        if path.startswith(b"/~"):
             path = path[1:]
             path = path[1:]
+        argv = self._get_cmd_path(cmd) + [path]
         con = get_ssh_vendor().run_command(
         con = get_ssh_vendor().run_command(
-            self.host, [self._get_cmd_path(cmd), path],
-            port=self.port, username=self.username)
+            self.host, argv, port=self.port, username=self.username)
         return (Protocol(con.read, con.write, con.close,
         return (Protocol(con.read, con.write, con.close,
                          report_activity=self._report_activity),
                          report_activity=self._report_activity),
                 con.can_read)
                 con.can_read)
@@ -1054,7 +1100,7 @@ class HttpGitClient(GitClient):
                   progress=None, write_pack=write_pack_objects):
                   progress=None, write_pack=write_pack_objects):
         """Upload a pack to a remote repository.
         """Upload a pack to a remote repository.
 
 
-        :param path: Repository path
+        :param path: Repository path (as bytestring)
         :param generate_pack_contents: Function that can return a sequence of
         :param generate_pack_contents: Function that can return a sequence of
             the shas of the objects to upload.
             the shas of the objects to upload.
         :param progress: Optional progress function
         :param progress: Optional progress function
@@ -1134,11 +1180,18 @@ class HttpGitClient(GitClient):
         finally:
         finally:
             resp.close()
             resp.close()
 
 
+    def get_refs(self, path):
+        """Retrieve the current refs from a git smart server."""
+        url = self._get_url(path)
+        refs, _ = self._discover_references(
+            b"git-upload-pack", url)
+        return refs
+
 
 
 def get_transport_and_path_from_url(url, config=None, **kwargs):
 def get_transport_and_path_from_url(url, config=None, **kwargs):
     """Obtain a git client from a URL.
     """Obtain a git client from a URL.
 
 
-    :param url: URL to open
+    :param url: URL to open (a unicode string)
     :param config: Optional config object
     :param config: Optional config object
     :param thin_packs: Whether or not thin packs should be retrieved
     :param thin_packs: Whether or not thin packs should be retrieved
     :param report_activity: Optional callback for reporting transport
     :param report_activity: Optional callback for reporting transport
@@ -1167,7 +1220,7 @@ def get_transport_and_path_from_url(url, config=None, **kwargs):
 def get_transport_and_path(location, **kwargs):
 def get_transport_and_path(location, **kwargs):
     """Obtain a git client from a URL.
     """Obtain a git client from a URL.
 
 
-    :param location: URL or path
+    :param location: URL or path (a string)
     :param config: Optional config object
     :param config: Optional config object
     :param thin_packs: Whether or not thin packs should be retrieved
     :param thin_packs: Whether or not thin packs should be retrieved
     :param report_activity: Optional callback for reporting transport
     :param report_activity: Optional callback for reporting transport

+ 42 - 26
dulwich/config.py

@@ -171,50 +171,65 @@ def _format_string(value):
     return _escape_value(value)
     return _escape_value(value)
 
 
 
 
+_ESCAPE_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"),
+    }
+_COMMENT_CHARS = [ord(b"#"), ord(b";")]
+_WHITESPACE_CHARS = [ord(b"\t"), ord(b" ")]
+
 def _parse_string(value):
 def _parse_string(value):
     value = bytearray(value.strip())
     value = bytearray(value.strip())
     ret = bytearray()
     ret = bytearray()
-    block = bytearray()
+    whitespace = bytearray()
     in_quotes = False
     in_quotes = False
-    for c in value:
-        if c == ord(b"\""):
+    i = 0
+    while i < len(value):
+        c = value[i]
+        if c == ord(b"\\"):
+            i += 1
+            try:
+                v = _ESCAPE_TABLE[value[i]]
+            except IndexError:
+                raise ValueError(
+                    "escape character in %r at %d before end of string" %
+                    (value, i))
+            except KeyError:
+                raise ValueError(
+                    "escape character followed by unknown character %s at %d in %r" %
+                    (value[i], i, value))
+            if whitespace:
+                ret.extend(whitespace)
+                whitespace = bytearray()
+            ret.append(v)
+        elif c == ord(b"\""):
             in_quotes = (not in_quotes)
             in_quotes = (not in_quotes)
-            ret.extend(_unescape_value(block))
-            block = bytearray()
-        elif c in (ord(b"#"), ord(b";")) and not in_quotes:
+        elif c in _COMMENT_CHARS and not in_quotes:
             # the rest of the line is a comment
             # the rest of the line is a comment
             break
             break
+        elif c in _WHITESPACE_CHARS:
+            whitespace.append(c)
         else:
         else:
-            block.append(c)
+            if whitespace:
+                ret.extend(whitespace)
+                whitespace = bytearray()
+            ret.append(c)
+        i += 1
 
 
     if in_quotes:
     if in_quotes:
-        raise ValueError("value starts with quote but lacks end quote")
-
-    ret.extend(_unescape_value(block).rstrip())
+        raise ValueError("missing end quote")
 
 
     return bytes(ret)
     return bytes(ret)
 
 
 
 
 def _unescape_value(value):
 def _unescape_value(value):
     """Unescape a value."""
     """Unescape a 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()
     ret = bytearray()
     i = 0
     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
     return ret
 
 
 
 
@@ -258,6 +273,7 @@ class ConfigFile(ConfigDict):
         for lineno, line in enumerate(f.readlines()):
         for lineno, line in enumerate(f.readlines()):
             line = line.lstrip()
             line = line.lstrip()
             if setting is None:
             if setting is None:
+                # Parse section header ("[bla]")
                 if len(line) > 0 and line[:1] == b"[":
                 if len(line) > 0 and line[:1] == b"[":
                     line = _strip_comments(line).rstrip()
                     line = _strip_comments(line).rstrip()
                     last = line.index(b"]")
                     last = line.index(b"]")

+ 7 - 5
dulwich/file.py

@@ -19,9 +19,10 @@
 """Safe access to git files."""
 """Safe access to git files."""
 
 
 import errno
 import errno
+import io
 import os
 import os
+import sys
 import tempfile
 import tempfile
-import io
 
 
 def ensure_dir_exists(dirname):
 def ensure_dir_exists(dirname):
     """Ensure a directory exists, creating if necessary."""
     """Ensure a directory exists, creating if necessary."""
@@ -32,7 +33,7 @@ def ensure_dir_exists(dirname):
             raise
             raise
 
 
 
 
-def fancy_rename(oldname, newname):
+def _fancy_rename(oldname, newname):
     """Rename file with temporary backup file to rollback if rename fails"""
     """Rename file with temporary backup file to rollback if rename fails"""
     if not os.path.exists(newname):
     if not os.path.exists(newname):
         try:
         try:
@@ -148,10 +149,11 @@ class _GitFile(object):
             try:
             try:
                 os.rename(self._lockfilename, self._filename)
                 os.rename(self._lockfilename, self._filename)
             except OSError as e:
             except OSError as e:
-                # Windows versions prior to Vista don't support atomic renames
-                if e.errno != errno.EEXIST:
+                if sys.platform == 'win32' and e.errno == errno.EEXIST:
+                    # Windows versions prior to Vista don't support atomic renames
+                    _fancy_rename(self._lockfilename, self._filename)
+                else:
                     raise
                     raise
-                fancy_rename(self._lockfilename, self._filename)
         finally:
         finally:
             self.abort()
             self.abort()
 
 

+ 15 - 1
dulwich/objects.py

@@ -25,7 +25,6 @@ from collections import namedtuple
 import os
 import os
 import posixpath
 import posixpath
 import stat
 import stat
-import sys
 import warnings
 import warnings
 import zlib
 import zlib
 from hashlib import sha1
 from hashlib import sha1
@@ -520,6 +519,21 @@ class ShaFile(object):
         """
         """
         return isinstance(other, ShaFile) and self.id == other.id
         return isinstance(other, ShaFile) and self.id == other.id
 
 
+    def __lt__(self, other):
+        if not isinstance(other, ShaFile):
+            raise TypeError
+        return self.id < other.id
+
+    def __le__(self, other):
+        if not isinstance(other, ShaFile):
+            raise TypeError
+        return self.id <= other.id
+
+    def __cmp__(self, other):
+        if not isinstance(other, ShaFile):
+            raise TypeError
+        return cmp(self.id, other.id)
+
 
 
 class Blob(ShaFile):
 class Blob(ShaFile):
     """A Git Blob object."""
     """A Git Blob object."""

+ 94 - 4
dulwich/objectspec.py

@@ -19,6 +19,12 @@
 """Object specification."""
 """Object specification."""
 
 
 
 
+def to_bytes(text):
+    if getattr(text, "encode", None) is not None:
+        text = text.encode('ascii')
+    return text
+
+
 def parse_object(repo, objectish):
 def parse_object(repo, objectish):
     """Parse a string referring to an object.
     """Parse a string referring to an object.
 
 
@@ -27,11 +33,96 @@ def parse_object(repo, objectish):
     :return: A git object
     :return: A git object
     :raise KeyError: If the object can not be found
     :raise KeyError: If the object can not be found
     """
     """
-    if getattr(objectish, "encode", None) is not None:
-        objectish = objectish.encode('ascii')
+    objectish = to_bytes(objectish)
     return repo[objectish]
     return repo[objectish]
 
 
 
 
+def parse_ref(container, refspec):
+    """Parse a string referring to a reference.
+
+    :param container: A RefsContainer object
+    :param refspec: A string referring to a ref
+    :return: A ref
+    :raise KeyError: If the ref can not be found
+    """
+    refspec = to_bytes(refspec)
+    for ref in [refspec, b"refs/heads/" + refspec]:
+        if ref in container:
+            return ref
+    else:
+        raise KeyError(refspec)
+
+
+def parse_reftuple(lh_container, rh_container, refspec):
+    """Parse a reftuple spec.
+
+    :param lh_container: A RefsContainer object
+    :param hh_container: A RefsContainer object
+    :param refspec: A string
+    :return: A tuple with left and right ref
+    :raise KeyError: If one of the refs can not be found
+    """
+    if refspec.startswith(b"+"):
+        force = True
+        refspec = refspec[1:]
+    else:
+        force = False
+    refspec = to_bytes(refspec)
+    if b":" in refspec:
+        (lh, rh) = refspec.split(b":")
+    else:
+        lh = rh = refspec
+    if rh == b"":
+        lh = None
+    else:
+        lh = parse_ref(lh_container, lh)
+    if rh == b"":
+        rh = None
+    else:
+        try:
+            rh = parse_ref(rh_container, rh)
+        except KeyError:
+            # TODO: check force?
+            if not b"/" in rh:
+                rh = b"refs/heads/" + rh
+    return (lh, rh, force)
+
+
+def parse_reftuples(lh_container, rh_container, refspecs):
+    """Parse a list of reftuple specs to a list of reftuples.
+
+    :param lh_container: A RefsContainer object
+    :param hh_container: A RefsContainer object
+    :param refspecs: A list of refspecs or a string
+    :return: A list of refs
+    :raise KeyError: If one of the refs can not be found
+    """
+    if not isinstance(refspecs, list):
+        refspecs = [refspecs]
+    ret = []
+    # TODO: Support * in refspecs
+    for refspec in refspecs:
+        ret.append(parse_reftuple(lh_container, rh_container, refspec))
+    return ret
+
+
+def parse_refs(container, refspecs):
+    """Parse a list of refspecs to a list of refs.
+
+    :param container: A RefsContainer object
+    :param refspecs: A list of refspecs or a string
+    :return: A list of refs
+    :raise KeyError: If one of the refs can not be found
+    """
+    # TODO: Support * in refspecs
+    if not isinstance(refspecs, list):
+        refspecs = [refspecs]
+    ret = []
+    for refspec in refspecs:
+        ret.append(parse_ref(container, refspec))
+    return ret
+
+
 def parse_commit_range(repo, committishs):
 def parse_commit_range(repo, committishs):
     """Parse a string referring to a range of commits.
     """Parse a string referring to a range of commits.
 
 
@@ -41,6 +132,5 @@ def parse_commit_range(repo, committishs):
     :raise KeyError: When the reference commits can not be found
     :raise KeyError: When the reference commits can not be found
     :raise ValueError: If the range can not be parsed
     :raise ValueError: If the range can not be parsed
     """
     """
-    if getattr(committishs, "encode", None) is not None:
-        committishs = committishs.encode('ascii')
+    committishs = to_bytes(committishs)
     return iter([repo[committishs]])
     return iter([repo[committishs]])

+ 54 - 35
dulwich/pack.py

@@ -1061,33 +1061,48 @@ class PackData(object):
 
 
         :return: Tuple with object type and contents.
         :return: Tuple with object type and contents.
         """
         """
-        if type not in DELTA_TYPES:
-            return type, obj
-
-        if get_ref is None:
-            get_ref = self.get_ref
-        if type == OFS_DELTA:
-            (delta_offset, delta) = obj
-            # TODO: clean up asserts and replace with nicer error messages
-            assert isinstance(offset, int) or isinstance(offset, long)
-            assert isinstance(delta_offset, int) or isinstance(offset, long)
-            base_offset = offset-delta_offset
-            type, base_obj = self.get_object_at(base_offset)
-            assert isinstance(type, int)
-        elif type == REF_DELTA:
-            (basename, delta) = obj
-            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)
-        chunks = apply_delta(base_chunks, delta)
-        # TODO(dborowitz): This can result in poor performance if large base
-        # objects are separated from deltas in the pack. We should reorganize
-        # so that we apply deltas to all objects in a chain one after the other
-        # to optimize cache performance.
-        if offset is not None:
-            self._offset_cache[offset] = type, chunks
-        return type, chunks
+        # Walk down the delta chain, building a stack of deltas to reach
+        # the requested object.
+        base_offset = offset
+        base_type = type
+        base_obj = obj
+        delta_stack = []
+        while base_type in DELTA_TYPES:
+            prev_offset = base_offset
+            if get_ref is None:
+                get_ref = self.get_ref
+            if base_type == OFS_DELTA:
+                (delta_offset, delta) = base_obj
+                # TODO: clean up asserts and replace with nicer error messages
+                assert (
+                    isinstance(base_offset, int)
+                    or isinstance(base_offset, long))
+                assert (
+                    isinstance(delta_offset, int)
+                    or isinstance(base_offset, long))
+                base_offset = base_offset - delta_offset
+                base_type, base_obj = self.get_object_at(base_offset)
+                assert isinstance(base_type, int)
+            elif base_type == REF_DELTA:
+                (basename, delta) = base_obj
+                assert isinstance(basename, bytes) and len(basename) == 20
+                base_offset, base_type, base_obj = get_ref(basename)
+                assert isinstance(base_type, int)
+            delta_stack.append((prev_offset, base_type, delta))
+
+        # Now grab the base object (mustn't be a delta) and apply the
+        # deltas all the way up the stack.
+        chunks = base_obj
+        for prev_offset, delta_type, delta in reversed(delta_stack):
+            chunks = apply_delta(chunks, delta)
+            # TODO(dborowitz): This can result in poor performance if
+            # large base objects are separated from deltas in the pack.
+            # We should reorganize so that we apply deltas to all
+            # objects in a chain one after the other to optimize cache
+            # performance.
+            if prev_offset is not None:
+                self._offset_cache[prev_offset] = base_type, chunks
+        return base_type, chunks
 
 
     def iterobjects(self, progress=None, compute_crc32=True):
     def iterobjects(self, progress=None, compute_crc32=True):
         self._file.seek(self._header_size)
         self._file.seek(self._header_size)
@@ -1322,15 +1337,16 @@ class DeltaChainIterator(object):
     def _follow_chain(self, offset, obj_type_num, base_chunks):
     def _follow_chain(self, offset, obj_type_num, base_chunks):
         # Unlike PackData.get_object_at, there is no need to cache offsets as
         # Unlike PackData.get_object_at, there is no need to cache offsets as
         # this approach by design inflates each object exactly once.
         # this approach by design inflates each object exactly once.
-        unpacked = self._resolve_object(offset, obj_type_num, base_chunks)
-        yield self._result(unpacked)
+        todo = [(offset, obj_type_num, base_chunks)]
+        for offset, obj_type_num, base_chunks in todo:
+            unpacked = self._resolve_object(offset, obj_type_num, base_chunks)
+            yield self._result(unpacked)
 
 
-        pending = chain(self._pending_ofs.pop(unpacked.offset, []),
-                        self._pending_ref.pop(unpacked.sha(), []))
-        for new_offset in pending:
-            for new_result in self._follow_chain(
-              new_offset, unpacked.obj_type_num, unpacked.obj_chunks):
-                yield new_result
+            unblocked = chain(self._pending_ofs.pop(unpacked.offset, []),
+                              self._pending_ref.pop(unpacked.sha(), []))
+            todo.extend(
+                (new_offset, unpacked.obj_type_num, unpacked.obj_chunks)
+                for new_offset in unblocked)
 
 
     def __iter__(self):
     def __iter__(self):
         return self._walk_all_chains()
         return self._walk_all_chains()
@@ -1778,6 +1794,9 @@ def write_pack_index_v2(f, entries, pack_checksum):
     return f.write_sha()
     return f.write_sha()
 
 
 
 
+write_pack_index = write_pack_index_v2
+
+
 class Pack(object):
 class Pack(object):
     """A Git pack object."""
     """A Git pack object."""
 
 

+ 128 - 44
dulwich/porcelain.py

@@ -29,6 +29,7 @@ Currently implemented:
  * diff-tree
  * diff-tree
  * fetch
  * fetch
  * init
  * init
+ * ls-remote
  * pull
  * pull
  * push
  * push
  * rm
  * rm
@@ -66,10 +67,18 @@ from dulwich.errors import (
     )
     )
 from dulwich.index import get_unstaged_changes
 from dulwich.index import get_unstaged_changes
 from dulwich.objects import (
 from dulwich.objects import (
+    Commit,
     Tag,
     Tag,
     parse_timezone,
     parse_timezone,
     )
     )
-from dulwich.objectspec import parse_object
+from dulwich.objectspec import (
+    parse_object,
+    parse_reftuples,
+    )
+from dulwich.pack import (
+    write_pack_index,
+    write_pack_objects,
+    )
 from dulwich.patch import write_tree_diff
 from dulwich.patch import write_tree_diff
 from dulwich.protocol import Protocol
 from dulwich.protocol import Protocol
 from dulwich.repo import (BaseRepo, Repo)
 from dulwich.repo import (BaseRepo, Repo)
@@ -86,6 +95,14 @@ from dulwich.server import (
 GitStatus = namedtuple('GitStatus', 'staged unstaged untracked')
 GitStatus = namedtuple('GitStatus', 'staged unstaged untracked')
 
 
 
 
+def encode_path(path):
+    """Encode a path as bytestring."""
+    # TODO(jelmer): Use something other than ascii?
+    if not isinstance(path, bytes):
+        path = path.encode('ascii')
+    return path
+
+
 def open_repo(path_or_repo):
 def open_repo(path_or_repo):
     """Open an argument that can be a repository or a path for a repository."""
     """Open an argument that can be a repository or a path for a repository."""
     if isinstance(path_or_repo, BaseRepo):
     if isinstance(path_or_repo, BaseRepo):
@@ -122,6 +139,8 @@ def archive(path, committish=None, outstream=sys.stdout,
     client = SubprocessGitClient()
     client = SubprocessGitClient()
     if committish is None:
     if committish is None:
         committish = "HEAD"
         committish = "HEAD"
+    if not isinstance(path, bytes):
+        path = path.encode(sys.getfilesystemencoding())
     # TODO(jelmer): This invokes C git; this introduces a dependency.
     # TODO(jelmer): This invokes C git; this introduces a dependency.
     # Instead, dulwich should have its own archiver implementation.
     # Instead, dulwich should have its own archiver implementation.
     client.archive(path, committish, outstream.write, errstream.write,
     client.archive(path, committish, outstream.write, errstream.write,
@@ -234,7 +253,7 @@ def clone(source, target=None, bare=False, checkout=None, errstream=sys.stdout,
             progress=errstream.write)
             progress=errstream.write)
         r[b"HEAD"] = remote_refs[b"HEAD"]
         r[b"HEAD"] = remote_refs[b"HEAD"]
         if checkout:
         if checkout:
-            errstream.write(b'Checking out HEAD')
+            errstream.write(b'Checking out HEAD\n')
             r.reset_index()
             r.reset_index()
     except:
     except:
         r.close()
         r.close()
@@ -276,93 +295,99 @@ def rm(repo=".", paths=None):
         index.write()
         index.write()
 
 
 
 
-def commit_decode(commit, contents):
+def commit_decode(commit, contents, default_encoding='utf-8'):
     if commit.encoding is not None:
     if commit.encoding is not None:
         return contents.decode(commit.encoding, "replace")
         return contents.decode(commit.encoding, "replace")
-    return contents.decode("utf-8", "replace")
+    return contents.decode(default_encoding, "replace")
 
 
 
 
-def print_commit(commit, outstream=sys.stdout):
+def print_commit(commit, decode, outstream=sys.stdout):
     """Write a human-readable commit log entry.
     """Write a human-readable commit log entry.
 
 
     :param commit: A `Commit` object
     :param commit: A `Commit` object
     :param outstream: A stream file to write to
     :param outstream: A stream file to write to
     """
     """
-    outstream.write(b"-" * 50 + b"\n")
-    outstream.write(b"commit: " + commit.id + b"\n")
+    outstream.write("-" * 50 + "\n")
+    outstream.write("commit: " + commit.id.decode('ascii') + "\n")
     if len(commit.parents) > 1:
     if len(commit.parents) > 1:
-        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")
+        outstream.write("merge: " +
+            "...".join([c.decode('ascii') for c in commit.parents[1:]]) + "\n")
+    outstream.write("author: " + decode(commit.author) + "\n")
+    outstream.write("committer: " + decode(commit.committer) + "\n")
+    outstream.write("\n")
+    outstream.write(decode(commit.message) + "\n")
+    outstream.write("\n")
 
 
 
 
-def print_tag(tag, outstream=sys.stdout):
+def print_tag(tag, decode, outstream=sys.stdout):
     """Write a human-readable tag.
     """Write a human-readable tag.
 
 
     :param tag: A `Tag` object
     :param tag: A `Tag` object
+    :param decode: Function for decoding bytes to unicode string
     :param outstream: A stream to write to
     :param outstream: A stream to write to
     """
     """
-    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")
+    outstream.write("Tagger: " + decode(tag.tagger) + "\n")
+    outstream.write("Date:   " + decode(tag.tag_time) + "\n")
+    outstream.write("\n")
+    outstream.write(decode(tag.message) + "\n")
+    outstream.write("\n")
 
 
 
 
-def show_blob(repo, blob, outstream=sys.stdout):
+def show_blob(repo, blob, decode, outstream=sys.stdout):
     """Write a blob to a stream.
     """Write a blob to a stream.
 
 
     :param repo: A `Repo` object
     :param repo: A `Repo` object
     :param blob: A `Blob` object
     :param blob: A `Blob` object
+    :param decode: Function for decoding bytes to unicode string
     :param outstream: A stream file to write to
     :param outstream: A stream file to write to
     """
     """
-    outstream.write(blob.data)
+    outstream.write(decode(blob.data))
 
 
 
 
-def show_commit(repo, commit, outstream=sys.stdout):
+def show_commit(repo, commit, decode, outstream=sys.stdout):
     """Show a commit to a stream.
     """Show a commit to a stream.
 
 
     :param repo: A `Repo` object
     :param repo: A `Repo` object
     :param commit: A `Commit` object
     :param commit: A `Commit` object
+    :param decode: Function for decoding bytes to unicode string
     :param outstream: Stream to write to
     :param outstream: Stream to write to
     """
     """
-    print_commit(commit, outstream)
+    print_commit(commit, decode=decode, outstream=outstream)
     parent_commit = repo[commit.parents[0]]
     parent_commit = repo[commit.parents[0]]
     write_tree_diff(outstream, repo.object_store, parent_commit.tree, commit.tree)
     write_tree_diff(outstream, repo.object_store, parent_commit.tree, commit.tree)
 
 
 
 
-def show_tree(repo, tree, outstream=sys.stdout):
+def show_tree(repo, tree, decode, outstream=sys.stdout):
     """Print a tree to a stream.
     """Print a tree to a stream.
 
 
     :param repo: A `Repo` object
     :param repo: A `Repo` object
     :param tree: A `Tree` object
     :param tree: A `Tree` object
+    :param decode: Function for decoding bytes to unicode string
     :param outstream: Stream to write to
     :param outstream: Stream to write to
     """
     """
     for n in tree:
     for n in tree:
-        outstream.write("%s\n" % n)
+        outstream.write(decode(n) + "\n")
 
 
 
 
-def show_tag(repo, tag, outstream=sys.stdout):
+def show_tag(repo, tag, decode, outstream=sys.stdout):
     """Print a tag to a stream.
     """Print a tag to a stream.
 
 
     :param repo: A `Repo` object
     :param repo: A `Repo` object
     :param tag: A `Tag` object
     :param tag: A `Tag` object
+    :param decode: Function for decoding bytes to unicode string
     :param outstream: Stream to write to
     :param outstream: Stream to write to
     """
     """
-    print_tag(tag, outstream)
+    print_tag(tag, decode, outstream)
     show_object(repo, repo[tag.object[1]], outstream)
     show_object(repo, repo[tag.object[1]], outstream)
 
 
 
 
-def show_object(repo, obj, outstream):
+def show_object(repo, obj, decode, outstream):
     return {
     return {
         b"tree": show_tree,
         b"tree": show_tree,
         b"blob": show_blob,
         b"blob": show_blob,
         b"commit": show_commit,
         b"commit": show_commit,
         b"tag": show_tag,
         b"tag": show_tag,
-            }[obj.type_name](repo, obj, outstream)
+            }[obj.type_name](repo, obj, decode, outstream)
 
 
 
 
 def log(repo=".", outstream=sys.stdout, max_entries=None):
 def log(repo=".", outstream=sys.stdout, max_entries=None):
@@ -375,15 +400,18 @@ def log(repo=".", outstream=sys.stdout, max_entries=None):
     with open_repo_closing(repo) as r:
     with open_repo_closing(repo) as r:
         walker = r.get_walker(max_entries=max_entries)
         walker = r.get_walker(max_entries=max_entries)
         for entry in walker:
         for entry in walker:
-            print_commit(entry.commit, outstream)
+            decode = lambda x: commit_decode(entry.commit, x)
+            print_commit(entry.commit, decode, outstream)
 
 
 
 
-def show(repo=".", objects=None, outstream=sys.stdout):
+# TODO(jelmer): better default for encoding?
+def show(repo=".", objects=None, outstream=sys.stdout, default_encoding='utf-8'):
     """Print the changes in a commit.
     """Print the changes in a commit.
 
 
     :param repo: Path to repository
     :param repo: Path to repository
     :param objects: Objects to show (defaults to [HEAD])
     :param objects: Objects to show (defaults to [HEAD])
     :param outstream: Stream to write to
     :param outstream: Stream to write to
+    :param default_encoding: Default encoding to use if none is set in the commit
     """
     """
     if objects is None:
     if objects is None:
         objects = ["HEAD"]
         objects = ["HEAD"]
@@ -391,7 +419,12 @@ def show(repo=".", objects=None, outstream=sys.stdout):
         objects = [objects]
         objects = [objects]
     with open_repo_closing(repo) as r:
     with open_repo_closing(repo) as r:
         for objectish in objects:
         for objectish in objects:
-            show_object(r, parse_object(r, objectish), outstream)
+            o = parse_object(r, objectish)
+            if isinstance(o, Commit):
+                decode = lambda x: commit_decode(o, x, default_encoding)
+            else:
+                decode = lambda x: x.decode(default_encoding)
+            show_object(r, o, decode, outstream)
 
 
 
 
 def diff_tree(repo, old_tree, new_tree, outstream=sys.stdout):
 def diff_tree(repo, old_tree, new_tree, outstream=sys.stdout):
@@ -518,13 +551,13 @@ def reset(repo, mode, committish="HEAD"):
         r.reset_index()
         r.reset_index()
 
 
 
 
-def push(repo, remote_location, refs_path,
+def push(repo, remote_location, refspecs=None,
          outstream=sys.stdout, errstream=sys.stderr):
          outstream=sys.stdout, errstream=sys.stderr):
     """Remote push with dulwich via dulwich.client
     """Remote push with dulwich via dulwich.client
 
 
     :param repo: Path to repository
     :param repo: Path to repository
     :param remote_location: Location of the remote
     :param remote_location: Location of the remote
-    :param refs_path: relative path to the refs to push to remote
+    :param refspecs: relative path to the refs to push to remote
     :param outstream: A stream file to write output
     :param outstream: A stream file to write output
     :param errstream: A stream file to write errors
     :param errstream: A stream file to write errors
     """
     """
@@ -535,10 +568,16 @@ def push(repo, remote_location, refs_path,
         # Get the client and path
         # Get the client and path
         client, path = get_transport_and_path(remote_location)
         client, path = get_transport_and_path(remote_location)
 
 
+        selected_refs = []
+
         def update_refs(refs):
         def update_refs(refs):
-            new_refs = r.get_refs()
-            refs[refs_path] = new_refs[b'HEAD']
-            del new_refs[b'HEAD']
+            selected_refs.extend(parse_reftuples(r.refs, refs, refspecs))
+            # TODO: Handle selected_refs == {None: None}
+            for (lh, rh, force) in selected_refs:
+                if lh is None:
+                    del refs[rh]
+                else:
+                    refs[rh] = r.refs[lh]
             return refs
             return refs
 
 
         err_encoding = getattr(errstream, 'encoding', 'utf-8')
         err_encoding = getattr(errstream, 'encoding', 'utf-8')
@@ -546,27 +585,37 @@ def push(repo, remote_location, refs_path,
         try:
         try:
             client.send_pack(path, update_refs,
             client.send_pack(path, update_refs,
                 r.object_store.generate_pack_contents, progress=errstream.write)
                 r.object_store.generate_pack_contents, progress=errstream.write)
-            errstream.write(b"Push to " + remote_location_bytes + b" successful.\n")
+            errstream.write(b"Push to " + remote_location_bytes +
+                            b" successful.\n")
         except (UpdateRefsError, SendPackError) as e:
         except (UpdateRefsError, SendPackError) as e:
-            errstream.write(b"Push to " + remote_location_bytes + b" failed -> " + e.message.encode(err_encoding) + b"\n")
+            errstream.write(b"Push to " + remote_location_bytes +
+                            b" failed -> " + e.message.encode(err_encoding) +
+                            b"\n")
 
 
 
 
-def pull(repo, remote_location, refs_path,
+def pull(repo, remote_location, refspecs=None,
          outstream=sys.stdout, errstream=sys.stderr):
          outstream=sys.stdout, errstream=sys.stderr):
     """Pull from remote via dulwich.client
     """Pull from remote via dulwich.client
 
 
     :param repo: Path to repository
     :param repo: Path to repository
     :param remote_location: Location of the remote
     :param remote_location: Location of the remote
-    :param refs_path: relative path to the fetched refs
+    :param refspec: refspecs to fetch
     :param outstream: A stream file to write to output
     :param outstream: A stream file to write to output
     :param errstream: A stream file to write to errors
     :param errstream: A stream file to write to errors
     """
     """
-
     # Open the repo
     # Open the repo
     with open_repo_closing(repo) as r:
     with open_repo_closing(repo) as r:
+        selected_refs = []
+        def determine_wants(remote_refs):
+            selected_refs.extend(parse_reftuples(remote_refs, r.refs, refspecs))
+            return [remote_refs[lh] for (lh, rh, force) in selected_refs]
         client, path = get_transport_and_path(remote_location)
         client, path = get_transport_and_path(remote_location)
-        remote_refs = client.fetch(path, r, progress=errstream.write)
-        r[b'HEAD'] = remote_refs[refs_path]
+        remote_refs = client.fetch(path, r, progress=errstream.write,
+                determine_wants=determine_wants)
+        for (lh, rh, force) in selected_refs:
+            r.refs[rh] = remote_refs[lh]
+        if selected_refs:
+            r[b'HEAD'] = remote_refs[selected_refs[0][1]]
 
 
         # Perform 'git checkout .' - syncs staged changes
         # Perform 'git checkout .' - syncs staged changes
         tree = r[b"HEAD"].tree
         tree = r[b"HEAD"].tree
@@ -762,3 +811,38 @@ def fetch(repo, remote_location, outstream=sys.stdout, errstream=sys.stderr):
         client, path = get_transport_and_path(remote_location)
         client, path = get_transport_and_path(remote_location)
         remote_refs = client.fetch(path, r, progress=errstream.write)
         remote_refs = client.fetch(path, r, progress=errstream.write)
     return remote_refs
     return remote_refs
+
+
+def ls_remote(remote):
+    client, host_path = get_transport_and_path(remote)
+    return client.get_refs(encode_path(host_path))
+
+
+def repack(repo):
+    """Repack loose files in a repository.
+
+    Currently this only packs loose objects.
+
+    :param repo: Path to the repository
+    """
+    with open_repo_closing(repo) as r:
+        r.object_store.pack_loose_objects()
+
+
+def pack_objects(repo, object_ids, packf, idxf, delta_window_size=None):
+    """Pack objects into a file.
+
+    :param repo: Path to the repository
+    :param object_ids: List of object ids to write
+    :param packf: File-like object to write to
+    :param idxf: File-like object to write to (can be None)
+    """
+    with open_repo_closing(repo) as r:
+        entries, data_sum = write_pack_objects(
+            packf,
+            r.object_store.iter_shas((oid, None) for oid in object_ids),
+            delta_window_size=delta_window_size)
+    if idxf is not None:
+        entries = [(k, v[0], v[1]) for (k, v) in entries.items()]
+        entries.sort()
+        write_pack_index(idxf, entries, data_sum)

+ 5 - 4
dulwich/refs.py

@@ -23,13 +23,13 @@
 """
 """
 import errno
 import errno
 import os
 import os
+import sys
 
 
 from dulwich.errors import (
 from dulwich.errors import (
     PackedRefsException,
     PackedRefsException,
     RefFormatError,
     RefFormatError,
     )
     )
 from dulwich.objects import (
 from dulwich.objects import (
-    hex_to_sha,
     git_line,
     git_line,
     valid_hexsha,
     valid_hexsha,
     )
     )
@@ -398,7 +398,7 @@ class DiskRefsContainer(RefsContainer):
             dir = root[len(path):].strip(os.path.sep).replace(os.path.sep, "/")
             dir = root[len(path):].strip(os.path.sep).replace(os.path.sep, "/")
             for filename in files:
             for filename in files:
                 refname = (("%s/%s" % (dir, filename))
                 refname = (("%s/%s" % (dir, filename))
-                           .strip("/").encode('ascii'))
+                           .strip("/").encode(sys.getfilesystemencoding()))
                 # check_ref_format requires at least one /, so we prepend the
                 # check_ref_format requires at least one /, so we prepend the
                 # base before calling it.
                 # base before calling it.
                 if check_ref_format(base + b'/' + refname):
                 if check_ref_format(base + b'/' + refname):
@@ -416,7 +416,7 @@ class DiskRefsContainer(RefsContainer):
         for root, dirs, files in os.walk(self.refpath(b'refs')):
         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(os.path.sep).replace(os.path.sep, "/")
             for filename in files:
             for filename in files:
-                refname = ("%s/%s" % (dir, filename)).strip("/").encode('ascii')
+                refname = ("%s/%s" % (dir, filename)).encode(sys.getfilesystemencoding())
                 if check_ref_format(refname):
                 if check_ref_format(refname):
                     allkeys.add(refname)
                     allkeys.add(refname)
         allkeys.update(self.get_packed_refs())
         allkeys.update(self.get_packed_refs())
@@ -426,7 +426,8 @@ class DiskRefsContainer(RefsContainer):
         """Return the disk path of a ref.
         """Return the disk path of a ref.
 
 
         """
         """
-        name = name.decode('ascii')
+        if getattr(self.path, "encode", None) and getattr(name, "decode", None):
+            name = name.decode(sys.getfilesystemencoding())
         if os.path.sep != "/":
         if os.path.sep != "/":
             name = name.replace("/", os.path.sep)
             name = name.replace("/", os.path.sep)
         return os.path.join(self.path, name)
         return os.path.join(self.path, name)

+ 22 - 2
dulwich/repo.py

@@ -227,8 +227,8 @@ class BaseRepo(object):
         if determine_wants is None:
         if determine_wants is None:
             determine_wants = target.object_store.determine_wants_all
             determine_wants = target.object_store.determine_wants_all
         target.object_store.add_objects(
         target.object_store.add_objects(
-          self.fetch_objects(determine_wants, target.get_graph_walker(),
-                             progress))
+            self.fetch_objects(determine_wants, target.get_graph_walker(),
+                               progress))
         return self.get_refs()
         return self.get_refs()
 
 
     def fetch_objects(self, determine_wants, graph_walker, progress,
     def fetch_objects(self, determine_wants, graph_walker, progress,
@@ -678,6 +678,26 @@ class Repo(BaseRepo):
         self.hooks['commit-msg'] = CommitMsgShellHook(self.controldir())
         self.hooks['commit-msg'] = CommitMsgShellHook(self.controldir())
         self.hooks['post-commit'] = PostCommitShellHook(self.controldir())
         self.hooks['post-commit'] = PostCommitShellHook(self.controldir())
 
 
+    @classmethod
+    def discover(cls, start='.'):
+        """Iterate parent directories to discover a repository
+
+        Return a Repo object for the first parent directory that looks like a
+        Git repository.
+
+        :param start: The directory to start discovery from (defaults to '.')
+        """
+        remaining = True
+        path = os.path.abspath(start)
+        while remaining:
+            try:
+                return cls(path)
+            except NotGitRepository:
+                path, remaining = os.path.split(path)
+        raise NotGitRepository(
+            "No git repository was found at %(path)s" % dict(path=start)
+        )
+
     def controldir(self):
     def controldir(self):
         """Return the path of the control directory."""
         """Return the path of the control directory."""
         return self._controldir
         return self._controldir

+ 3 - 2
dulwich/server.py

@@ -391,7 +391,8 @@ def _find_shallow(store, heads, depth):
 
 
     :param store: An ObjectStore for looking up objects.
     :param store: An ObjectStore for looking up objects.
     :param heads: Iterable of head SHAs to start walking from.
     :param heads: Iterable of head SHAs to start walking from.
-    :param depth: The depth of ancestors to include.
+    :param depth: The depth of ancestors to include. A depth of one includes
+        only the heads themselves.
     :return: A tuple of (shallow, not_shallow), sets of SHAs that should be
     :return: A tuple of (shallow, not_shallow), sets of SHAs that should be
         considered shallow and unshallow according to the arguments. Note that
         considered shallow and unshallow according to the arguments. Note that
         these sets may overlap if a commit is reachable along multiple paths.
         these sets may overlap if a commit is reachable along multiple paths.
@@ -408,7 +409,7 @@ def _find_shallow(store, heads, depth):
     for head_sha in heads:
     for head_sha in heads:
         obj = store.peel_sha(head_sha)
         obj = store.peel_sha(head_sha)
         if isinstance(obj, Commit):
         if isinstance(obj, Commit):
-            todo.append((obj.id, 0))
+            todo.append((obj.id, 1))
 
 
     not_shallow = set()
     not_shallow = set()
     shallow = set()
     shallow = set()

+ 31 - 10
dulwich/tests/compat/server_utils.py

@@ -171,11 +171,32 @@ class ServerTests(object):
         run_git_or_fail(['clone', '--mirror', '--depth=1', '--no-single-branch',
         run_git_or_fail(['clone', '--mirror', '--depth=1', '--no-single-branch',
                         self.url(port), self._stub_repo.path])
                         self.url(port), self._stub_repo.path])
         clone = self._stub_repo = Repo(self._stub_repo.path)
         clone = self._stub_repo = Repo(self._stub_repo.path)
-        expected_shallow = [b'94de09a530df27ac3bb613aaecdd539e0a0655e1',
-                            b'da5cd81e1883c62a25bb37c4d1f8ad965b29bf8d']
+        expected_shallow = [b'35e0b59e187dd72a0af294aedffc213eaa4d03ff',
+                            b'514dc6d3fbfe77361bcaef320c4d21b72bc10be9']
         self.assertEqual(expected_shallow, _get_shallow(clone))
         self.assertEqual(expected_shallow, _get_shallow(clone))
         self.assertReposNotEqual(clone, self._source_repo)
         self.assertReposNotEqual(clone, self._source_repo)
 
 
+    def test_shallow_clone_from_git_is_identical(self):
+        require_git_version(self.min_single_branch_version)
+        self._source_repo = self.import_repo('server_new.export')
+        self._stub_repo_git = _StubRepo('shallow-git')
+        self.addCleanup(tear_down_repo, self._stub_repo_git)
+        self._stub_repo_dw = _StubRepo('shallow-dw')
+        self.addCleanup(tear_down_repo, self._stub_repo_dw)
+
+        # shallow clone using stock git, then using dulwich
+        run_git_or_fail(['clone', '--mirror', '--depth=1', '--no-single-branch',
+                         'file://' + self._source_repo.path,
+                         self._stub_repo_git.path])
+
+        port = self._start_server(self._source_repo)
+        run_git_or_fail(['clone', '--mirror', '--depth=1', '--no-single-branch',
+                        self.url(port), self._stub_repo_dw.path])
+
+        # compare the two clones; they should be equal
+        self.assertReposEqual(Repo(self._stub_repo_git.path),
+                              Repo(self._stub_repo_dw.path))
+
     def test_fetch_same_depth_into_shallow_clone_from_dulwich(self):
     def test_fetch_same_depth_into_shallow_clone_from_dulwich(self):
         require_git_version(self.min_single_branch_version)
         require_git_version(self.min_single_branch_version)
         self._source_repo = self.import_repo('server_new.export')
         self._source_repo = self.import_repo('server_new.export')
@@ -183,14 +204,14 @@ class ServerTests(object):
         self.addCleanup(tear_down_repo, self._stub_repo)
         self.addCleanup(tear_down_repo, self._stub_repo)
         port = self._start_server(self._source_repo)
         port = self._start_server(self._source_repo)
 
 
-        # Fetch at depth 1
-        run_git_or_fail(['clone', '--mirror', '--depth=1', '--no-single-branch',
+        # Fetch at depth 2
+        run_git_or_fail(['clone', '--mirror', '--depth=2', '--no-single-branch',
                         self.url(port), self._stub_repo.path])
                         self.url(port), self._stub_repo.path])
         clone = self._stub_repo = Repo(self._stub_repo.path)
         clone = self._stub_repo = Repo(self._stub_repo.path)
 
 
         # Fetching at the same depth is a no-op.
         # Fetching at the same depth is a no-op.
         run_git_or_fail(
         run_git_or_fail(
-          ['fetch', '--depth=1', self.url(port)] + self.branch_args(),
+          ['fetch', '--depth=2', self.url(port)] + self.branch_args(),
           cwd=self._stub_repo.path)
           cwd=self._stub_repo.path)
         expected_shallow = [b'94de09a530df27ac3bb613aaecdd539e0a0655e1',
         expected_shallow = [b'94de09a530df27ac3bb613aaecdd539e0a0655e1',
                             b'da5cd81e1883c62a25bb37c4d1f8ad965b29bf8d']
                             b'da5cd81e1883c62a25bb37c4d1f8ad965b29bf8d']
@@ -204,19 +225,19 @@ class ServerTests(object):
         self.addCleanup(tear_down_repo, self._stub_repo)
         self.addCleanup(tear_down_repo, self._stub_repo)
         port = self._start_server(self._source_repo)
         port = self._start_server(self._source_repo)
 
 
-        # Fetch at depth 1
-        run_git_or_fail(['clone', '--mirror', '--depth=1', '--no-single-branch',
+        # Fetch at depth 2
+        run_git_or_fail(['clone', '--mirror', '--depth=2', '--no-single-branch',
                         self.url(port), self._stub_repo.path])
                         self.url(port), self._stub_repo.path])
         clone = self._stub_repo = Repo(self._stub_repo.path)
         clone = self._stub_repo = Repo(self._stub_repo.path)
 
 
         # Fetching at the same depth is a no-op.
         # Fetching at the same depth is a no-op.
         run_git_or_fail(
         run_git_or_fail(
-          ['fetch', '--depth=1', self.url(port)] + self.branch_args(),
+          ['fetch', '--depth=2', self.url(port)] + self.branch_args(),
           cwd=self._stub_repo.path)
           cwd=self._stub_repo.path)
 
 
-        # The whole repo only has depth 3, so it should equal server_new.
+        # The whole repo only has depth 4, so it should equal server_new.
         run_git_or_fail(
         run_git_or_fail(
-          ['fetch', '--depth=3', self.url(port)] + self.branch_args(),
+          ['fetch', '--depth=4', self.url(port)] + self.branch_args(),
           cwd=self._stub_repo.path)
           cwd=self._stub_repo.path)
         self.assertEqual([], _get_shallow(clone))
         self.assertEqual([], _get_shallow(clone))
         self.assertReposEqual(clone, self._source_repo)
         self.assertReposEqual(clone, self._source_repo)

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

@@ -60,6 +60,7 @@ from dulwich import (
 from dulwich.tests import (
 from dulwich.tests import (
     get_safe_env,
     get_safe_env,
     SkipTest,
     SkipTest,
+    expectedFailure,
     )
     )
 from dulwich.tests.utils import (
 from dulwich.tests.utils import (
     skipIfPY3,
     skipIfPY3,
@@ -105,7 +106,7 @@ class DulwichClientTestBase(object):
         with closing(repo.Repo(srcpath)) as src:
         with closing(repo.Repo(srcpath)) as src:
             sendrefs = dict(src.get_refs())
             sendrefs = dict(src.get_refs())
             del sendrefs[b'HEAD']
             del sendrefs[b'HEAD']
-            c.send_pack(self._build_path('/dest'), lambda _: sendrefs,
+            c.send_pack(self._build_path(b'/dest'), lambda _: sendrefs,
                         src.object_store.generate_pack_contents)
                         src.object_store.generate_pack_contents)
 
 
     def test_send_pack(self):
     def test_send_pack(self):
@@ -125,7 +126,7 @@ class DulwichClientTestBase(object):
         with closing(repo.Repo(srcpath)) as src:
         with closing(repo.Repo(srcpath)) as src:
             sendrefs = dict(src.get_refs())
             sendrefs = dict(src.get_refs())
             del sendrefs[b'HEAD']
             del sendrefs[b'HEAD']
-            c.send_pack(self._build_path('/dest'), lambda _: sendrefs,
+            c.send_pack(self._build_path(b'/dest'), lambda _: sendrefs,
                         src.object_store.generate_pack_contents)
                         src.object_store.generate_pack_contents)
             self.assertDestEqualsSrc()
             self.assertDestEqualsSrc()
 
 
@@ -163,7 +164,7 @@ class DulwichClientTestBase(object):
             sendrefs, gen_pack = self.compute_send(src)
             sendrefs, gen_pack = self.compute_send(src)
             c = self._client()
             c = self._client()
             try:
             try:
-                c.send_pack(self._build_path('/dest'), lambda _: sendrefs, gen_pack)
+                c.send_pack(self._build_path(b'/dest'), lambda _: sendrefs, gen_pack)
             except errors.UpdateRefsError as e:
             except errors.UpdateRefsError as e:
                 self.assertEqual('refs/heads/master failed to update',
                 self.assertEqual('refs/heads/master failed to update',
                                  e.args[0])
                                  e.args[0])
@@ -181,7 +182,7 @@ class DulwichClientTestBase(object):
             sendrefs, gen_pack = self.compute_send(src)
             sendrefs, gen_pack = self.compute_send(src)
             c = self._client()
             c = self._client()
             try:
             try:
-                c.send_pack(self._build_path('/dest'), lambda _: sendrefs, gen_pack)
+                c.send_pack(self._build_path(b'/dest'), lambda _: sendrefs, gen_pack)
             except errors.UpdateRefsError as e:
             except errors.UpdateRefsError as e:
                 self.assertIn(str(e),
                 self.assertIn(str(e),
                               ['{0}, {1} failed to update'.format(
                               ['{0}, {1} failed to update'.format(
@@ -195,7 +196,7 @@ class DulwichClientTestBase(object):
     def test_archive(self):
     def test_archive(self):
         c = self._client()
         c = self._client()
         f = BytesIO()
         f = BytesIO()
-        c.archive(self._build_path('/server_new.export'), b'HEAD', f.write)
+        c.archive(self._build_path(b'/server_new.export'), b'HEAD', f.write)
         f.seek(0)
         f.seek(0)
         tf = tarfile.open(fileobj=f)
         tf = tarfile.open(fileobj=f)
         self.assertEqual(['baz', 'foo'], tf.getnames())
         self.assertEqual(['baz', 'foo'], tf.getnames())
@@ -203,7 +204,7 @@ class DulwichClientTestBase(object):
     def test_fetch_pack(self):
     def test_fetch_pack(self):
         c = self._client()
         c = self._client()
         with closing(repo.Repo(os.path.join(self.gitroot, 'dest'))) as dest:
         with closing(repo.Repo(os.path.join(self.gitroot, 'dest'))) as dest:
-            refs = c.fetch(self._build_path('/server_new.export'), dest)
+            refs = c.fetch(self._build_path(b'/server_new.export'), dest)
             for r in refs.items():
             for r in refs.items():
                 dest.refs.set_if_equals(r[0], None, r[1])
                 dest.refs.set_if_equals(r[0], None, r[1])
             self.assertDestEqualsSrc()
             self.assertDestEqualsSrc()
@@ -215,7 +216,7 @@ class DulwichClientTestBase(object):
         c = self._client()
         c = self._client()
         repo_dir = os.path.join(self.gitroot, 'server_new.export')
         repo_dir = os.path.join(self.gitroot, 'server_new.export')
         with closing(repo.Repo(repo_dir)) as dest:
         with closing(repo.Repo(repo_dir)) as dest:
-            refs = c.fetch(self._build_path('/dest'), dest)
+            refs = c.fetch(self._build_path(b'/dest'), dest)
             for r in refs.items():
             for r in refs.items():
                 dest.refs.set_if_equals(r[0], None, r[1])
                 dest.refs.set_if_equals(r[0], None, r[1])
             self.assertDestEqualsSrc()
             self.assertDestEqualsSrc()
@@ -224,7 +225,7 @@ class DulwichClientTestBase(object):
         c = self._client()
         c = self._client()
         c._fetch_capabilities.remove(b'side-band-64k')
         c._fetch_capabilities.remove(b'side-band-64k')
         with closing(repo.Repo(os.path.join(self.gitroot, 'dest'))) as dest:
         with closing(repo.Repo(os.path.join(self.gitroot, 'dest'))) as dest:
-            refs = c.fetch(self._build_path('/server_new.export'), dest)
+            refs = c.fetch(self._build_path(b'/server_new.export'), dest)
             for r in refs.items():
             for r in refs.items():
                 dest.refs.set_if_equals(r[0], None, r[1])
                 dest.refs.set_if_equals(r[0], None, r[1])
             self.assertDestEqualsSrc()
             self.assertDestEqualsSrc()
@@ -234,7 +235,7 @@ class DulwichClientTestBase(object):
         # be ignored
         # be ignored
         c = self._client()
         c = self._client()
         with closing(repo.Repo(os.path.join(self.gitroot, 'dest'))) as dest:
         with closing(repo.Repo(os.path.join(self.gitroot, 'dest'))) as dest:
-            refs = c.fetch(self._build_path('/server_new.export'), dest,
+            refs = c.fetch(self._build_path(b'/server_new.export'), dest,
                 lambda refs: [protocol.ZERO_SHA])
                 lambda refs: [protocol.ZERO_SHA])
             for r in refs.items():
             for r in refs.items():
                 dest.refs.set_if_equals(r[0], None, r[1])
                 dest.refs.set_if_equals(r[0], None, r[1])
@@ -250,9 +251,17 @@ class DulwichClientTestBase(object):
             gen_pack = lambda have, want: []
             gen_pack = lambda have, want: []
             c = self._client()
             c = self._client()
             self.assertEqual(dest.refs[b"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)
+            c.send_pack(self._build_path(b'/dest'), lambda _: sendrefs, gen_pack)
             self.assertFalse(b"refs/heads/abranch" in dest.refs)
             self.assertFalse(b"refs/heads/abranch" in dest.refs)
 
 
+    def test_get_refs(self):
+        c = self._client()
+        refs = c.get_refs(self._build_path(b'/server_new.export'))
+
+        repo_dir = os.path.join(self.gitroot, 'server_new.export')
+        with closing(repo.Repo(repo_dir)) as dest:
+            self.assertDictEqual(dest.refs.as_dict(), refs)
+
 
 
 class DulwichTCPClientTest(CompatTestCase, DulwichClientTestBase):
 class DulwichTCPClientTest(CompatTestCase, DulwichClientTestBase):
 
 
@@ -300,17 +309,23 @@ class DulwichTCPClientTest(CompatTestCase, DulwichClientTestBase):
         CompatTestCase.tearDown(self)
         CompatTestCase.tearDown(self)
 
 
     def _client(self):
     def _client(self):
-        return client.TCPGitClient(b'localhost')
+        return client.TCPGitClient('localhost')
 
 
     def _build_path(self, path):
     def _build_path(self, path):
-        return path.encode(sys.getfilesystemencoding())
+        return path
+
+    if sys.platform == 'win32':
+        @expectedFailure
+        def test_fetch_pack_no_side_band_64k(self):
+            DulwichClientTestBase.test_fetch_pack_no_side_band_64k(self)
 
 
 
 
 class TestSSHVendor(object):
 class TestSSHVendor(object):
+
     @staticmethod
     @staticmethod
     def run_command(host, command, username=None, port=None):
     def run_command(host, command, username=None, port=None):
         cmd, path = command
         cmd, path = command
-        cmd = cmd.split('-', 1)
+        cmd = cmd.split(b'-', 1)
         p = subprocess.Popen(cmd + [path], bufsize=0, env=get_safe_env(), stdin=subprocess.PIPE,
         p = subprocess.Popen(cmd + [path], bufsize=0, env=get_safe_env(), stdin=subprocess.PIPE,
                              stdout=subprocess.PIPE, stderr=subprocess.PIPE)
                              stdout=subprocess.PIPE, stderr=subprocess.PIPE)
         return client.SubprocessWrapper(p)
         return client.SubprocessWrapper(p)
@@ -330,10 +345,10 @@ class DulwichMockSSHClientTest(CompatTestCase, DulwichClientTestBase):
         client.get_ssh_vendor = self.real_vendor
         client.get_ssh_vendor = self.real_vendor
 
 
     def _client(self):
     def _client(self):
-        return client.SSHGitClient(b'localhost')
+        return client.SSHGitClient('localhost')
 
 
     def _build_path(self, path):
     def _build_path(self, path):
-        return self.gitroot + path
+        return self.gitroot.encode(sys.getfilesystemencoding()) + path
 
 
 
 
 class DulwichSubprocessClientTest(CompatTestCase, DulwichClientTestBase):
 class DulwichSubprocessClientTest(CompatTestCase, DulwichClientTestBase):
@@ -350,7 +365,7 @@ class DulwichSubprocessClientTest(CompatTestCase, DulwichClientTestBase):
         return client.SubprocessGitClient(stderr=subprocess.PIPE)
         return client.SubprocessGitClient(stderr=subprocess.PIPE)
 
 
     def _build_path(self, path):
     def _build_path(self, path):
-        return self.gitroot + path
+        return self.gitroot.encode(sys.getfilesystemencoding()) + path
 
 
 
 
 class GitHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
 class GitHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):

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

@@ -187,6 +187,11 @@ class DumbWebTestCase(WebTests, CompatTestCase):
         # clones.
         # clones.
         raise SkipTest('Dumb web shallow cloning not supported.')
         raise SkipTest('Dumb web shallow cloning not supported.')
 
 
+    def test_shallow_clone_from_git_is_identical(self):
+        # Note: remove this if C git and dulwich implement dumb web shallow
+        # clones.
+        raise SkipTest('Dumb web shallow cloning not supported.')
+
     def test_fetch_same_depth_into_shallow_clone_from_dulwich(self):
     def test_fetch_same_depth_into_shallow_clone_from_dulwich(self):
         # Note: remove this if C git and dulwich implement dumb web shallow
         # Note: remove this if C git and dulwich implement dumb web shallow
         # clones.
         # clones.

+ 6 - 3
dulwich/tests/test_blackbox.py

@@ -41,14 +41,15 @@ class GitReceivePackTests(BlackboxTestCase):
     def test_basic(self):
     def test_basic(self):
         process = self.run_command("dul-receive-pack", [self.path])
         process = self.run_command("dul-receive-pack", [self.path])
         (stdout, stderr) = process.communicate(b"0000")
         (stdout, stderr) = process.communicate(b"0000")
-        self.assertEqual(b'', stderr, stderr)
         self.assertEqual(b'0000', stdout[-4:])
         self.assertEqual(b'0000', stdout[-4:])
         self.assertEqual(0, process.returncode)
         self.assertEqual(0, process.returncode)
 
 
     def test_missing_arg(self):
     def test_missing_arg(self):
         process = self.run_command("dul-receive-pack", [])
         process = self.run_command("dul-receive-pack", [])
         (stdout, stderr) = process.communicate()
         (stdout, stderr) = process.communicate()
-        self.assertEqual([b'usage: dul-receive-pack <git-dir>'], stderr.splitlines())
+        self.assertEqual(
+            [b'usage: dul-receive-pack <git-dir>'],
+            stderr.splitlines()[-1:])
         self.assertEqual(b'', stdout)
         self.assertEqual(b'', stdout)
         self.assertEqual(1, process.returncode)
         self.assertEqual(1, process.returncode)
 
 
@@ -65,6 +66,8 @@ class GitUploadPackTests(BlackboxTestCase):
     def test_missing_arg(self):
     def test_missing_arg(self):
         process = self.run_command("dul-upload-pack", [])
         process = self.run_command("dul-upload-pack", [])
         (stdout, stderr) = process.communicate()
         (stdout, stderr) = process.communicate()
-        self.assertEqual([b'usage: dul-upload-pack <git-dir>'], stderr.splitlines())
+        self.assertEqual(
+            [b'usage: dul-upload-pack <git-dir>'],
+            stderr.splitlines()[-1:])
         self.assertEqual(b'', stdout)
         self.assertEqual(b'', stdout)
         self.assertEqual(1, process.returncode)
         self.assertEqual(1, process.returncode)

+ 26 - 8
dulwich/tests/test_client.py

@@ -498,6 +498,10 @@ class TestSSHVendor(object):
         self.port = None
         self.port = None
 
 
     def run_command(self, host, command, username=None, port=None):
     def run_command(self, host, command, username=None, port=None):
+        if (type(command) is not list or
+            not all([isinstance(b, bytes) for b in command])):
+            raise TypeError(command)
+
         self.host = host
         self.host = host
         self.command = command
         self.command = command
         self.username = username
         self.username = username
@@ -527,13 +531,19 @@ class SSHGitClientTests(TestCase):
         client.get_ssh_vendor = self.real_vendor
         client.get_ssh_vendor = self.real_vendor
 
 
     def test_default_command(self):
     def test_default_command(self):
-        self.assertEqual('git-upload-pack',
+        self.assertEqual([b'git-upload-pack'],
                 self.client._get_cmd_path(b'upload-pack'))
                 self.client._get_cmd_path(b'upload-pack'))
 
 
     def test_alternative_command_path(self):
     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.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_alternative_command_path_spaces(self):
+        self.client.alternative_paths[b'upload-pack'] = (
+            b'/usr/lib/git/git-upload-pack -ibla')
+        self.assertEqual([b'/usr/lib/git/git-upload-pack', b'-ibla'],
             self.client._get_cmd_path(b'upload-pack'))
             self.client._get_cmd_path(b'upload-pack'))
 
 
     def test_connect(self):
     def test_connect(self):
@@ -543,13 +553,13 @@ class SSHGitClientTests(TestCase):
         client.username = b"username"
         client.username = b"username"
         client.port = 1337
         client.port = 1337
 
 
-        client._connect(b"command", "/path/to/repo")
+        client._connect(b"command", b"/path/to/repo")
         self.assertEqual(b"username", server.username)
         self.assertEqual(b"username", server.username)
         self.assertEqual(1337, server.port)
         self.assertEqual(1337, server.port)
-        self.assertEqual(["git-command", "/path/to/repo"], server.command)
+        self.assertEqual([b"git-command", b"/path/to/repo"], server.command)
 
 
-        client._connect(b"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", b"~/path/to/repo"],
                           server.command)
                           server.command)
 
 
 
 
@@ -627,6 +637,14 @@ class LocalGitClientTests(TestCase):
         with closing(Repo.init_bare(target_path)) as target:
         with closing(Repo.init_bare(target_path)) as target:
             self.send_and_verify(b"master", local, target)
             self.send_and_verify(b"master", local, target)
 
 
+    def test_get_refs(self):
+        local = open_repo('refs.git')
+        self.addCleanup(tear_down_repo, local)
+
+        client = LocalGitClient()
+        refs = client.get_refs(local.path)
+        self.assertDictEqual(local.refs.as_dict(), refs)
+
     def send_and_verify(self, branch, local, target):
     def send_and_verify(self, branch, local, target):
         client = LocalGitClient()
         client = LocalGitClient()
         ref_name = b"refs/heads/" + branch
         ref_name = b"refs/heads/" + branch

+ 24 - 17
dulwich/tests/test_config.py

@@ -30,7 +30,9 @@ from dulwich.config import (
     _parse_string,
     _parse_string,
     _unescape_value,
     _unescape_value,
     )
     )
-from dulwich.tests import TestCase
+from dulwich.tests import (
+    TestCase,
+    )
 
 
 
 
 class ConfigFileTests(TestCase):
 class ConfigFileTests(TestCase):
@@ -150,6 +152,15 @@ class ConfigFileTests(TestCase):
         cf = self.from_file(b"[branch.foo] foo = bar\n")
         cf = self.from_file(b"[branch.foo] foo = bar\n")
         self.assertEqual(b"bar", cf.get((b"branch", b"foo"), b"foo"))
         self.assertEqual(b"bar", cf.get((b"branch", b"foo"), b"foo"))
 
 
+    #@expectedFailure
+    def test_quoted(self):
+        cf = self.from_file(b"""[gui]
+	fontdiff = -family \\\"Ubuntu Mono\\\" -size 11 -weight normal -slant roman -underline 0 -overstrike 0
+""")
+        self.assertEqual(ConfigFile({(b'gui', ): {
+            b'fontdiff': b'-family "Ubuntu Mono" -size 11 -weight normal -slant roman -underline 0 -overstrike 0',
+        }}), cf)
+
 
 
 class ConfigDictTests(TestCase):
 class ConfigDictTests(TestCase):
 
 
@@ -205,28 +216,12 @@ class ConfigDictTests(TestCase):
             list(cd.itersections()))
             list(cd.itersections()))
 
 
 
 
-
 class StackedConfigTests(TestCase):
 class StackedConfigTests(TestCase):
 
 
     def test_default_backends(self):
     def test_default_backends(self):
         StackedConfig.default_backends()
         StackedConfig.default_backends()
 
 
 
 
-class UnescapeTests(TestCase):
-
-    def test_nothing(self):
-        self.assertEqual(b"", bytes(_unescape_value(bytearray())))
-
-    def test_tab(self):
-        self.assertEqual(b"\tbar\t", bytes(_unescape_value(bytearray(b"\\tbar\\t"))))
-
-    def test_newline(self):
-        self.assertEqual(b"\nbar\t", bytes(_unescape_value(bytearray(b"\\nbar\\t"))))
-
-    def test_quote(self):
-        self.assertEqual(b"\"foo\"", bytes(_unescape_value(bytearray(b"\\\"foo\\\""))))
-
-
 class EscapeValueTests(TestCase):
 class EscapeValueTests(TestCase):
 
 
     def test_nothing(self):
     def test_nothing(self):
@@ -260,6 +255,18 @@ class ParseStringTests(TestCase):
         self.assertEqual(b'foo', _parse_string(b"foo"))
         self.assertEqual(b'foo', _parse_string(b"foo"))
         self.assertEqual(b'foo bar', _parse_string(b"foo bar"))
         self.assertEqual(b'foo bar', _parse_string(b"foo bar"))
 
 
+    def test_nothing(self):
+        self.assertEqual(b"", _parse_string(b''))
+
+    def test_tab(self):
+        self.assertEqual(b"\tbar\t", _parse_string(b"\\tbar\\t"))
+
+    def test_newline(self):
+        self.assertEqual(b"\nbar\t", _parse_string(b"\\nbar\\t\t"))
+
+    def test_quote(self):
+        self.assertEqual(b"\"foo\"", _parse_string(b"\\\"foo\\\""))
+
 
 
 class CheckVariableNameTests(TestCase):
 class CheckVariableNameTests(TestCase):
 
 

+ 4 - 4
dulwich/tests/test_file.py

@@ -23,7 +23,7 @@ import shutil
 import sys
 import sys
 import tempfile
 import tempfile
 
 
-from dulwich.file import GitFile, fancy_rename
+from dulwich.file import GitFile, _fancy_rename
 from dulwich.tests import (
 from dulwich.tests import (
     SkipTest,
     SkipTest,
     TestCase,
     TestCase,
@@ -53,7 +53,7 @@ class FancyRenameTests(TestCase):
 
 
     def test_no_dest_exists(self):
     def test_no_dest_exists(self):
         self.assertFalse(os.path.exists(self.bar))
         self.assertFalse(os.path.exists(self.bar))
-        fancy_rename(self.foo, self.bar)
+        _fancy_rename(self.foo, self.bar)
         self.assertFalse(os.path.exists(self.foo))
         self.assertFalse(os.path.exists(self.foo))
 
 
         new_f = open(self.bar, 'rb')
         new_f = open(self.bar, 'rb')
@@ -62,7 +62,7 @@ class FancyRenameTests(TestCase):
 
 
     def test_dest_exists(self):
     def test_dest_exists(self):
         self.create(self.bar, b'bar contents')
         self.create(self.bar, b'bar contents')
-        fancy_rename(self.foo, self.bar)
+        _fancy_rename(self.foo, self.bar)
         self.assertFalse(os.path.exists(self.foo))
         self.assertFalse(os.path.exists(self.foo))
 
 
         new_f = open(self.bar, 'rb')
         new_f = open(self.bar, 'rb')
@@ -74,7 +74,7 @@ class FancyRenameTests(TestCase):
             raise SkipTest("platform allows overwriting open files")
             raise SkipTest("platform allows overwriting open files")
         self.create(self.bar, b'bar contents')
         self.create(self.bar, b'bar contents')
         dest_f = open(self.bar, 'rb')
         dest_f = open(self.bar, 'rb')
-        self.assertRaises(OSError, fancy_rename, self.foo, self.bar)
+        self.assertRaises(OSError, _fancy_rename, self.foo, self.bar)
         dest_f.close()
         dest_f.close()
         self.assertTrue(os.path.exists(self.path('foo')))
         self.assertTrue(os.path.exists(self.path('foo')))
 
 

+ 1 - 0
dulwich/tests/test_object_store.py

@@ -392,6 +392,7 @@ class TreeLookupPathTests(TestCase):
     def test_lookup_not_tree(self):
     def test_lookup_not_tree(self):
         self.assertRaises(NotTreeError, tree_lookup_path, self.get_object, self.tree_id, b'ad/b/j')
         self.assertRaises(NotTreeError, tree_lookup_path, self.get_object, self.tree_id, b'ad/b/j')
 
 
+
 class ObjectStoreGraphWalkerTests(TestCase):
 class ObjectStoreGraphWalkerTests(TestCase):
 
 
     def get_walker(self, heads, parent_map):
     def get_walker(self, heads, parent_map):

+ 73 - 0
dulwich/tests/test_objectspec.py

@@ -28,6 +28,10 @@ from dulwich.objects import (
 from dulwich.objectspec import (
 from dulwich.objectspec import (
     parse_object,
     parse_object,
     parse_commit_range,
     parse_commit_range,
+    parse_ref,
+    parse_refs,
+    parse_reftuple,
+    parse_reftuples,
     )
     )
 from dulwich.repo import MemoryRepo
 from dulwich.repo import MemoryRepo
 from dulwich.tests import (
 from dulwich.tests import (
@@ -64,3 +68,72 @@ class ParseCommitRangeTests(TestCase):
         c1, c2, c3 = build_commit_graph(r.object_store, [[1], [2, 1],
         c1, c2, c3 = build_commit_graph(r.object_store, [[1], [2, 1],
             [3, 1, 2]])
             [3, 1, 2]])
         self.assertEqual([c1], list(parse_commit_range(r, c1.id)))
         self.assertEqual([c1], list(parse_commit_range(r, c1.id)))
+
+
+class ParseRefTests(TestCase):
+
+    def test_nonexistent(self):
+        r = {}
+        self.assertRaises(KeyError, parse_ref, r, b"thisdoesnotexist")
+
+    def test_head(self):
+        r = {b"refs/heads/foo": "bla"}
+        self.assertEqual(b"refs/heads/foo", parse_ref(r, b"foo"))
+
+    def test_full(self):
+        r = {b"refs/heads/foo": "bla"}
+        self.assertEqual(b"refs/heads/foo", parse_ref(r, b"refs/heads/foo"))
+
+
+class ParseRefsTests(TestCase):
+
+    def test_nonexistent(self):
+        r = {}
+        self.assertRaises(KeyError, parse_refs, r, [b"thisdoesnotexist"])
+
+    def test_head(self):
+        r = {b"refs/heads/foo": "bla"}
+        self.assertEqual([b"refs/heads/foo"], parse_refs(r, [b"foo"]))
+
+    def test_full(self):
+        r = {b"refs/heads/foo": "bla"}
+        self.assertEqual([b"refs/heads/foo"], parse_refs(r, b"refs/heads/foo"))
+
+
+class ParseReftupleTests(TestCase):
+
+    def test_nonexistent(self):
+        r = {}
+        self.assertRaises(KeyError, parse_reftuple, r, r, b"thisdoesnotexist")
+
+    def test_head(self):
+        r = {b"refs/heads/foo": "bla"}
+        self.assertEqual((b"refs/heads/foo", b"refs/heads/foo", False),
+            parse_reftuple(r, r, b"foo"))
+        self.assertEqual((b"refs/heads/foo", b"refs/heads/foo", True),
+            parse_reftuple(r, r, b"+foo"))
+        self.assertEqual((b"refs/heads/foo", b"refs/heads/foo", True),
+            parse_reftuple(r, {}, b"+foo"))
+
+    def test_full(self):
+        r = {b"refs/heads/foo": "bla"}
+        self.assertEqual((b"refs/heads/foo", b"refs/heads/foo", False),
+            parse_reftuple(r, r, b"refs/heads/foo"))
+
+
+class ParseReftuplesTests(TestCase):
+
+    def test_nonexistent(self):
+        r = {}
+        self.assertRaises(KeyError, parse_reftuples, r, r,
+            [b"thisdoesnotexist"])
+
+    def test_head(self):
+        r = {b"refs/heads/foo": "bla"}
+        self.assertEqual([(b"refs/heads/foo", b"refs/heads/foo", False)],
+            parse_reftuples(r, r, [b"foo"]))
+
+    def test_full(self):
+        r = {b"refs/heads/foo": "bla"}
+        self.assertEqual([(b"refs/heads/foo", b"refs/heads/foo", False)],
+            parse_reftuples(r, r, b"refs/heads/foo"))

+ 1 - 1
dulwich/tests/test_pack.py

@@ -924,7 +924,7 @@ class DeltaChainIteratorTests(TestCase):
             (OFS_DELTA, (1, b'blob3')),
             (OFS_DELTA, (1, b'blob3')),
             (OFS_DELTA, (0, b'bob')),
             (OFS_DELTA, (0, b'bob')),
         ])
         ])
-        self.assertEntriesMatch([0, 2, 1, 3, 4], entries,
+        self.assertEntriesMatch([0, 2, 4, 1, 3], entries,
                                 self.make_pack_iter(f))
                                 self.make_pack_iter(f))
 
 
     def test_long_chain(self):
     def test_long_chain(self):

+ 37 - 15
dulwich/tests/test_porcelain.py

@@ -20,6 +20,10 @@
 
 
 from contextlib import closing
 from contextlib import closing
 from io import BytesIO
 from io import BytesIO
+try:
+    from StringIO import StringIO
+except ImportError:
+    from io import StringIO
 import os
 import os
 import shutil
 import shutil
 import tarfile
 import tarfile
@@ -232,17 +236,17 @@ class LogTests(PorcelainTestCase):
         c1, c2, c3 = build_commit_graph(self.repo.object_store, [[1], [2, 1],
         c1, c2, c3 = build_commit_graph(self.repo.object_store, [[1], [2, 1],
             [3, 1, 2]])
             [3, 1, 2]])
         self.repo.refs[b"HEAD"] = c3.id
         self.repo.refs[b"HEAD"] = c3.id
-        outstream = BytesIO()
+        outstream = StringIO()
         porcelain.log(self.repo.path, outstream=outstream)
         porcelain.log(self.repo.path, outstream=outstream)
-        self.assertEqual(3, outstream.getvalue().count(b"-" * 50))
+        self.assertEqual(3, outstream.getvalue().count("-" * 50))
 
 
     def test_max_entries(self):
     def test_max_entries(self):
         c1, c2, c3 = build_commit_graph(self.repo.object_store, [[1], [2, 1],
         c1, c2, c3 = build_commit_graph(self.repo.object_store, [[1], [2, 1],
             [3, 1, 2]])
             [3, 1, 2]])
         self.repo.refs[b"HEAD"] = c3.id
         self.repo.refs[b"HEAD"] = c3.id
-        outstream = BytesIO()
+        outstream = StringIO()
         porcelain.log(self.repo.path, outstream=outstream, max_entries=1)
         porcelain.log(self.repo.path, outstream=outstream, max_entries=1)
-        self.assertEqual(1, outstream.getvalue().count(b"-" * 50))
+        self.assertEqual(1, outstream.getvalue().count("-" * 50))
 
 
 
 
 class ShowTests(PorcelainTestCase):
 class ShowTests(PorcelainTestCase):
@@ -251,24 +255,24 @@ class ShowTests(PorcelainTestCase):
         c1, c2, c3 = build_commit_graph(self.repo.object_store, [[1], [2, 1],
         c1, c2, c3 = build_commit_graph(self.repo.object_store, [[1], [2, 1],
             [3, 1, 2]])
             [3, 1, 2]])
         self.repo.refs[b"HEAD"] = c3.id
         self.repo.refs[b"HEAD"] = c3.id
-        outstream = BytesIO()
+        outstream = StringIO()
         porcelain.show(self.repo.path, objects=c3.id, outstream=outstream)
         porcelain.show(self.repo.path, objects=c3.id, outstream=outstream)
-        self.assertTrue(outstream.getvalue().startswith(b"-" * 50))
+        self.assertTrue(outstream.getvalue().startswith("-" * 50))
 
 
     def test_simple(self):
     def test_simple(self):
         c1, c2, c3 = build_commit_graph(self.repo.object_store, [[1], [2, 1],
         c1, c2, c3 = build_commit_graph(self.repo.object_store, [[1], [2, 1],
             [3, 1, 2]])
             [3, 1, 2]])
         self.repo.refs[b"HEAD"] = c3.id
         self.repo.refs[b"HEAD"] = c3.id
-        outstream = BytesIO()
+        outstream = StringIO()
         porcelain.show(self.repo.path, objects=[c3.id], outstream=outstream)
         porcelain.show(self.repo.path, objects=[c3.id], outstream=outstream)
-        self.assertTrue(outstream.getvalue().startswith(b"-" * 50))
+        self.assertTrue(outstream.getvalue().startswith("-" * 50))
 
 
     def test_blob(self):
     def test_blob(self):
         b = Blob.from_string(b"The Foo\n")
         b = Blob.from_string(b"The Foo\n")
         self.repo.object_store.add_object(b)
         self.repo.object_store.add_object(b)
-        outstream = BytesIO()
+        outstream = StringIO()
         porcelain.show(self.repo.path, objects=[b.id], outstream=outstream)
         porcelain.show(self.repo.path, objects=[b.id], outstream=outstream)
-        self.assertEqual(outstream.getvalue(), b"The Foo\n")
+        self.assertEqual(outstream.getvalue(), "The Foo\n")
 
 
 
 
 class SymbolicRefTests(PorcelainTestCase):
 class SymbolicRefTests(PorcelainTestCase):
@@ -453,7 +457,8 @@ class PushTests(PorcelainTestCase):
         # Setup target repo cloned from temp test repo
         # Setup target repo cloned from temp test repo
         clone_path = tempfile.mkdtemp()
         clone_path = tempfile.mkdtemp()
         self.addCleanup(shutil.rmtree, clone_path)
         self.addCleanup(shutil.rmtree, clone_path)
-        target_repo = porcelain.clone(self.repo.path, target=clone_path, errstream=errstream)
+        target_repo = porcelain.clone(self.repo.path, target=clone_path,
+            errstream=errstream)
         target_repo.close()
         target_repo.close()
 
 
         # create a second file to be pushed back to origin
         # create a second file to be pushed back to origin
@@ -465,11 +470,11 @@ class PushTests(PorcelainTestCase):
 
 
         # Setup a non-checked out branch in the remote
         # Setup a non-checked out branch in the remote
         refs_path = b"refs/heads/foo"
         refs_path = b"refs/heads/foo"
-        self.repo[refs_path] = self.repo[b'HEAD']
+        self.repo.refs[refs_path] = self.repo[b'HEAD'].id
 
 
         # Push to the remote
         # Push to the remote
-        porcelain.push(clone_path, self.repo.path, refs_path, outstream=outstream,
-                errstream=errstream)
+        porcelain.push(clone_path, self.repo.path, b"HEAD:" + refs_path, outstream=outstream,
+            errstream=errstream)
 
 
         # Check that the target and source
         # Check that the target and source
         with closing(Repo(clone_path)) as r_clone:
         with closing(Repo(clone_path)) as r_clone:
@@ -479,7 +484,8 @@ class PushTests(PorcelainTestCase):
             # this will be in the foo branch.
             # this will be in the foo branch.
             change = list(tree_changes(self.repo, self.repo[b'HEAD'].tree,
             change = list(tree_changes(self.repo, self.repo[b'HEAD'].tree,
                                        self.repo[b'refs/heads/foo'].tree))[0]
                                        self.repo[b'refs/heads/foo'].tree))[0]
-            self.assertEqual(os.path.basename(fullpath), change.new.path.decode('ascii'))
+            self.assertEqual(os.path.basename(fullpath),
+                change.new.path.decode('ascii'))
 
 
 
 
 class PullTests(PorcelainTestCase):
 class PullTests(PorcelainTestCase):
@@ -511,6 +517,9 @@ class PullTests(PorcelainTestCase):
         porcelain.commit(repo=self.repo.path, message=b'test2',
         porcelain.commit(repo=self.repo.path, message=b'test2',
             author=b'test2', committer=b'test2')
             author=b'test2', committer=b'test2')
 
 
+        self.assertTrue(b'refs/heads/master' in self.repo.refs)
+        self.assertTrue(b'refs/heads/master' in target_repo.refs)
+
         # Pull changes into the cloned repo
         # Pull changes into the cloned repo
         porcelain.pull(target_path, self.repo.path, b'refs/heads/master',
         porcelain.pull(target_path, self.repo.path, b'refs/heads/master',
             outstream=outstream, errstream=errstream)
             outstream=outstream, errstream=errstream)
@@ -731,3 +740,16 @@ class FetchTests(PorcelainTestCase):
         # Check the target repo for pushed changes
         # Check the target repo for pushed changes
         with closing(Repo(target_path)) as r:
         with closing(Repo(target_path)) as r:
             self.assertTrue(self.repo[b'HEAD'].id in r)
             self.assertTrue(self.repo[b'HEAD'].id in r)
+
+
+class RepackTests(PorcelainTestCase):
+
+    def test_empty(self):
+        porcelain.repack(self.repo)
+
+    def test_simple(self):
+        handle, fullpath = tempfile.mkstemp(dir=self.repo.path)
+        os.close(handle)
+        filename = os.path.basename(fullpath)
+        porcelain.add(repo=self.repo.path, paths=filename)
+        porcelain.repack(self.repo)

+ 17 - 0
dulwich/tests/test_refs.py

@@ -1,4 +1,5 @@
 # test_refs.py -- tests for refs.py
 # test_refs.py -- tests for refs.py
+# encoding: utf-8
 # Copyright (C) 2013 Jelmer Vernooij <jelmer@samba.org>
 # Copyright (C) 2013 Jelmer Vernooij <jelmer@samba.org>
 #
 #
 # This program is free software; you can redistribute it and/or
 # This program is free software; you can redistribute it and/or
@@ -21,6 +22,7 @@
 
 
 from io import BytesIO
 from io import BytesIO
 import os
 import os
+import sys
 import tempfile
 import tempfile
 
 
 from dulwich import errors
 from dulwich import errors
@@ -39,6 +41,7 @@ from dulwich.refs import (
 from dulwich.repo import Repo
 from dulwich.repo import Repo
 
 
 from dulwich.tests import (
 from dulwich.tests import (
+    SkipTest,
     TestCase,
     TestCase,
     )
     )
 
 
@@ -435,6 +438,20 @@ class DiskRefsContainerTests(RefsContainerTests, TestCase):
                          self._refs.read_ref(b'refs/heads/packed'))
                          self._refs.read_ref(b'refs/heads/packed'))
         self.assertEqual(None, self._refs.read_ref(b'nonexistant'))
         self.assertEqual(None, self._refs.read_ref(b'nonexistant'))
 
 
+    def test_non_ascii(self):
+        try:
+            encoded_ref = u'refs/tags/schön'.encode(sys.getfilesystemencoding())
+        except UnicodeDecodeError:
+            raise SkipTest("filesystem encoding doesn't support special character")
+        p = os.path.join(self._repo.path, 'refs', 'tags', 'schön')
+        with open(p, 'w') as f:
+            f.write('00' * 20)
+
+        expected_refs = dict(_TEST_REFS)
+        expected_refs[encoded_ref] = b'00' * 20
+
+        self.assertEqual(expected_refs, self._repo.get_refs())
+
 
 
 _TEST_REFS_SERIALIZED = (
 _TEST_REFS_SERIALIZED = (
     b'42d06bd4b77fed026b154d16493e5deab78f02ec\trefs/heads/40-char-ref-aaaaaaaaaaaaaaaaaa\n'
     b'42d06bd4b77fed026b154d16493e5deab78f02ec\trefs/heads/40-char-ref-aaaaaaaaaaaaaaaaaa\n'

+ 25 - 0
dulwich/tests/test_repository.py

@@ -35,6 +35,7 @@ from dulwich.object_store import (
     )
     )
 from dulwich import objects
 from dulwich import objects
 from dulwich.config import Config
 from dulwich.config import Config
+from dulwich.errors import NotGitRepository
 from dulwich.repo import (
 from dulwich.repo import (
     Repo,
     Repo,
     MemoryRepo,
     MemoryRepo,
@@ -586,6 +587,17 @@ class BuildRepoRootTests(TestCase):
         tree = r[r[commit_sha].tree]
         tree = r[r[commit_sha].tree]
         self.assertEqual([], list(tree.iteritems()))
         self.assertEqual([], list(tree.iteritems()))
 
 
+    def test_commit_follows(self):
+        r = self._repo
+        r.refs.set_symbolic_ref(b'HEAD', b'refs/heads/bla')
+        commit_sha = r.do_commit(b'commit with strange character',
+             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=b'HEAD')
+        self.assertEqual(commit_sha, r[b'refs/heads/bla'].id)
+
     def test_commit_encoding(self):
     def test_commit_encoding(self):
         r = self._repo
         r = self._repo
         commit_sha = r.do_commit(b'commit with strange character \xee',
         commit_sha = r.do_commit(b'commit with strange character \xee',
@@ -765,3 +777,16 @@ class BuildRepoRootTests(TestCase):
             mode, id = tree_lookup_path(r.get_object, r[commit_sha].tree, name)
             mode, id = tree_lookup_path(r.get_object, r[commit_sha].tree, name)
             self.assertEqual(stat.S_IFREG | 0o644, mode)
             self.assertEqual(stat.S_IFREG | 0o644, mode)
             self.assertEqual(encoding.encode('ascii'), r[id].data)
             self.assertEqual(encoding.encode('ascii'), r[id].data)
+
+    def test_discover_intended(self):
+        path = os.path.join(self._repo_dir, 'b/c')
+        r = Repo.discover(path)
+        self.assertEqual(r.head(), self._repo.head())
+
+    def test_discover_isrepo(self):
+        r = Repo.discover(self._repo_dir)
+        self.assertEqual(r.head(), self._repo.head())
+
+    def test_discover_notrepo(self):
+        with self.assertRaises(NotGitRepository):
+            Repo.discover('/')

+ 11 - 11
dulwich/tests/test_server.py

@@ -235,13 +235,13 @@ class FindShallowTests(TestCase):
         c1, c2, c3 = self.make_linear_commits(3)
         c1, c2, c3 = self.make_linear_commits(3)
 
 
         self.assertEqual((set([c3.id]), set([])),
         self.assertEqual((set([c3.id]), set([])),
-                         _find_shallow(self._store, [c3.id], 0))
-        self.assertEqual((set([c2.id]), set([c3.id])),
                          _find_shallow(self._store, [c3.id], 1))
                          _find_shallow(self._store, [c3.id], 1))
-        self.assertEqual((set([c1.id]), set([c2.id, c3.id])),
+        self.assertEqual((set([c2.id]), set([c3.id])),
                          _find_shallow(self._store, [c3.id], 2))
                          _find_shallow(self._store, [c3.id], 2))
-        self.assertEqual((set([]), set([c1.id, c2.id, c3.id])),
+        self.assertEqual((set([c1.id]), set([c2.id, c3.id])),
                          _find_shallow(self._store, [c3.id], 3))
                          _find_shallow(self._store, [c3.id], 3))
+        self.assertEqual((set([]), set([c1.id, c2.id, c3.id])),
+                         _find_shallow(self._store, [c3.id], 4))
 
 
     def test_multiple_independent(self):
     def test_multiple_independent(self):
         a = self.make_linear_commits(2, message=b'a')
         a = self.make_linear_commits(2, message=b'a')
@@ -250,7 +250,7 @@ class FindShallowTests(TestCase):
         heads = [a[1].id, b[1].id, c[1].id]
         heads = [a[1].id, b[1].id, c[1].id]
 
 
         self.assertEqual((set([a[0].id, b[0].id, c[0].id]), set(heads)),
         self.assertEqual((set([a[0].id, b[0].id, c[0].id]), set(heads)),
-                         _find_shallow(self._store, heads, 1))
+                         _find_shallow(self._store, heads, 2))
 
 
     def test_multiple_overlapping(self):
     def test_multiple_overlapping(self):
         # Create the following commit tree:
         # Create the following commit tree:
@@ -263,7 +263,7 @@ class FindShallowTests(TestCase):
 
 
         # 1 is shallow along the path from 4, but not along the path from 2.
         # 1 is shallow along the path from 4, but not along the path from 2.
         self.assertEqual((set([c1.id]), set([c1.id, c2.id, c3.id, c4.id])),
         self.assertEqual((set([c1.id]), set([c1.id, c2.id, c3.id, c4.id])),
-                         _find_shallow(self._store, [c2.id, c4.id], 2))
+                         _find_shallow(self._store, [c2.id, c4.id], 3))
 
 
     def test_merge(self):
     def test_merge(self):
         c1 = self.make_commit()
         c1 = self.make_commit()
@@ -271,7 +271,7 @@ class FindShallowTests(TestCase):
         c3 = self.make_commit(parents=[c1.id, c2.id])
         c3 = self.make_commit(parents=[c1.id, c2.id])
 
 
         self.assertEqual((set([c1.id, c2.id]), set([c3.id])),
         self.assertEqual((set([c1.id, c2.id]), set([c3.id])),
-                         _find_shallow(self._store, [c3.id], 1))
+                         _find_shallow(self._store, [c3.id], 2))
 
 
     def test_tag(self):
     def test_tag(self):
         c1, c2 = self.make_linear_commits(2)
         c1, c2 = self.make_linear_commits(2)
@@ -279,7 +279,7 @@ class FindShallowTests(TestCase):
         self._store.add_object(tag)
         self._store.add_object(tag)
 
 
         self.assertEqual((set([c1.id]), set([c2.id])),
         self.assertEqual((set([c1.id]), set([c2.id])),
-                         _find_shallow(self._store, [tag.id], 1))
+                         _find_shallow(self._store, [tag.id], 2))
 
 
 
 
 class TestUploadPackHandler(UploadPackHandler):
 class TestUploadPackHandler(UploadPackHandler):
@@ -479,7 +479,7 @@ class ProtocolGraphWalkerTestCase(TestCase):
           expected, list(iter(self._walker.proto.get_received_line, None)))
           expected, list(iter(self._walker.proto.get_received_line, None)))
 
 
     def test_handle_shallow_request_no_client_shallows(self):
     def test_handle_shallow_request_no_client_shallows(self):
-        self._handle_shallow_request([b'deepen 1\n'], [FOUR, FIVE])
+        self._handle_shallow_request([b'deepen 2\n'], [FOUR, FIVE])
         self.assertEqual(set([TWO, THREE]), self._walker.shallow)
         self.assertEqual(set([TWO, THREE]), self._walker.shallow)
         self.assertReceived([
         self.assertReceived([
           b'shallow ' + TWO,
           b'shallow ' + TWO,
@@ -490,7 +490,7 @@ class ProtocolGraphWalkerTestCase(TestCase):
         lines = [
         lines = [
           b'shallow ' + TWO + b'\n',
           b'shallow ' + TWO + b'\n',
           b'shallow ' + THREE + b'\n',
           b'shallow ' + THREE + b'\n',
-          b'deepen 1\n',
+          b'deepen 2\n',
           ]
           ]
         self._handle_shallow_request(lines, [FOUR, FIVE])
         self._handle_shallow_request(lines, [FOUR, FIVE])
         self.assertEqual(set([TWO, THREE]), self._walker.shallow)
         self.assertEqual(set([TWO, THREE]), self._walker.shallow)
@@ -499,7 +499,7 @@ class ProtocolGraphWalkerTestCase(TestCase):
     def test_handle_shallow_request_unshallows(self):
     def test_handle_shallow_request_unshallows(self):
         lines = [
         lines = [
           b'shallow ' + TWO + b'\n',
           b'shallow ' + TWO + b'\n',
-          b'deepen 2\n',
+          b'deepen 3\n',
           ]
           ]
         self._handle_shallow_request(lines, [FOUR, FIVE])
         self._handle_shallow_request(lines, [FOUR, FIVE])
         self.assertEqual(set([ONE]), self._walker.shallow)
         self.assertEqual(set([ONE]), self._walker.shallow)

+ 2 - 18
examples/clone.py

@@ -6,8 +6,7 @@
 
 
 import sys
 import sys
 from getopt import getopt
 from getopt import getopt
-from dulwich.repo import Repo
-from dulwich.client import get_transport_and_path
+from dulwich import porcelain
 
 
 opts, args = getopt(sys.argv, "", [])
 opts, args = getopt(sys.argv, "", [])
 opts = dict(opts)
 opts = dict(opts)
@@ -16,19 +15,4 @@ if len(args) < 2:
     print("usage: %s host:path path" % (args[0], ))
     print("usage: %s host:path path" % (args[0], ))
     sys.exit(1)
     sys.exit(1)
 
 
-# Connect to the remote repository
-client, host_path = get_transport_and_path(args[1])
-path = args[2]
-
-# Create the local repository
-r = Repo.init(path, mkdir=True)
-
-# Fetch the remote objects
-remote_refs = client.fetch(host_path, r,
-    determine_wants=r.object_store.determine_wants_all,
-    progress=sys.stdout.write)
-
-# Update the local head to point at the right object
-r["HEAD"] = remote_refs["HEAD"]
-
-r._build_tree()
+porcelain.clone(args[1], args[2])

+ 0 - 110
relicensing-apachev2.txt

@@ -1,110 +0,0 @@
-At the moment, Dulwich is licensed under the GNU General Public License,
-version 2 or later.
-
-We'd like to relicense Dulwich under the Apache v2 (or later) license, as
-the GPL is problematic for many free software Python projects that are under
-BSD-style licenses. See also https://github.com/jelmer/dulwich/issues/153
-
-For reference, a full copy of the Apachev2 license can be found here:
-https://www.apache.org/licenses/LICENSE-2.0
-
-New contributions to Dulwich should be dual licensed under the GNU GPLv2 (or
-later) and the Apachev2 (or later) licenses.
-
-Contributions made prior were contributed under the GPLv2 (or later) license
-alone. The following contributors have not (yet) relicensed their code under
-dual Apachev2/GPLv2:
-
-Aaron O'Mullan <aaron.omullan@friendco.de>
-Abderrahim Kitouni <a.kitouni@gmail.com>
-Adam "Cezar" Jenkins <emperorcezar@gmail.com>
-Alberto Ruiz <aruiz@gnome.org>
-Alexander Belchenko <bialix@ukr.net>
-Alex Holmes <alex@alex-holmes.com>
-Ali Sabil <ali.sabil@gmail.com>
-Andi McClure <andi.m.mcclure@gmail.com>
-André Roth <neolynx@gmail.com>
-Andres Lowrie <andres.lowrie@gmail.com>
-Artem Tikhomirov <artem.tikhomirov@syntevo.com>
-Augie Fackler <durin42@gmail.com> <raf@durin42.com>
-Benjamin Pollack <benjamin@bitquabit.com> <benjamin@fogcreek.com>
-Brendan Cully <brendan@kublai.com>
-Brian Visel <eode@eptitude.net>
-Bruce Duncan <Bruce.Duncan@ed.ac.uk>
-Bruno Renié <buburno@gmail.com>
-Chaiwat Suttipongsakul <cwt@bashell.com>
-Chow Loong Jin <hyperair@debian.org>
-Chris Eberle <eberle1080@gmail.com>
-Chris Reid <chris@reidsy.com>
-codingtony <tony.bussieres@gmail.com>
-dak180 <dak180@users.sourceforge.net>
-Damien Tournoud <damien@commerceguys.com>
-Dan Callaghan <dcallagh@redhat.com>
-Daniele Sluijters <daniele.sluijters@gmail.com>
-David Bennett <davbennett@google.com>
-David Blewett <davidb@sixfeetup.com>
-David Borowitz <dborowitz@google.com>
-David Carr <david@carrclan.us>
-David Keijser <david.keijser@klarna.com>
-David Ostrovsky <david@ostrovsky.org>
-David Pursehouse <david.pursehouse@gmail.com>
-DeeKey <dkomarov@gmail.com>
-Dirk <dirk@opani.com>
-diryboy <lancevdance@gmail.com>
-D-Key <dkomarov@gmail.com>
-Dmitrij D. Czarkoff <czarkoff@gmail.com>
-Dmitriy <dkomarov@gmail.com>
-Dov Feldstern <dovdevel@gmail.com>
-Fabien Boucher <fabien.boucher@enovance.com>
-Gary van der Merwé <garyvdm@gmail.com>
-Hal Wine <hal.wine@gmail.com>
-Hannu Valtonen <hannu.valtonen@ohmu.fi>
-Hans Kolek <hkolek@gmail.com>
-Hervé Cauwelier <herve@oursours.net> <herve@itaapy.com>
-Hwee Miin Koh <hwee-miin.koh@ubisoft.com>
-Jameson Nash <jameson@mit.edu>
-James Westby <jw+debian@jameswestby.net>
-Jason R. Coombs <jaraco@jaraco.com>
-John Arbash Meinel <john@arbash-meinel.com>
-John Carr <john.carr@unrouted.co.uk>
-Jonathan Chu <jchonphoenix@gmail.com>
-kwatters <kwatters@tagged.com>
-Lukasz Balcerzak <lukasz.balcerzak@python-center.org>
-Marc Brinkmann <git@marcbrinkmann.de>
-Marcin Kuźmiński <marcin@python-blog.com> <marcin@python-works.com>
-Martin Packman <gzlist@googlemail.com>
-Max Bowsher <maxb@f2s.com>
-max <max0d41@github.com>
-Max Shawabkeh <max99x@gmail.com>
-Michael K <michael-k@users.noreply.github.com>
-Mike Edgar <adgar@google.com>
-Mike Williams <miwilliams@google.com>
-Nick Stenning <nick@whiteink.com>
-Nick Ward <ward.nickjames@gmail.com>
-Nix <nix@esperi.co.uk>
-Pascal Quantin <pascal.quantin@gmail.com>
-Paul Chen <lancevdance@gmail.com>
-Paul Hummer <paul@eventuallyanyway.com>
-rfaulk <rfaulkner@wikimedia.org>
-Ricardo Salveti <ricardo.salveti@openbossa.org>
-Risto Kankkunen <risto.kankkunen@f-secure.com> <risto.kankkunen@iki.fi>
-Robert Brown <robert.brown@gmail.com>
-Rod Cloutier <rodcloutier@gmail.com>
-Roland Mas <lolando@debian.org>
-Ronald Blaschke <ron@rblasch.org>
-Ross Light <rlight2@gmail.com> <ross@zombiezen.com>
-Ryan Faulkner <rfaulk@yahoo-inc.com>
-Ryan McKern <ryan@orangefort.com>
-Sam Vilain <svilain@saymedia.com>
-Siddharth Agarwal <sid0@fb.com>
-Stefan Zimmermann <zimmermann.code@gmail.com>
-Takeshi Kanemoto <tak.kanemoto@gmail.com>
-Tay Ray Chuan <rctay89@gmail.com>
-Ted Horst <ted.horst@earthlink.net>
-Timo Schmid <info@bluec0re.eu>
-Tommy Yu <tommy.yu@auckland.ac.nz>
-Travis Cline <travis.cline@gmail.com>
-Víðir Valberg Guðmundsson <vidir.valberg@orn.li>
-William Grant <william.grant@canonical.com>
-Yifan Zhang <yifan@wavii.com>
-Yuval Langer <yuval.langer@gmail.com>

+ 6 - 0
setup.cfg

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

+ 9 - 8
setup.py

@@ -1,6 +1,6 @@
 #!/usr/bin/python
 #!/usr/bin/python
 # Setup file for dulwich
 # Setup file for dulwich
-# Copyright (C) 2008-2011 Jelmer Vernooij <jelmer@samba.org>
+# Copyright (C) 2008-2011 Jelmer Vernooij <jelmer@jelmer.uk>
 
 
 try:
 try:
     from setuptools import setup, Extension
     from setuptools import setup, Extension
@@ -8,7 +8,7 @@ except ImportError:
     from distutils.core import setup, Extension
     from distutils.core import setup, Extension
 from distutils.core import Distribution
 from distutils.core import Distribution
 
 
-dulwich_version_string = '0.10.2'
+dulwich_version_string = '0.11.1'
 
 
 include_dirs = []
 include_dirs = []
 # Windows MSVC support
 # Windows MSVC support
@@ -47,15 +47,16 @@ if sys.platform == 'darwin' and os.path.exists('/usr/bin/xcodebuild'):
             os.environ['ARCHFLAGS'] = ''
             os.environ['ARCHFLAGS'] = ''
 
 
 if sys.version_info[0] == 2:
 if sys.version_info[0] == 2:
-    tests_require = ['fastimport', 'mock']
+    tests_require = ['fastimport']
     if not '__pypy__' in sys.modules and not sys.platform == 'win32':
     if not '__pypy__' in sys.modules and not sys.platform == 'win32':
-        tests_require.extend(['gevent', 'geventhttpclient'])
+        tests_require.extend([
+            'gevent', 'geventhttpclient', 'mock', 'setuptools>=17.1'])
+    if sys.version_info < (2, 7):
+        tests_require.append('unittest2')
 else:
 else:
     # fastimport, gevent, geventhttpclient are not available for PY3
     # fastimport, gevent, geventhttpclient are not available for PY3
     # mock only used for test_swift, which requires gevent/geventhttpclient
     # mock only used for test_swift, which requires gevent/geventhttpclient
     tests_require = []
     tests_require = []
-if sys.version_info < (2, 7):
-    tests_require.append('unittest2')
 
 
 if sys.version_info[0] > 2 and sys.platform == 'win32':
 if sys.version_info[0] > 2 and sys.platform == 'win32':
     # C Modules don't build for python3 windows, and prevent tests from running
     # C Modules don't build for python3 windows, and prevent tests from running
@@ -75,10 +76,10 @@ setup(name='dulwich',
       description='Python Git Library',
       description='Python Git Library',
       keywords='git',
       keywords='git',
       version=dulwich_version_string,
       version=dulwich_version_string,
-      url='https://samba.org/~jelmer/dulwich',
+      url='https://www.dulwich.io/',
       license='GPLv2 or later',
       license='GPLv2 or later',
       author='Jelmer Vernooij',
       author='Jelmer Vernooij',
-      author_email='jelmer@samba.org',
+      author_email='jelmer@jelmer.uk',
       long_description="""
       long_description="""
       Python implementation of the Git file formats and protocols,
       Python implementation of the Git file formats and protocols,
       without the need to have git installed.
       without the need to have git installed.

+ 4 - 10
tox.ini

@@ -1,6 +1,6 @@
 [tox]
 [tox]
 downloadcache = {toxworkdir}/cache/
 downloadcache = {toxworkdir}/cache/
-envlist = py27, pypy, py27-noext, pypy-noext
+envlist = py26, py27, pypy, py27-noext, pypy-noext, py34, py34-noext
 
 
 [testenv]
 [testenv]
 deps =
 deps =
@@ -10,17 +10,11 @@ commands = make check
 recreate = True
 recreate = True
 whitelist_externals = make
 whitelist_externals = make
 
 
-[testenv:py24]
-setenv =
-    PIP_INSECURE=1
-
-
-[testenv:py25]
-setenv =
-    PIP_INSECURE=1
-
 [testenv:py27-noext]
 [testenv:py27-noext]
 commands = make check-noextensions
 commands = make check-noextensions
 
 
 [testenv:pypy-noext]
 [testenv:pypy-noext]
 commands = make check-noextensions
 commands = make check-noextensions
+
+[testenv:py34-noext]
+commands = make check-noextensions