Browse Source

Imported Upstream version 0.11.1

Jelmer Vernooij 9 years ago
parent
commit
7c78398c33
51 changed files with 1095 additions and 556 deletions
  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>
 John Carr <john.carr@unrouted.co.uk>
 Dave Borowitz <dborowitz@google.com>

+ 4 - 0
MANIFEST.in

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

+ 4 - 0
Makefile

@@ -15,6 +15,10 @@ DESTDIR=/
 all: build
 
 doc:: pydoctor
+doc:: sphinx
+
+sphinx::
+	$(MAKE) -C docs html
 
 pydoctor::
 	$(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
 
@@ -12,12 +16,28 @@
     capabilities. (Jelmer Vernooij)
   * Switched `default_local_git_client_cls` to `LocalGitClient`.
     (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
 
   * Fix handling of 'done' in graph walker and implement the
     '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
 
  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
 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
 in the particular Monty Python sketch.
@@ -31,11 +31,11 @@ Further documentation
 
 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:
 
-http://www.samba.org/~jelmer/dulwich/apidocs
+https://www.dulwich.io/apidocs
 
 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:
   matrix:
     - PYTHON: "C:\\Python27"
-      PYTHON_ARCH: "32"
       PYWIN32_URL: "https://downloads.sourceforge.net/project/pywin32/pywin32/Build%20219/pywin32-219.win32-py2.7.exe"
 
     - PYTHON: "C:\\Python34"
-      PYTHON_ARCH: "32"
       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:
   - 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 wheel"
 
 build: off
 
 test_script:
   - "%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])
 
 
+def cmd_repack(args):
+    opts, args = getopt(args, "", [])
+    opts = dict(opts)
+    porcelain.repack('.')
+
+
 def cmd_reset(args):
     opts, args = getopt(args, "", ["hard", "soft", "mixed"])
     opts = dict(opts)
@@ -342,6 +348,37 @@ def cmd_status(args):
         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 = {
     "add": cmd_add,
     "archive": cmd_archive,
@@ -357,7 +394,10 @@ commands = {
     "fetch": cmd_fetch,
     "init": cmd_init,
     "log": cmd_log,
+    "ls-remote": cmd_ls_remote,
+    "pack-objects": cmd_pack_objects,
     "receive-pack": cmd_receive_pack,
+    "repack": cmd_repack,
     "reset": cmd_reset,
     "rev-list": cmd_rev_list,
     "rm": cmd_rm,

+ 1 - 1
docs/index.txt

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

+ 0 - 2
docs/tutorial/.gitignore

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

+ 1 - 1
dulwich.cfg

@@ -1,5 +1,5 @@
 packages: dulwich
 docformat: restructuredtext
 projectname: Dulwich
-projecturl: http://samba.org/~jelmer/dulwich/
+projecturl: https://www.dulwich.io/
 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."""
 
-__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);
-		if (!PyInt_Check(py_mode)) {
+		if (!PyInt_Check(py_mode) && !PyLong_Check(py_mode)) {
 			PyErr_SetString(PyExc_TypeError, "Mode is not an integral type");
 			goto error;
 		}
@@ -210,7 +210,7 @@ static PyObject *py_sorted_tree_items(PyObject *self, PyObject *args)
 			goto error;
 		}
 		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(
 		                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
 import dulwich
 import select
+import shlex
 import socket
 import subprocess
 import sys
@@ -201,7 +202,7 @@ class GitClient(object):
                   progress=None, write_pack=write_pack_objects):
         """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
             the shas of the objects to upload.
         :param progress: Optional progress function
@@ -217,7 +218,7 @@ class GitClient(object):
     def fetch(self, path, target, determine_wants=None, progress=None):
         """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 determine_wants: Optional function to determine what refs
             to fetch
@@ -226,6 +227,7 @@ class GitClient(object):
         """
         if determine_wants is None:
             determine_wants = target.object_store.determine_wants_all
+
         f, commit, abort = target.object_store.add_pack()
         try:
             result = self.fetch_pack(
@@ -249,6 +251,13 @@ class GitClient(object):
         """
         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):
         unpack = proto.read_pkt_line().strip()
         if unpack != b'unpack ok':
@@ -445,7 +454,7 @@ class TraditionalGitClient(GitClient):
         reads would block.
 
         :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()
 
@@ -453,7 +462,7 @@ class TraditionalGitClient(GitClient):
                   progress=None, write_pack=write_pack_objects):
         """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
             the shas of the objects to upload.
         :param progress: Optional callback called with progress updates
@@ -553,6 +562,14 @@ class TraditionalGitClient(GitClient):
                 proto, negotiated_capabilities, graph_walker, pack_data, progress)
             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,
                 write_error=None):
         proto, can_read = self._connect(b'upload-archive', path)
@@ -588,6 +605,10 @@ class TCPGitClient(TraditionalGitClient):
         TraditionalGitClient.__init__(self, *args, **kwargs)
 
     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(
             self._host, self._port, socket.AF_UNSPEC, socket.SOCK_STREAM)
         s = None
@@ -617,7 +638,8 @@ class TCPGitClient(TraditionalGitClient):
                          report_activity=self._report_activity)
         if path.startswith(b"/~"):
             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)
 
 
@@ -679,11 +701,14 @@ class SubprocessGitClient(TraditionalGitClient):
     git_command = None
 
     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
         if self.git_command is None:
             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(
             subprocess.Popen(argv, bufsize=0, stdin=subprocess.PIPE,
                              stdout=subprocess.PIPE,
@@ -698,7 +723,6 @@ class LocalGitClient(GitClient):
     def __init__(self, thin_packs=True, report_activity=None):
         """Create a new LocalGitClient instance.
 
-        :param path: Path to the local repository
         :param thin_packs: Whether or not thin packs should be retrieved
         :param report_activity: Optional callback for reporting transport
             activity.
@@ -710,7 +734,7 @@ class LocalGitClient(GitClient):
                   progress=None, write_pack=write_pack_objects):
         """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
             the shas of the objects to upload.
         :param progress: Optional progress function
@@ -749,7 +773,7 @@ class LocalGitClient(GitClient):
     def fetch(self, path, target, determine_wants=None, progress=None):
         """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 determine_wants: Optional function to determine what refs
             to fetch
@@ -780,6 +804,13 @@ class LocalGitClient(GitClient):
                 return
             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
 default_local_git_client_cls = LocalGitClient
@@ -802,7 +833,7 @@ class SSHVendor(object):
         with the remote command.
 
         :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 port: Optional SSH port to use
         """
@@ -813,6 +844,10 @@ class SubprocessSSHVendor(SSHVendor):
     """SSH vendor that shells out to the local 'ssh' command."""
 
     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
         #FIXME: This has no way to deal with passwords..
         args = ['ssh', '-x']
@@ -915,7 +950,9 @@ else:
 
         def run_command(self, host, command, username=None, port=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
             if port is None:
                 port = 22
@@ -931,7 +968,7 @@ else:
             channel = client.get_transport().open_session()
 
             # Run commands
-            channel.exec_command(*command)
+            channel.exec_command(subprocess.list2cmdline(command))
 
             return ParamikoWrapper(
                 client, channel, progress_stderr=progress_stderr)
@@ -951,15 +988,24 @@ class SSHGitClient(TraditionalGitClient):
         self.alternative_paths = {}
 
     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):
-        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:]
+        argv = self._get_cmd_path(cmd) + [path]
         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,
                          report_activity=self._report_activity),
                 con.can_read)
@@ -1054,7 +1100,7 @@ class HttpGitClient(GitClient):
                   progress=None, write_pack=write_pack_objects):
         """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
             the shas of the objects to upload.
         :param progress: Optional progress function
@@ -1134,11 +1180,18 @@ class HttpGitClient(GitClient):
         finally:
             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):
     """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 thin_packs: Whether or not thin packs should be retrieved
     :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):
     """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 thin_packs: Whether or not thin packs should be retrieved
     :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)
 
 
+_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):
     value = bytearray(value.strip())
     ret = bytearray()
-    block = bytearray()
+    whitespace = bytearray()
     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)
-            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
             break
+        elif c in _WHITESPACE_CHARS:
+            whitespace.append(c)
         else:
-            block.append(c)
+            if whitespace:
+                ret.extend(whitespace)
+                whitespace = bytearray()
+            ret.append(c)
+        i += 1
 
     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)
 
 
 def _unescape_value(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()
     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
 
 
@@ -258,6 +273,7 @@ class ConfigFile(ConfigDict):
         for lineno, line in enumerate(f.readlines()):
             line = line.lstrip()
             if setting is None:
+                # Parse section header ("[bla]")
                 if len(line) > 0 and line[:1] == b"[":
                     line = _strip_comments(line).rstrip()
                     last = line.index(b"]")

+ 7 - 5
dulwich/file.py

@@ -19,9 +19,10 @@
 """Safe access to git files."""
 
 import errno
+import io
 import os
+import sys
 import tempfile
-import io
 
 def ensure_dir_exists(dirname):
     """Ensure a directory exists, creating if necessary."""
@@ -32,7 +33,7 @@ def ensure_dir_exists(dirname):
             raise
 
 
-def fancy_rename(oldname, newname):
+def _fancy_rename(oldname, newname):
     """Rename file with temporary backup file to rollback if rename fails"""
     if not os.path.exists(newname):
         try:
@@ -148,10 +149,11 @@ class _GitFile(object):
             try:
                 os.rename(self._lockfilename, self._filename)
             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
-                fancy_rename(self._lockfilename, self._filename)
         finally:
             self.abort()
 

+ 15 - 1
dulwich/objects.py

@@ -25,7 +25,6 @@ from collections import namedtuple
 import os
 import posixpath
 import stat
-import sys
 import warnings
 import zlib
 from hashlib import sha1
@@ -520,6 +519,21 @@ class ShaFile(object):
         """
         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):
     """A Git Blob object."""

+ 94 - 4
dulwich/objectspec.py

@@ -19,6 +19,12 @@
 """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):
     """Parse a string referring to an object.
 
@@ -27,11 +33,96 @@ def parse_object(repo, objectish):
     :return: A git object
     :raise KeyError: If the object can not be found
     """
-    if getattr(objectish, "encode", None) is not None:
-        objectish = objectish.encode('ascii')
+    objectish = to_bytes(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):
     """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 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]])

+ 54 - 35
dulwich/pack.py

@@ -1061,33 +1061,48 @@ class PackData(object):
 
         :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):
         self._file.seek(self._header_size)
@@ -1322,15 +1337,16 @@ class DeltaChainIterator(object):
     def _follow_chain(self, offset, obj_type_num, base_chunks):
         # Unlike PackData.get_object_at, there is no need to cache offsets as
         # 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):
         return self._walk_all_chains()
@@ -1778,6 +1794,9 @@ def write_pack_index_v2(f, entries, pack_checksum):
     return f.write_sha()
 
 
+write_pack_index = write_pack_index_v2
+
+
 class Pack(object):
     """A Git pack object."""
 

+ 128 - 44
dulwich/porcelain.py

@@ -29,6 +29,7 @@ Currently implemented:
  * diff-tree
  * fetch
  * init
+ * ls-remote
  * pull
  * push
  * rm
@@ -66,10 +67,18 @@ from dulwich.errors import (
     )
 from dulwich.index import get_unstaged_changes
 from dulwich.objects import (
+    Commit,
     Tag,
     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.protocol import Protocol
 from dulwich.repo import (BaseRepo, Repo)
@@ -86,6 +95,14 @@ from dulwich.server import (
 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):
     """Open an argument that can be a repository or a path for a repository."""
     if isinstance(path_or_repo, BaseRepo):
@@ -122,6 +139,8 @@ def archive(path, committish=None, outstream=sys.stdout,
     client = SubprocessGitClient()
     if committish is None:
         committish = "HEAD"
+    if not isinstance(path, bytes):
+        path = path.encode(sys.getfilesystemencoding())
     # TODO(jelmer): This invokes C git; this introduces a dependency.
     # Instead, dulwich should have its own archiver implementation.
     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)
         r[b"HEAD"] = remote_refs[b"HEAD"]
         if checkout:
-            errstream.write(b'Checking out HEAD')
+            errstream.write(b'Checking out HEAD\n')
             r.reset_index()
     except:
         r.close()
@@ -276,93 +295,99 @@ def rm(repo=".", paths=None):
         index.write()
 
 
-def commit_decode(commit, contents):
+def commit_decode(commit, contents, default_encoding='utf-8'):
     if commit.encoding is not None:
         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.
 
     :param commit: A `Commit` object
     :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:
-        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.
 
     :param tag: A `Tag` object
+    :param decode: Function for decoding bytes to unicode string
     :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.
 
     :param repo: A `Repo` object
     :param blob: A `Blob` object
+    :param decode: Function for decoding bytes to unicode string
     :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.
 
     :param repo: A `Repo` object
     :param commit: A `Commit` object
+    :param decode: Function for decoding bytes to unicode string
     :param outstream: Stream to write to
     """
-    print_commit(commit, outstream)
+    print_commit(commit, decode=decode, outstream=outstream)
     parent_commit = repo[commit.parents[0]]
     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.
 
     :param repo: A `Repo` object
     :param tree: A `Tree` object
+    :param decode: Function for decoding bytes to unicode string
     :param outstream: Stream to write to
     """
     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.
 
     :param repo: A `Repo` object
     :param tag: A `Tag` object
+    :param decode: Function for decoding bytes to unicode string
     :param outstream: Stream to write to
     """
-    print_tag(tag, outstream)
+    print_tag(tag, decode, outstream)
     show_object(repo, repo[tag.object[1]], outstream)
 
 
-def show_object(repo, obj, outstream):
+def show_object(repo, obj, decode, outstream):
     return {
         b"tree": show_tree,
         b"blob": show_blob,
         b"commit": show_commit,
         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):
@@ -375,15 +400,18 @@ def log(repo=".", outstream=sys.stdout, max_entries=None):
     with open_repo_closing(repo) as r:
         walker = r.get_walker(max_entries=max_entries)
         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.
 
     :param repo: Path to repository
     :param objects: Objects to show (defaults to [HEAD])
     :param outstream: Stream to write to
+    :param default_encoding: Default encoding to use if none is set in the commit
     """
     if objects is None:
         objects = ["HEAD"]
@@ -391,7 +419,12 @@ def show(repo=".", objects=None, outstream=sys.stdout):
         objects = [objects]
     with open_repo_closing(repo) as r:
         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):
@@ -518,13 +551,13 @@ def reset(repo, mode, committish="HEAD"):
         r.reset_index()
 
 
-def push(repo, remote_location, refs_path,
+def push(repo, remote_location, refspecs=None,
          outstream=sys.stdout, errstream=sys.stderr):
     """Remote push with dulwich via dulwich.client
 
     :param repo: Path to repository
     :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 errstream: A stream file to write errors
     """
@@ -535,10 +568,16 @@ def push(repo, remote_location, refs_path,
         # Get the client and path
         client, path = get_transport_and_path(remote_location)
 
+        selected_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
 
         err_encoding = getattr(errstream, 'encoding', 'utf-8')
@@ -546,27 +585,37 @@ def push(repo, remote_location, refs_path,
         try:
             client.send_pack(path, update_refs,
                 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:
-            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):
     """Pull from remote via dulwich.client
 
     :param repo: Path to repository
     :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 errstream: A stream file to write to errors
     """
-
     # Open the repo
     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)
-        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
         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)
         remote_refs = client.fetch(path, r, progress=errstream.write)
     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 os
+import sys
 
 from dulwich.errors import (
     PackedRefsException,
     RefFormatError,
     )
 from dulwich.objects import (
-    hex_to_sha,
     git_line,
     valid_hexsha,
     )
@@ -398,7 +398,7 @@ class DiskRefsContainer(RefsContainer):
             dir = root[len(path):].strip(os.path.sep).replace(os.path.sep, "/")
             for filename in files:
                 refname = (("%s/%s" % (dir, filename))
-                           .strip("/").encode('ascii'))
+                           .strip("/").encode(sys.getfilesystemencoding()))
                 # check_ref_format requires at least one /, so we prepend the
                 # base before calling it.
                 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')):
             dir = root[len(path):].strip(os.path.sep).replace(os.path.sep, "/")
             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):
                     allkeys.add(refname)
         allkeys.update(self.get_packed_refs())
@@ -426,7 +426,8 @@ class DiskRefsContainer(RefsContainer):
         """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 != "/":
             name = name.replace("/", os.path.sep)
         return os.path.join(self.path, name)

+ 22 - 2
dulwich/repo.py

@@ -227,8 +227,8 @@ class BaseRepo(object):
         if determine_wants is None:
             determine_wants = target.object_store.determine_wants_all
         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()
 
     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['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):
         """Return the path of the control directory."""
         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 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
         considered shallow and unshallow according to the arguments. Note that
         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:
         obj = store.peel_sha(head_sha)
         if isinstance(obj, Commit):
-            todo.append((obj.id, 0))
+            todo.append((obj.id, 1))
 
     not_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',
                         self.url(port), 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.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):
         require_git_version(self.min_single_branch_version)
         self._source_repo = self.import_repo('server_new.export')
@@ -183,14 +204,14 @@ class ServerTests(object):
         self.addCleanup(tear_down_repo, self._stub_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])
         clone = self._stub_repo = Repo(self._stub_repo.path)
 
         # Fetching at the same depth is a no-op.
         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)
         expected_shallow = [b'94de09a530df27ac3bb613aaecdd539e0a0655e1',
                             b'da5cd81e1883c62a25bb37c4d1f8ad965b29bf8d']
@@ -204,19 +225,19 @@ class ServerTests(object):
         self.addCleanup(tear_down_repo, self._stub_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])
         clone = self._stub_repo = Repo(self._stub_repo.path)
 
         # Fetching at the same depth is a no-op.
         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)
 
-        # 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(
-          ['fetch', '--depth=3', self.url(port)] + self.branch_args(),
+          ['fetch', '--depth=4', self.url(port)] + self.branch_args(),
           cwd=self._stub_repo.path)
         self.assertEqual([], _get_shallow(clone))
         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 (
     get_safe_env,
     SkipTest,
+    expectedFailure,
     )
 from dulwich.tests.utils import (
     skipIfPY3,
@@ -105,7 +106,7 @@ class DulwichClientTestBase(object):
         with closing(repo.Repo(srcpath)) as src:
             sendrefs = dict(src.get_refs())
             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)
 
     def test_send_pack(self):
@@ -125,7 +126,7 @@ class DulwichClientTestBase(object):
         with closing(repo.Repo(srcpath)) as src:
             sendrefs = dict(src.get_refs())
             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)
             self.assertDestEqualsSrc()
 
@@ -163,7 +164,7 @@ class DulwichClientTestBase(object):
             sendrefs, gen_pack = self.compute_send(src)
             c = self._client()
             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:
                 self.assertEqual('refs/heads/master failed to update',
                                  e.args[0])
@@ -181,7 +182,7 @@ class DulwichClientTestBase(object):
             sendrefs, gen_pack = self.compute_send(src)
             c = self._client()
             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:
                 self.assertIn(str(e),
                               ['{0}, {1} failed to update'.format(
@@ -195,7 +196,7 @@ class DulwichClientTestBase(object):
     def test_archive(self):
         c = self._client()
         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)
         tf = tarfile.open(fileobj=f)
         self.assertEqual(['baz', 'foo'], tf.getnames())
@@ -203,7 +204,7 @@ class DulwichClientTestBase(object):
     def test_fetch_pack(self):
         c = self._client()
         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():
                 dest.refs.set_if_equals(r[0], None, r[1])
             self.assertDestEqualsSrc()
@@ -215,7 +216,7 @@ class DulwichClientTestBase(object):
         c = self._client()
         repo_dir = os.path.join(self.gitroot, 'server_new.export')
         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():
                 dest.refs.set_if_equals(r[0], None, r[1])
             self.assertDestEqualsSrc()
@@ -224,7 +225,7 @@ class DulwichClientTestBase(object):
         c = self._client()
         c._fetch_capabilities.remove(b'side-band-64k')
         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():
                 dest.refs.set_if_equals(r[0], None, r[1])
             self.assertDestEqualsSrc()
@@ -234,7 +235,7 @@ class DulwichClientTestBase(object):
         # be ignored
         c = self._client()
         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])
             for r in refs.items():
                 dest.refs.set_if_equals(r[0], None, r[1])
@@ -250,9 +251,17 @@ class DulwichClientTestBase(object):
             gen_pack = lambda have, want: []
             c = self._client()
             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)
 
+    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):
 
@@ -300,17 +309,23 @@ class DulwichTCPClientTest(CompatTestCase, DulwichClientTestBase):
         CompatTestCase.tearDown(self)
 
     def _client(self):
-        return client.TCPGitClient(b'localhost')
+        return client.TCPGitClient('localhost')
 
     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):
+
     @staticmethod
     def run_command(host, command, username=None, port=None):
         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,
                              stdout=subprocess.PIPE, stderr=subprocess.PIPE)
         return client.SubprocessWrapper(p)
@@ -330,10 +345,10 @@ class DulwichMockSSHClientTest(CompatTestCase, DulwichClientTestBase):
         client.get_ssh_vendor = self.real_vendor
 
     def _client(self):
-        return client.SSHGitClient(b'localhost')
+        return client.SSHGitClient('localhost')
 
     def _build_path(self, path):
-        return self.gitroot + path
+        return self.gitroot.encode(sys.getfilesystemencoding()) + path
 
 
 class DulwichSubprocessClientTest(CompatTestCase, DulwichClientTestBase):
@@ -350,7 +365,7 @@ class DulwichSubprocessClientTest(CompatTestCase, DulwichClientTestBase):
         return client.SubprocessGitClient(stderr=subprocess.PIPE)
 
     def _build_path(self, path):
-        return self.gitroot + path
+        return self.gitroot.encode(sys.getfilesystemencoding()) + path
 
 
 class GitHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):

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

@@ -187,6 +187,11 @@ class DumbWebTestCase(WebTests, CompatTestCase):
         # clones.
         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):
         # Note: remove this if C git and dulwich implement dumb web shallow
         # clones.

+ 6 - 3
dulwich/tests/test_blackbox.py

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

+ 26 - 8
dulwich/tests/test_client.py

@@ -498,6 +498,10 @@ class TestSSHVendor(object):
         self.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.command = command
         self.username = username
@@ -527,13 +531,19 @@ class SSHGitClientTests(TestCase):
         client.get_ssh_vendor = self.real_vendor
 
     def test_default_command(self):
-        self.assertEqual('git-upload-pack',
+        self.assertEqual([b'git-upload-pack'],
                 self.client._get_cmd_path(b'upload-pack'))
 
     def test_alternative_command_path(self):
-        self.client.alternative_paths['upload-pack'] = (
-            '/usr/lib/git/git-upload-pack')
-        self.assertEqual('/usr/lib/git/git-upload-pack',
+        self.client.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'))
 
     def test_connect(self):
@@ -543,13 +553,13 @@ class SSHGitClientTests(TestCase):
         client.username = b"username"
         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(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)
 
 
@@ -627,6 +637,14 @@ class LocalGitClientTests(TestCase):
         with closing(Repo.init_bare(target_path)) as 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):
         client = LocalGitClient()
         ref_name = b"refs/heads/" + branch

+ 24 - 17
dulwich/tests/test_config.py

@@ -30,7 +30,9 @@ from dulwich.config import (
     _parse_string,
     _unescape_value,
     )
-from dulwich.tests import TestCase
+from dulwich.tests import (
+    TestCase,
+    )
 
 
 class ConfigFileTests(TestCase):
@@ -150,6 +152,15 @@ class ConfigFileTests(TestCase):
         cf = self.from_file(b"[branch.foo] foo = bar\n")
         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):
 
@@ -205,28 +216,12 @@ class ConfigDictTests(TestCase):
             list(cd.itersections()))
 
 
-
 class StackedConfigTests(TestCase):
 
     def test_default_backends(self):
         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):
 
     def test_nothing(self):
@@ -260,6 +255,18 @@ class ParseStringTests(TestCase):
         self.assertEqual(b'foo', _parse_string(b"foo"))
         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):
 

+ 4 - 4
dulwich/tests/test_file.py

@@ -23,7 +23,7 @@ import shutil
 import sys
 import tempfile
 
-from dulwich.file import GitFile, fancy_rename
+from dulwich.file import GitFile, _fancy_rename
 from dulwich.tests import (
     SkipTest,
     TestCase,
@@ -53,7 +53,7 @@ class FancyRenameTests(TestCase):
 
     def test_no_dest_exists(self):
         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))
 
         new_f = open(self.bar, 'rb')
@@ -62,7 +62,7 @@ class FancyRenameTests(TestCase):
 
     def test_dest_exists(self):
         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))
 
         new_f = open(self.bar, 'rb')
@@ -74,7 +74,7 @@ class FancyRenameTests(TestCase):
             raise SkipTest("platform allows overwriting open files")
         self.create(self.bar, b'bar contents')
         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()
         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):
         self.assertRaises(NotTreeError, tree_lookup_path, self.get_object, self.tree_id, b'ad/b/j')
 
+
 class ObjectStoreGraphWalkerTests(TestCase):
 
     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 (
     parse_object,
     parse_commit_range,
+    parse_ref,
+    parse_refs,
+    parse_reftuple,
+    parse_reftuples,
     )
 from dulwich.repo import MemoryRepo
 from dulwich.tests import (
@@ -64,3 +68,72 @@ class ParseCommitRangeTests(TestCase):
         c1, c2, c3 = build_commit_graph(r.object_store, [[1], [2, 1],
             [3, 1, 2]])
         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, (0, b'bob')),
         ])
-        self.assertEntriesMatch([0, 2, 1, 3, 4], entries,
+        self.assertEntriesMatch([0, 2, 4, 1, 3], entries,
                                 self.make_pack_iter(f))
 
     def test_long_chain(self):

+ 37 - 15
dulwich/tests/test_porcelain.py

@@ -20,6 +20,10 @@
 
 from contextlib import closing
 from io import BytesIO
+try:
+    from StringIO import StringIO
+except ImportError:
+    from io import StringIO
 import os
 import shutil
 import tarfile
@@ -232,17 +236,17 @@ class LogTests(PorcelainTestCase):
         c1, c2, c3 = build_commit_graph(self.repo.object_store, [[1], [2, 1],
             [3, 1, 2]])
         self.repo.refs[b"HEAD"] = c3.id
-        outstream = BytesIO()
+        outstream = StringIO()
         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):
         c1, c2, c3 = build_commit_graph(self.repo.object_store, [[1], [2, 1],
             [3, 1, 2]])
         self.repo.refs[b"HEAD"] = c3.id
-        outstream = BytesIO()
+        outstream = StringIO()
         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):
@@ -251,24 +255,24 @@ class ShowTests(PorcelainTestCase):
         c1, c2, c3 = build_commit_graph(self.repo.object_store, [[1], [2, 1],
             [3, 1, 2]])
         self.repo.refs[b"HEAD"] = c3.id
-        outstream = BytesIO()
+        outstream = StringIO()
         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):
         c1, c2, c3 = build_commit_graph(self.repo.object_store, [[1], [2, 1],
             [3, 1, 2]])
         self.repo.refs[b"HEAD"] = c3.id
-        outstream = BytesIO()
+        outstream = StringIO()
         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):
         b = Blob.from_string(b"The Foo\n")
         self.repo.object_store.add_object(b)
-        outstream = BytesIO()
+        outstream = StringIO()
         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):
@@ -453,7 +457,8 @@ class PushTests(PorcelainTestCase):
         # Setup target repo cloned from temp test repo
         clone_path = tempfile.mkdtemp()
         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()
 
         # 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
         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
-        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
         with closing(Repo(clone_path)) as r_clone:
@@ -479,7 +484,8 @@ class PushTests(PorcelainTestCase):
             # this will be in the foo branch.
             change = list(tree_changes(self.repo, self.repo[b'HEAD'].tree,
                                        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):
@@ -511,6 +517,9 @@ class PullTests(PorcelainTestCase):
         porcelain.commit(repo=self.repo.path, message=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
         porcelain.pull(target_path, self.repo.path, b'refs/heads/master',
             outstream=outstream, errstream=errstream)
@@ -731,3 +740,16 @@ class FetchTests(PorcelainTestCase):
         # Check the target repo for pushed changes
         with closing(Repo(target_path)) as 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
+# encoding: utf-8
 # Copyright (C) 2013 Jelmer Vernooij <jelmer@samba.org>
 #
 # This program is free software; you can redistribute it and/or
@@ -21,6 +22,7 @@
 
 from io import BytesIO
 import os
+import sys
 import tempfile
 
 from dulwich import errors
@@ -39,6 +41,7 @@ from dulwich.refs import (
 from dulwich.repo import Repo
 
 from dulwich.tests import (
+    SkipTest,
     TestCase,
     )
 
@@ -435,6 +438,20 @@ class DiskRefsContainerTests(RefsContainerTests, TestCase):
                          self._refs.read_ref(b'refs/heads/packed'))
         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 = (
     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.config import Config
+from dulwich.errors import NotGitRepository
 from dulwich.repo import (
     Repo,
     MemoryRepo,
@@ -586,6 +587,17 @@ class BuildRepoRootTests(TestCase):
         tree = r[r[commit_sha].tree]
         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):
         r = self._repo
         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)
             self.assertEqual(stat.S_IFREG | 0o644, mode)
             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)
 
         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))
-        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))
-        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))
+        self.assertEqual((set([]), set([c1.id, c2.id, c3.id])),
+                         _find_shallow(self._store, [c3.id], 4))
 
     def test_multiple_independent(self):
         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]
 
         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):
         # 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.
         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):
         c1 = self.make_commit()
@@ -271,7 +271,7 @@ class FindShallowTests(TestCase):
         c3 = self.make_commit(parents=[c1.id, c2.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):
         c1, c2 = self.make_linear_commits(2)
@@ -279,7 +279,7 @@ class FindShallowTests(TestCase):
         self._store.add_object(tag)
 
         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):
@@ -479,7 +479,7 @@ class ProtocolGraphWalkerTestCase(TestCase):
           expected, list(iter(self._walker.proto.get_received_line, None)))
 
     def test_handle_shallow_request_no_client_shallows(self):
-        self._handle_shallow_request([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.assertReceived([
           b'shallow ' + TWO,
@@ -490,7 +490,7 @@ class ProtocolGraphWalkerTestCase(TestCase):
         lines = [
           b'shallow ' + TWO + b'\n',
           b'shallow ' + THREE + b'\n',
-          b'deepen 1\n',
+          b'deepen 2\n',
           ]
         self._handle_shallow_request(lines, [FOUR, FIVE])
         self.assertEqual(set([TWO, THREE]), self._walker.shallow)
@@ -499,7 +499,7 @@ class ProtocolGraphWalkerTestCase(TestCase):
     def test_handle_shallow_request_unshallows(self):
         lines = [
           b'shallow ' + TWO + b'\n',
-          b'deepen 2\n',
+          b'deepen 3\n',
           ]
         self._handle_shallow_request(lines, [FOUR, FIVE])
         self.assertEqual(set([ONE]), self._walker.shallow)

+ 2 - 18
examples/clone.py

@@ -6,8 +6,7 @@
 
 import sys
 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 = dict(opts)
@@ -16,19 +15,4 @@ if len(args) < 2:
     print("usage: %s host:path path" % (args[0], ))
     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]
+
+[egg_info]
+tag_build = 
+tag_date = 0
+tag_svn_revision = 0
+

+ 9 - 8
setup.py

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

+ 4 - 10
tox.ini

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