瀏覽代碼

Imported Upstream version 0.12.0

Jelmer Vernooij 9 年之前
父節點
當前提交
64a442c5b9

+ 4 - 0
.testr.conf

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

+ 2 - 0
CONTRIBUTING

@@ -26,6 +26,8 @@ unittest2 (which you will need to have installed) on older versions of Python.
 
 
  $ make check
  $ make check
 
 
+Tox configuration is also present as well as a Travis configuration file.
+
 String Types
 String Types
 ------------
 ------------
 Like Linux, Git treats filenames as arbitrary bytestrings. There is no prescribed
 Like Linux, Git treats filenames as arbitrary bytestrings. There is no prescribed

+ 2 - 0
MANIFEST.in

@@ -5,6 +5,7 @@ include Makefile
 include COPYING
 include COPYING
 include HACKING
 include HACKING
 include CONTRIBUTING
 include CONTRIBUTING
+include TODO
 include setup.cfg
 include setup.cfg
 include dulwich/stdint.h
 include dulwich/stdint.h
 recursive-include docs conf.py *.txt Makefile make.bat
 recursive-include docs conf.py *.txt Makefile make.bat
@@ -13,3 +14,4 @@ graft dulwich/tests/data
 include tox.ini
 include tox.ini
 include dulwich.cfg
 include dulwich.cfg
 include appveyor.yml
 include appveyor.yml
+include .testr.conf

+ 30 - 0
NEWS

@@ -1,3 +1,33 @@
+0.12.0	2015-12-13
+
+ IMPROVEMENTS
+
+  * Add a `dulwich.archive` module that can create tarballs.
+    Based on code from Jonas Haag in klaus.
+
+  * Add a `dulwich.reflog` module for reading and writing reflogs.
+    (Jelmer Vernooij)
+
+  * Fix handling of ambiguous refs in `parse_ref` to make
+    it match the behaviour described in https://git-scm.com/docs/gitrevisions.
+    (Chris Bunney)
+
+  * Support Python3 in C modules. (Lele Gaifax)
+
+ BUG FIXES
+
+  * Simplify handling of SSH command invocation.
+    Fixes quoting of paths. Thanks, Thomas Liebetraut. (#384)
+
+  * Fix inconsistent handling of trailing slashes for DictRefsContainer. (#383)
+
+  * Add hack to support thin packs duing fetch(), albeit while requiring the
+    entire pack file to be loaded into memory. (jsbain)
+
+ CHANGES
+
+  * This will be the last release to support Python 2.6.
+
 0.11.2	2015-09-18
 0.11.2	2015-09-18
 
 
  IMPROVEMENTS
  IMPROVEMENTS

+ 1 - 1
PKG-INFO

@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Metadata-Version: 1.1
 Name: dulwich
 Name: dulwich
-Version: 0.11.2
+Version: 0.12.0
 Summary: Python Git Library
 Summary: Python Git Library
 Home-page: https://www.dulwich.io/
 Home-page: https://www.dulwich.io/
 Author: Jelmer Vernooij
 Author: Jelmer Vernooij

+ 2 - 0
TODO

@@ -0,0 +1,2 @@
+- 'git annotate' equivalent
+- repacking

+ 1 - 0
docs/tutorial/index.txt

@@ -13,5 +13,6 @@ Tutorial
    object-store
    object-store
    remote
    remote
    tag
    tag
+   porcelain
    conclusion
    conclusion
 
 

+ 8 - 4
docs/tutorial/introduction.txt

@@ -10,7 +10,11 @@ The plumbing is the lower layer and it deals with the Git object database and th
 nitty gritty internals. The porcelain is roughly what you would expect to
 nitty gritty internals. The porcelain is roughly what you would expect to
 be exposed to as a user of the ``git`` command-like tool.
 be exposed to as a user of the ``git`` command-like tool.
 
 
-Dulwich has a fairly complete plumbing implementation, and only a somewhat
-smaller porcelain implementation. The porcelain code lives in
-``dulwich.porcelain``. For the large part, this tutorial introduces you to the
-internal concepts of Git and the main plumbing parts of Dulwich.
+Dulwich has a fairly complete plumbing implementation, and a more recently
+added porcelain implementation. The porcelain code lives in
+``dulwich.porcelain``.
+
+
+For the large part, this tutorial introduces you to the internal concepts of
+Git and the main plumbing parts of Dulwich. The last chapter covers
+the porcelain.

+ 34 - 0
docs/tutorial/porcelain.txt

@@ -0,0 +1,34 @@
+Porcelain
+=========
+
+The ``porcelain'' is the higher level interface, built on top of the lower
+level implementation covered in previous chapters of this tutorial. The
+``dulwich.porcelain'' module in Dulwich is aimed to closely resemble
+the Git command-line API that you are familiar with.
+
+Basic concepts
+--------------
+The porcelain operations are implemented as top-level functions in the
+``dulwich.porcelain'' module. Most arguments can either be strings or
+more complex Dulwich objects; e.g. a repository argument will either take
+a string with a path to the repository or an instance of a ``Repo`` object.
+
+Initializing a new repository
+-----------------------------
+
+  >>> from dulwich import porcelain
+
+  >>> repo = porcelain.init("myrepo")
+
+Clone a repository
+------------------
+
+  >>> porcelain.clone("git://github.com/jelmer/dulwich", "dulwich-clone")
+
+Commit changes
+--------------
+
+  >>> r = porcelain.init("testrepo")
+  >>> open("testrepo/testfile", "w").write("data")
+  >>> porcelain.add(r, "testrepo/testfile")
+  >>> porcelain.commit(r, "A sample commit")

+ 1 - 1
dulwich.egg-info/PKG-INFO

@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Metadata-Version: 1.1
 Name: dulwich
 Name: dulwich
-Version: 0.11.2
+Version: 0.12.0
 Summary: Python Git Library
 Summary: Python Git Library
 Home-page: https://www.dulwich.io/
 Home-page: https://www.dulwich.io/
 Author: Jelmer Vernooij
 Author: Jelmer Vernooij

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

@@ -1,3 +1,4 @@
+.testr.conf
 AUTHORS
 AUTHORS
 CONTRIBUTING
 CONTRIBUTING
 COPYING
 COPYING
@@ -5,6 +6,7 @@ MANIFEST.in
 Makefile
 Makefile
 NEWS
 NEWS
 README.md
 README.md
+TODO
 appveyor.yml
 appveyor.yml
 dulwich.cfg
 dulwich.cfg
 setup.cfg
 setup.cfg
@@ -25,6 +27,7 @@ docs/tutorial/file-format.txt
 docs/tutorial/index.txt
 docs/tutorial/index.txt
 docs/tutorial/introduction.txt
 docs/tutorial/introduction.txt
 docs/tutorial/object-store.txt
 docs/tutorial/object-store.txt
+docs/tutorial/porcelain.txt
 docs/tutorial/remote.txt
 docs/tutorial/remote.txt
 docs/tutorial/repo.txt
 docs/tutorial/repo.txt
 docs/tutorial/tag.txt
 docs/tutorial/tag.txt
@@ -33,6 +36,7 @@ dulwich/_compat.py
 dulwich/_diff_tree.c
 dulwich/_diff_tree.c
 dulwich/_objects.c
 dulwich/_objects.c
 dulwich/_pack.c
 dulwich/_pack.c
+dulwich/archive.py
 dulwich/client.py
 dulwich/client.py
 dulwich/config.py
 dulwich/config.py
 dulwich/diff_tree.py
 dulwich/diff_tree.py
@@ -51,6 +55,7 @@ dulwich/pack.py
 dulwich/patch.py
 dulwich/patch.py
 dulwich/porcelain.py
 dulwich/porcelain.py
 dulwich/protocol.py
 dulwich/protocol.py
+dulwich/reflog.py
 dulwich/refs.py
 dulwich/refs.py
 dulwich/repo.py
 dulwich/repo.py
 dulwich/server.py
 dulwich/server.py
@@ -60,7 +65,6 @@ dulwich/web.py
 dulwich.egg-info/PKG-INFO
 dulwich.egg-info/PKG-INFO
 dulwich.egg-info/SOURCES.txt
 dulwich.egg-info/SOURCES.txt
 dulwich.egg-info/dependency_links.txt
 dulwich.egg-info/dependency_links.txt
-dulwich.egg-info/pbr.json
 dulwich.egg-info/top_level.txt
 dulwich.egg-info/top_level.txt
 dulwich/contrib/__init__.py
 dulwich/contrib/__init__.py
 dulwich/contrib/paramiko_vendor.py
 dulwich/contrib/paramiko_vendor.py
@@ -68,6 +72,7 @@ dulwich/contrib/swift.py
 dulwich/contrib/test_swift.py
 dulwich/contrib/test_swift.py
 dulwich/contrib/test_swift_smoke.py
 dulwich/contrib/test_swift_smoke.py
 dulwich/tests/__init__.py
 dulwich/tests/__init__.py
+dulwich/tests/test_archive.py
 dulwich/tests/test_blackbox.py
 dulwich/tests/test_blackbox.py
 dulwich/tests/test_client.py
 dulwich/tests/test_client.py
 dulwich/tests/test_config.py
 dulwich/tests/test_config.py
@@ -87,6 +92,7 @@ dulwich/tests/test_pack.py
 dulwich/tests/test_patch.py
 dulwich/tests/test_patch.py
 dulwich/tests/test_porcelain.py
 dulwich/tests/test_porcelain.py
 dulwich/tests/test_protocol.py
 dulwich/tests/test_protocol.py
+dulwich/tests/test_reflog.py
 dulwich/tests/test_refs.py
 dulwich/tests/test_refs.py
 dulwich/tests/test_repository.py
 dulwich/tests/test_repository.py
 dulwich/tests/test_server.py
 dulwich/tests/test_server.py

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

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

+ 1 - 1
dulwich/__init__.py

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

+ 64 - 14
dulwich/_diff_tree.c

@@ -32,6 +32,22 @@ typedef int Py_ssize_t;
 #define Py_SIZE(ob)             (((PyVarObject*)(ob))->ob_size)
 #define Py_SIZE(ob)             (((PyVarObject*)(ob))->ob_size)
 #endif
 #endif
 
 
+#if PY_MAJOR_VERSION >= 3
+#define PyInt_FromLong PyLong_FromLong
+#define PyInt_AsLong PyLong_AsLong
+#define PyInt_AS_LONG PyLong_AS_LONG
+#define PyString_AS_STRING PyBytes_AS_STRING
+#define PyString_AsString PyBytes_AsString
+#define PyString_AsStringAndSize PyBytes_AsStringAndSize
+#define PyString_Check PyBytes_Check
+#define PyString_CheckExact PyBytes_CheckExact
+#define PyString_FromStringAndSize PyBytes_FromStringAndSize
+#define PyString_FromString PyBytes_FromString
+#define PyString_GET_SIZE PyBytes_GET_SIZE
+#define PyString_Size PyBytes_Size
+#define _PyString_Join _PyBytes_Join
+#endif
+
 static PyObject *tree_entry_cls = NULL, *null_entry = NULL,
 static PyObject *tree_entry_cls = NULL, *null_entry = NULL,
 	*defaultdict_cls = NULL, *int_cls = NULL;
 	*defaultdict_cls = NULL, *int_cls = NULL;
 static int block_size;
 static int block_size;
@@ -124,8 +140,13 @@ static PyObject **tree_entries(char *path, Py_ssize_t path_len, PyObject *tree,
 			memcpy(new_path, PyString_AS_STRING(name), name_len);
 			memcpy(new_path, PyString_AS_STRING(name), name_len);
 		}
 		}
 
 
+#if PY_MAJOR_VERSION >= 3
+		result[i] = PyObject_CallFunction(tree_entry_cls, "y#OO", new_path,
+			new_path_len, PyTuple_GET_ITEM(old_entry, 1), sha);
+#else
 		result[i] = PyObject_CallFunction(tree_entry_cls, "s#OO", new_path,
 		result[i] = PyObject_CallFunction(tree_entry_cls, "s#OO", new_path,
 			new_path_len, PyTuple_GET_ITEM(old_entry, 1), sha);
 			new_path_len, PyTuple_GET_ITEM(old_entry, 1), sha);
+#endif
 		PyMem_Free(new_path);
 		PyMem_Free(new_path);
 		if (!result[i]) {
 		if (!result[i]) {
 			goto error;
 			goto error;
@@ -152,16 +173,18 @@ static int entry_path_cmp(PyObject *entry1, PyObject *entry2)
 	path1 = PyObject_GetAttrString(entry1, "path");
 	path1 = PyObject_GetAttrString(entry1, "path");
 	if (!path1)
 	if (!path1)
 		goto done;
 		goto done;
+
 	if (!PyString_Check(path1)) {
 	if (!PyString_Check(path1)) {
-		PyErr_SetString(PyExc_TypeError, "path is not a string");
+		PyErr_SetString(PyExc_TypeError, "path is not a (byte)string");
 		goto done;
 		goto done;
 	}
 	}
 
 
 	path2 = PyObject_GetAttrString(entry2, "path");
 	path2 = PyObject_GetAttrString(entry2, "path");
 	if (!path2)
 	if (!path2)
 		goto done;
 		goto done;
+
 	if (!PyString_Check(path2)) {
 	if (!PyString_Check(path2)) {
-		PyErr_SetString(PyExc_TypeError, "path is not a string");
+		PyErr_SetString(PyExc_TypeError, "path is not a (byte)string");
 		goto done;
 		goto done;
 	}
 	}
 
 
@@ -182,7 +205,11 @@ static PyObject *py_merge_entries(PyObject *self, PyObject *args)
 	char *path_str;
 	char *path_str;
 	int cmp;
 	int cmp;
 
 
+#if PY_MAJOR_VERSION >= 3
+	if (!PyArg_ParseTuple(args, "y#OO", &path_str, &path_len, &tree1, &tree2))
+#else
 	if (!PyArg_ParseTuple(args, "s#OO", &path_str, &path_len, &tree1, &tree2))
 	if (!PyArg_ParseTuple(args, "s#OO", &path_str, &path_len, &tree1, &tree2))
+#endif
 		return NULL;
 		return NULL;
 
 
 	entries1 = tree_entries(path_str, path_len, tree1, &n1);
 	entries1 = tree_entries(path_str, path_len, tree1, &n1);
@@ -390,12 +417,28 @@ static PyMethodDef py_diff_tree_methods[] = {
 	{ NULL, NULL, 0, NULL }
 	{ NULL, NULL, 0, NULL }
 };
 };
 
 
-PyMODINIT_FUNC
-init_diff_tree(void)
+static PyObject *
+moduleinit(void)
 {
 {
 	PyObject *m, *objects_mod = NULL, *diff_tree_mod = NULL;
 	PyObject *m, *objects_mod = NULL, *diff_tree_mod = NULL;
-        PyObject *block_size_obj = NULL;
+	PyObject *block_size_obj = NULL;
+
+#if PY_MAJOR_VERSION >= 3
+	static struct PyModuleDef moduledef = {
+		PyModuleDef_HEAD_INIT,
+		"_diff_tree",         /* m_name */
+		NULL,                 /* m_doc */
+		-1,                   /* m_size */
+		py_diff_tree_methods, /* m_methods */
+		NULL,                 /* m_reload */
+		NULL,                 /* m_traverse */
+		NULL,                 /* m_clear*/
+		NULL,                 /* m_free */
+	};
+	m = PyModule_Create(&moduledef);
+#else
 	m = Py_InitModule("_diff_tree", py_diff_tree_methods);
 	m = Py_InitModule("_diff_tree", py_diff_tree_methods);
+#endif
 	if (!m)
 	if (!m)
 		goto error;
 		goto error;
 
 
@@ -437,11 +480,8 @@ init_diff_tree(void)
 	}
 	}
 
 
 	Py_DECREF(diff_tree_mod);
 	Py_DECREF(diff_tree_mod);
-#if PY_MAJOR_VERSION < 3
-	return;
-#else
-	return NULL;
-#endif
+
+	return m;
 
 
 error:
 error:
 	Py_XDECREF(objects_mod);
 	Py_XDECREF(objects_mod);
@@ -450,9 +490,19 @@ error:
 	Py_XDECREF(block_size_obj);
 	Py_XDECREF(block_size_obj);
 	Py_XDECREF(defaultdict_cls);
 	Py_XDECREF(defaultdict_cls);
 	Py_XDECREF(int_cls);
 	Py_XDECREF(int_cls);
-#if PY_MAJOR_VERSION < 3
-	return;
-#else
 	return NULL;
 	return NULL;
-#endif
 }
 }
+
+#if PY_MAJOR_VERSION >= 3
+PyMODINIT_FUNC
+PyInit__diff_tree(void)
+{
+	return moduleinit();
+}
+#else
+PyMODINIT_FUNC
+init_diff_tree(void)
+{
+	moduleinit();
+}
+#endif

+ 52 - 27
dulwich/_objects.c

@@ -25,6 +25,15 @@
 typedef int Py_ssize_t;
 typedef int Py_ssize_t;
 #endif
 #endif
 
 
+#if PY_MAJOR_VERSION >= 3
+#define PyInt_Check(obj) 0
+#define PyInt_CheckExact(obj) 0
+#define PyInt_AsLong PyLong_AsLong
+#define PyString_AS_STRING PyBytes_AS_STRING
+#define PyString_Check PyBytes_Check
+#define PyString_FromStringAndSize PyBytes_FromStringAndSize
+#endif
+
 #if defined(__MINGW32_VERSION) || defined(__APPLE__)
 #if defined(__MINGW32_VERSION) || defined(__APPLE__)
 size_t rep_strnlen(char *text, size_t maxlen);
 size_t rep_strnlen(char *text, size_t maxlen);
 size_t rep_strnlen(char *text, size_t maxlen)
 size_t rep_strnlen(char *text, size_t maxlen)
@@ -59,8 +68,13 @@ static PyObject *py_parse_tree(PyObject *self, PyObject *args, PyObject *kw)
 	PyObject *ret, *item, *name, *sha, *py_strict = NULL;
 	PyObject *ret, *item, *name, *sha, *py_strict = NULL;
 	static char *kwlist[] = {"text", "strict", NULL};
 	static char *kwlist[] = {"text", "strict", NULL};
 
 
+#if PY_MAJOR_VERSION >= 3
+	if (!PyArg_ParseTupleAndKeywords(args, kw, "y#|O", kwlist,
+	                                 &text, &len, &py_strict))
+#else
 	if (!PyArg_ParseTupleAndKeywords(args, kw, "s#|O", kwlist,
 	if (!PyArg_ParseTupleAndKeywords(args, kw, "s#|O", kwlist,
 	                                 &text, &len, &py_strict))
 	                                 &text, &len, &py_strict))
+#endif
 		return NULL;
 		return NULL;
 	strict = py_strict ?  PyObject_IsTrue(py_strict) : 0;
 	strict = py_strict ?  PyObject_IsTrue(py_strict) : 0;
 	/* TODO: currently this returns a list; if memory usage is a concern,
 	/* TODO: currently this returns a list; if memory usage is a concern,
@@ -248,58 +262,69 @@ static PyMethodDef py_objects_methods[] = {
 	{ NULL, NULL, 0, NULL }
 	{ NULL, NULL, 0, NULL }
 };
 };
 
 
-PyMODINIT_FUNC
-init_objects(void)
+static PyObject *
+moduleinit(void)
 {
 {
 	PyObject *m, *objects_mod, *errors_mod;
 	PyObject *m, *objects_mod, *errors_mod;
 
 
-	m = Py_InitModule3("_objects", py_objects_methods, NULL);
-	if (m == NULL) {
-#if PY_MAJOR_VERSION < 3
-	  return;
+#if PY_MAJOR_VERSION >= 3
+	static struct PyModuleDef moduledef = {
+		PyModuleDef_HEAD_INIT,
+		"_objects",         /* m_name */
+		NULL,               /* m_doc */
+		-1,                 /* m_size */
+		py_objects_methods, /* m_methods */
+		NULL,               /* m_reload */
+		NULL,               /* m_traverse */
+		NULL,               /* m_clear*/
+		NULL,               /* m_free */
+	};
+	m = PyModule_Create(&moduledef);
 #else
 #else
-	  return NULL;
+	m = Py_InitModule3("_objects", py_objects_methods, NULL);
 #endif
 #endif
+	if (m == NULL) {
+		return NULL;
 	}
 	}
 
 
 	errors_mod = PyImport_ImportModule("dulwich.errors");
 	errors_mod = PyImport_ImportModule("dulwich.errors");
 	if (errors_mod == NULL) {
 	if (errors_mod == NULL) {
-#if PY_MAJOR_VERSION < 3
-	  return;
-#else
-	  return NULL;
-#endif
+		return NULL;
 	}
 	}
 
 
 	object_format_exception_cls = PyObject_GetAttrString(
 	object_format_exception_cls = PyObject_GetAttrString(
 		errors_mod, "ObjectFormatException");
 		errors_mod, "ObjectFormatException");
 	Py_DECREF(errors_mod);
 	Py_DECREF(errors_mod);
 	if (object_format_exception_cls == NULL) {
 	if (object_format_exception_cls == NULL) {
-#if PY_MAJOR_VERSION < 3
-	  return;
-#else
-	  return NULL;
-#endif
+		return NULL;
 	}
 	}
 
 
 	/* This is a circular import but should be safe since this module is
 	/* This is a circular import but should be safe since this module is
 	 * imported at at the very bottom of objects.py. */
 	 * imported at at the very bottom of objects.py. */
 	objects_mod = PyImport_ImportModule("dulwich.objects");
 	objects_mod = PyImport_ImportModule("dulwich.objects");
 	if (objects_mod == NULL) {
 	if (objects_mod == NULL) {
-#if PY_MAJOR_VERSION < 3
-	  return;
-#else
-	  return NULL;
-#endif
+		return NULL;
 	}
 	}
 
 
 	tree_entry_cls = PyObject_GetAttrString(objects_mod, "TreeEntry");
 	tree_entry_cls = PyObject_GetAttrString(objects_mod, "TreeEntry");
 	Py_DECREF(objects_mod);
 	Py_DECREF(objects_mod);
 	if (tree_entry_cls == NULL) {
 	if (tree_entry_cls == NULL) {
-#if PY_MAJOR_VERSION < 3
-	  return;
-#else
-	  return NULL;
-#endif
+		return NULL;
 	}
 	}
+
+	return m;
 }
 }
+
+#if PY_MAJOR_VERSION >= 3
+PyMODINIT_FUNC
+PyInit__objects(void)
+{
+	return moduleinit();
+}
+#else
+PyMODINIT_FUNC
+init_objects(void)
+{
+	moduleinit();
+}
+#endif

+ 57 - 6
dulwich/_pack.c

@@ -20,6 +20,19 @@
 #include <Python.h>
 #include <Python.h>
 #include <stdint.h>
 #include <stdint.h>
 
 
+#if PY_MAJOR_VERSION >= 3
+#define PyInt_FromLong PyLong_FromLong
+#define PyString_AS_STRING PyBytes_AS_STRING
+#define PyString_AsString PyBytes_AsString
+#define PyString_Check PyBytes_Check
+#define PyString_CheckExact PyBytes_CheckExact
+#define PyString_FromStringAndSize PyBytes_FromStringAndSize
+#define PyString_FromString PyBytes_FromString
+#define PyString_GET_SIZE PyBytes_GET_SIZE
+#define PyString_Size PyBytes_Size
+#define _PyString_Join _PyBytes_Join
+#endif
+
 static PyObject *PyExc_ApplyDeltaError = NULL;
 static PyObject *PyExc_ApplyDeltaError = NULL;
 
 
 static int py_is_sha(PyObject *sha)
 static int py_is_sha(PyObject *sha)
@@ -193,8 +206,13 @@ static PyObject *py_bisect_find_sha(PyObject *self, PyObject *args)
 	char *sha;
 	char *sha;
 	int sha_len;
 	int sha_len;
 	int start, end;
 	int start, end;
-	if (!PyArg_ParseTuple(args, "iis#O", &start, &end, 
-						  &sha, &sha_len, &unpack_name))
+#if PY_MAJOR_VERSION >= 3
+	if (!PyArg_ParseTuple(args, "iiy#O", &start, &end,
+			      &sha, &sha_len, &unpack_name))
+#else
+	if (!PyArg_ParseTuple(args, "iis#O", &start, &end,
+			      &sha, &sha_len, &unpack_name))
+#endif
 		return NULL;
 		return NULL;
 
 
 	if (sha_len != 20) {
 	if (sha_len != 20) {
@@ -239,20 +257,53 @@ static PyMethodDef py_pack_methods[] = {
 	{ NULL, NULL, 0, NULL }
 	{ NULL, NULL, 0, NULL }
 };
 };
 
 
-void init_pack(void)
+static PyObject *
+moduleinit(void)
 {
 {
 	PyObject *m;
 	PyObject *m;
 	PyObject *errors_module;
 	PyObject *errors_module;
 
 
 	errors_module = PyImport_ImportModule("dulwich.errors");
 	errors_module = PyImport_ImportModule("dulwich.errors");
 	if (errors_module == NULL)
 	if (errors_module == NULL)
-		return;
+		return NULL;
 
 
 	PyExc_ApplyDeltaError = PyObject_GetAttrString(errors_module, "ApplyDeltaError");
 	PyExc_ApplyDeltaError = PyObject_GetAttrString(errors_module, "ApplyDeltaError");
+	Py_DECREF(errors_module);
 	if (PyExc_ApplyDeltaError == NULL)
 	if (PyExc_ApplyDeltaError == NULL)
-		return;
+		return NULL;
 
 
+#if PY_MAJOR_VERSION >= 3
+	static struct PyModuleDef moduledef = {
+	  PyModuleDef_HEAD_INIT,
+	  "_pack",         /* m_name */
+	  NULL,            /* m_doc */
+	  -1,              /* m_size */
+	  py_pack_methods, /* m_methods */
+	  NULL,            /* m_reload */
+	  NULL,            /* m_traverse */
+	  NULL,            /* m_clear*/
+	  NULL,            /* m_free */
+	};
+	m = PyModule_Create(&moduledef);
+#else
 	m = Py_InitModule3("_pack", py_pack_methods, NULL);
 	m = Py_InitModule3("_pack", py_pack_methods, NULL);
+#endif
 	if (m == NULL)
 	if (m == NULL)
-		return;
+		return NULL;
+
+	return m;
+}
+
+#if PY_MAJOR_VERSION >= 3
+PyMODINIT_FUNC
+PyInit__pack(void)
+{
+	return moduleinit();
+}
+#else
+PyMODINIT_FUNC
+init_pack(void)
+{
+	moduleinit();
 }
 }
+#endif

+ 112 - 0
dulwich/archive.py

@@ -0,0 +1,112 @@
+# archive.py -- Creating an archive from a tarball
+# Copyright (C) 2015 Jonas Haag <jonas@lophus.org>
+# Copyright (C) 2015 Jelmer Vernooij <jelmer@jelmer.uk>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# or (at your option) a later version of the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA  02110-1301, USA.
+
+"""Generates tarballs for Git trees.
+
+"""
+
+import posixpath
+import stat
+import tarfile
+from io import BytesIO
+from contextlib import closing
+
+
+class ChunkedBytesIO(object):
+    """Turn a list of bytestrings into a file-like object.
+
+    This is similar to creating a `BytesIO` from a concatenation of the
+    bytestring list, but saves memory by NOT creating one giant bytestring first::
+
+        BytesIO(b''.join(list_of_bytestrings)) =~= ChunkedBytesIO(list_of_bytestrings)
+    """
+    def __init__(self, contents):
+        self.contents = contents
+        self.pos = (0, 0)
+
+    def read(self, maxbytes=None):
+        if maxbytes < 0:
+            maxbytes = float('inf')
+
+        buf = []
+        chunk, cursor = self.pos
+
+        while chunk < len(self.contents):
+            if maxbytes < len(self.contents[chunk]) - cursor:
+                buf.append(self.contents[chunk][cursor:cursor+maxbytes])
+                cursor += maxbytes
+                self.pos = (chunk, cursor)
+                break
+            else:
+                buf.append(self.contents[chunk][cursor:])
+                maxbytes -= len(self.contents[chunk]) - cursor
+                chunk += 1
+                cursor = 0
+                self.pos = (chunk, cursor)
+        return b''.join(buf)
+
+
+def tar_stream(store, tree, mtime, format=''):
+    """Generate a tar stream for the contents of a Git tree.
+
+    Returns a generator that lazily assembles a .tar.gz archive, yielding it in
+    pieces (bytestrings). To obtain the complete .tar.gz binary file, simply
+    concatenate these chunks.
+
+    :param store: Object store to retrieve objects from
+    :param tree: Tree object for the tree root
+    :param mtime: UNIX timestamp that is assigned as the modification time for
+        all files
+    :param format: Optional compression format for tarball
+    :return: Bytestrings
+    """
+    buf = BytesIO()
+    with closing(tarfile.open(None, "w:%s" % format, buf)) as tar:
+        for entry_abspath, entry in _walk_tree(store, tree):
+            try:
+                blob = store[entry.sha]
+            except KeyError:
+                # Entry probably refers to a submodule, which we don't yet support.
+                continue
+            data = ChunkedBytesIO(blob.chunked)
+
+            info = tarfile.TarInfo()
+            info.name = entry_abspath.decode('ascii') # tarfile only works with ascii.
+            info.size = blob.raw_length()
+            info.mode = entry.mode
+            info.mtime = mtime
+
+            tar.addfile(info, data)
+            yield buf.getvalue()
+            buf.truncate(0)
+            buf.seek(0)
+    yield buf.getvalue()
+
+
+def _walk_tree(store, tree, root=b''):
+    """Recursively walk a dulwich Tree, yielding tuples of
+    (absolute path, TreeEntry) along the way.
+    """
+    for entry in tree.iteritems():
+        entry_abspath = posixpath.join(root, entry.path)
+        if stat.S_ISDIR(entry.mode):
+            for _ in _walk_tree(store, store[entry.sha], entry_abspath):
+                yield _
+        else:
+            yield (entry_abspath, entry)

+ 21 - 16
dulwich/client.py

@@ -42,7 +42,6 @@ from contextlib import closing
 from io import BytesIO, BufferedReader
 from io import BytesIO, BufferedReader
 import dulwich
 import dulwich
 import select
 import select
-import shlex
 import socket
 import socket
 import subprocess
 import subprocess
 import sys
 import sys
@@ -230,12 +229,20 @@ class GitClient(object):
         :param determine_wants: Optional function to determine what refs
         :param determine_wants: Optional function to determine what refs
             to fetch
             to fetch
         :param progress: Optional progress function
         :param progress: Optional progress function
-        :return: remote refs as dictionary
+        :return: Dictionary with all remote refs (not just those fetched)
         """
         """
         if determine_wants is None:
         if determine_wants is None:
             determine_wants = target.object_store.determine_wants_all
             determine_wants = target.object_store.determine_wants_all
-
-        f, commit, abort = target.object_store.add_pack()
+        if CAPABILITY_THIN_PACK in self._fetch_capabilities:
+           # TODO(jelmer): Avoid reading entire file into memory and
+           # only processing it after the whole file has been fetched.
+           f = BytesIO()
+           def commit():
+              if f.tell():
+                f.seek(0)
+                target.object_store.add_thin_pack(f.read, None)
+        else:
+           f, commit, abort = target.object_store.add_pack()
         try:
         try:
             result = self.fetch_pack(
             result = self.fetch_pack(
                 path, determine_wants, target.get_graph_walker(), f.write,
                 path, determine_wants, target.get_graph_walker(), f.write,
@@ -255,6 +262,7 @@ class GitClient(object):
         :param graph_walker: Object with next() and ack().
         :param graph_walker: Object with next() and ack().
         :param pack_data: Callback called for each bit of data in the pack
         :param pack_data: Callback called for each bit of data in the pack
         :param progress: Callback for progress reports (strings)
         :param progress: Callback for progress reports (strings)
+        :return: Dictionary with all remote refs (not just those fetched)
         """
         """
         raise NotImplementedError(self.fetch_pack)
         raise NotImplementedError(self.fetch_pack)
 
 
@@ -542,6 +550,7 @@ class TraditionalGitClient(GitClient):
         :param graph_walker: Object with next() and ack().
         :param graph_walker: Object with next() and ack().
         :param pack_data: Callback called for each bit of data in the pack
         :param pack_data: Callback called for each bit of data in the pack
         :param progress: Callback for progress reports (strings)
         :param progress: Callback for progress reports (strings)
+        :return: Dictionary with all remote refs (not just those fetched)
         """
         """
         proto, can_read = self._connect(b'upload-pack', path)
         proto, can_read = self._connect(b'upload-pack', path)
         with proto:
         with proto:
@@ -662,7 +671,7 @@ class SubprocessWrapper(object):
         self.write = proc.stdin.write
         self.write = proc.stdin.write
 
 
     def can_read(self):
     def can_read(self):
-        if subprocess.mswindows:
+        if sys.platform == 'win32':
             from msvcrt import get_osfhandle
             from msvcrt import get_osfhandle
             from win32pipe import PeekNamedPipe
             from win32pipe import PeekNamedPipe
             handle = get_osfhandle(self.proc.stdout.fileno())
             handle = get_osfhandle(self.proc.stdout.fileno())
@@ -784,7 +793,7 @@ class LocalGitClient(GitClient):
         :param determine_wants: Optional function to determine what refs
         :param determine_wants: Optional function to determine what refs
             to fetch
             to fetch
         :param progress: Optional progress function
         :param progress: Optional progress function
-        :return: remote refs as dictionary
+        :return: Dictionary with all remote refs (not just those fetched)
         """
         """
         from dulwich.repo import Repo
         from dulwich.repo import Repo
         with closing(Repo(path)) as r:
         with closing(Repo(path)) as r:
@@ -799,6 +808,7 @@ class LocalGitClient(GitClient):
         :param graph_walker: Object with next() and ack().
         :param graph_walker: Object with next() and ack().
         :param pack_data: Callback called for each bit of data in the pack
         :param pack_data: Callback called for each bit of data in the pack
         :param progress: Callback for progress reports (strings)
         :param progress: Callback for progress reports (strings)
+        :return: Dictionary with all remote refs (not just those fetched)
         """
         """
         from dulwich.repo import Repo
         from dulwich.repo import Repo
         with closing(Repo(path)) as r:
         with closing(Repo(path)) as r:
@@ -850,8 +860,7 @@ class SubprocessSSHVendor(SSHVendor):
     """SSH vendor that shells out to the local 'ssh' command."""
     """SSH vendor that shells out to the local 'ssh' command."""
 
 
     def run_command(self, host, command, username=None, port=None):
     def run_command(self, host, command, username=None, port=None):
-        if (type(command) is not list or
-            not all([isinstance(b, bytes) for b in command])):
+        if not isinstance(command, bytes):
             raise TypeError(command)
             raise TypeError(command)
 
 
         #FIXME: This has no way to deal with passwords..
         #FIXME: This has no way to deal with passwords..
@@ -861,7 +870,7 @@ class SubprocessSSHVendor(SSHVendor):
         if username is not None:
         if username is not None:
             host = '%s@%s' % (username, host)
             host = '%s@%s' % (username, host)
         args.append(host)
         args.append(host)
-        proc = subprocess.Popen(args + command,
+        proc = subprocess.Popen(args + [command],
                                 stdin=subprocess.PIPE,
                                 stdin=subprocess.PIPE,
                                 stdout=subprocess.PIPE)
                                 stdout=subprocess.PIPE)
         return SubprocessWrapper(proc)
         return SubprocessWrapper(proc)
@@ -896,11 +905,7 @@ class SSHGitClient(TraditionalGitClient):
     def _get_cmd_path(self, cmd):
     def _get_cmd_path(self, cmd):
         cmd = self.alternative_paths.get(cmd, b'git-' + cmd)
         cmd = self.alternative_paths.get(cmd, b'git-' + cmd)
         assert isinstance(cmd, bytes)
         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'))]
+        return cmd
 
 
     def _connect(self, cmd, path):
     def _connect(self, cmd, path):
         if type(cmd) is not bytes:
         if type(cmd) is not bytes:
@@ -909,7 +914,7 @@ class SSHGitClient(TraditionalGitClient):
             raise TypeError(path)
             raise TypeError(path)
         if path.startswith(b"/~"):
         if path.startswith(b"/~"):
             path = path[1:]
             path = path[1:]
-        argv = self._get_cmd_path(cmd) + [path]
+        argv = self._get_cmd_path(cmd) + b" '" + path + b"'"
         con = self.ssh_vendor.run_command(
         con = self.ssh_vendor.run_command(
             self.host, argv, port=self.port, username=self.username)
             self.host, argv, port=self.port, username=self.username)
         return (Protocol(con.read, con.write, con.close,
         return (Protocol(con.read, con.write, con.close,
@@ -1057,7 +1062,7 @@ class HttpGitClient(GitClient):
         :param graph_walker: Object with next() and ack().
         :param graph_walker: Object with next() and ack().
         :param pack_data: Callback called for each bit of data in the pack
         :param pack_data: Callback called for each bit of data in the pack
         :param progress: Callback for progress reports (strings)
         :param progress: Callback for progress reports (strings)
-        :return: Dictionary with the refs of the remote repository
+        :return: Dictionary with all remote refs (not just those fetched)
         """
         """
         url = self._get_url(path)
         url = self._get_url(path)
         refs, server_capabilities = self._discover_references(
         refs, server_capabilities = self._discover_references(

+ 2 - 47
dulwich/contrib/paramiko_vendor.py

@@ -108,40 +108,6 @@ class _ParamikoWrapper(object):
         self.stop_monitoring()
         self.stop_monitoring()
 
 
 
 
-# {{{ shell quoting
-
-# Adapted from
-# https://github.com/python/cpython/blob/8cd133c63f156451eb3388b9308734f699f4f1af/Lib/shlex.py#L278
-
-def is_shell_safe(s):
-    import re
-    import sys
-
-    flags = 0
-    if sys.version_info >= (3,):
-        flags = re.ASCII
-
-    unsafe_re = re.compile(br'[^\w@%+=:,./-]', flags)
-
-    return unsafe_re.search(s) is None
-
-
-def shell_quote(s):
-    """Return a shell-escaped version of the byte string *s*."""
-
-    # Unconditionally quotes because that's apparently git's behavior, too,
-    # and some code hosting sites (notably Bitbucket) appear to rely on that.
-
-    if not s:
-        return b"''"
-
-    # use single quotes, and put single quotes into double quotes
-    # the string $'b is then quoted as '$'"'"'b'
-    return b"'" + s.replace(b"'", b"'\"'\"'") + b"'"
-
-# }}}
-
-
 class ParamikoSSHVendor(object):
 class ParamikoSSHVendor(object):
 
 
     def __init__(self):
     def __init__(self):
@@ -149,8 +115,7 @@ class ParamikoSSHVendor(object):
 
 
     def run_command(self, host, command, username=None, port=None,
     def run_command(self, host, command, username=None, port=None,
                     progress_stderr=None):
                     progress_stderr=None):
-        if (type(command) is not list or
-            not all([isinstance(b, bytes) for b in command])):
+        if not isinstance(command, bytes):
             raise TypeError(command)
             raise TypeError(command)
         # Paramiko needs an explicit port. None is not valid
         # Paramiko needs an explicit port. None is not valid
         if port is None:
         if port is None:
@@ -166,18 +131,8 @@ class ParamikoSSHVendor(object):
         # Open SSH session
         # Open SSH session
         channel = client.get_transport().open_session()
         channel = client.get_transport().open_session()
 
 
-        # Quote command
-        assert command
-        assert is_shell_safe(command[0])
-
-        quoted_command = (
-            command[0]
-            + b' '
-            + b' '.join(
-                shell_quote(c) for c in command[1:]))
-
         # Run commands
         # Run commands
-        channel.exec_command(quoted_command)
+        channel.exec_command(command)
 
 
         return _ParamikoWrapper(
         return _ParamikoWrapper(
             client, channel, progress_stderr=progress_stderr)
             client, channel, progress_stderr=progress_stderr)

+ 27 - 2
dulwich/objectspec.py

@@ -46,7 +46,15 @@ def parse_ref(container, refspec):
     :raise KeyError: If the ref can not be found
     :raise KeyError: If the ref can not be found
     """
     """
     refspec = to_bytes(refspec)
     refspec = to_bytes(refspec)
-    for ref in [refspec, b"refs/heads/" + refspec]:
+    possible_refs = [
+        refspec,
+        b"refs/" + refspec,
+        b"refs/tags/" + refspec,
+        b"refs/heads/" + refspec,
+        b"refs/remotes/" + refspec,
+        b"refs/remotes/" + refspec + b"/HEAD"
+    ]
+    for ref in possible_refs:
         if ref in container:
         if ref in container:
             return ref
             return ref
     else:
     else:
@@ -133,4 +141,21 @@ def parse_commit_range(repo, committishs):
     :raise ValueError: If the range can not be parsed
     :raise ValueError: If the range can not be parsed
     """
     """
     committishs = to_bytes(committishs)
     committishs = to_bytes(committishs)
-    return iter([repo[committishs]])
+    # TODO(jelmer): Support more than a single commit..
+    return iter([parse_commit(repo, committishs)])
+
+
+def parse_commit(repo, committish):
+    """Parse a string referring to a single commit.
+
+    :param repo: A` Repo` object
+    :param commitish: A string referring to a single commit.
+    :return: A Commit object
+    :raise KeyError: When the reference commits can not be found
+    :raise ValueError: If the range can not be parsed
+    """
+    committish = to_bytes(committish)
+    return repo[committish] # For now..
+
+
+# TODO: parse_path_in_tree(), which handles e.g. v1.0:Documentation

+ 16 - 11
dulwich/porcelain.py

@@ -57,9 +57,11 @@ import os
 import sys
 import sys
 import time
 import time
 
 
+from dulwich.archive import (
+    tar_stream,
+    )
 from dulwich.client import (
 from dulwich.client import (
     get_transport_and_path,
     get_transport_and_path,
-    SubprocessGitClient,
     )
     )
 from dulwich.errors import (
 from dulwich.errors import (
     SendPackError,
     SendPackError,
@@ -95,6 +97,10 @@ from dulwich.server import (
 GitStatus = namedtuple('GitStatus', 'staged unstaged untracked')
 GitStatus = namedtuple('GitStatus', 'staged unstaged untracked')
 
 
 
 
+default_bytes_out_stream = getattr(sys.stdout, 'buffer', sys.stdout)
+default_bytes_err_stream = getattr(sys.stderr, 'buffer', sys.stderr)
+
+
 def encode_path(path):
 def encode_path(path):
     """Encode a path as bytestring."""
     """Encode a path as bytestring."""
     # TODO(jelmer): Use something other than ascii?
     # TODO(jelmer): Use something other than ascii?
@@ -126,25 +132,24 @@ def open_repo_closing(path_or_repo):
     return closing(Repo(path_or_repo))
     return closing(Repo(path_or_repo))
 
 
 
 
-def archive(path, committish=None, outstream=sys.stdout,
+def archive(repo, committish=None, outstream=sys.stdout,
             errstream=sys.stderr):
             errstream=sys.stderr):
     """Create an archive.
     """Create an archive.
 
 
-    :param path: Path of repository for which to generate an archive.
+    :param repo: Path of repository for which to generate an archive.
     :param committish: Commit SHA1 or ref to use
     :param committish: Commit SHA1 or ref to use
     :param outstream: Output stream (defaults to stdout)
     :param outstream: Output stream (defaults to stdout)
     :param errstream: Error stream (defaults to stderr)
     :param errstream: Error stream (defaults to stderr)
     """
     """
 
 
-    client = SubprocessGitClient()
     if committish is None:
     if committish is None:
         committish = "HEAD"
         committish = "HEAD"
-    if not isinstance(path, bytes):
-        path = path.encode(sys.getfilesystemencoding())
-    # TODO(jelmer): This invokes C git; this introduces a dependency.
-    # Instead, dulwich should have its own archiver implementation.
-    client.archive(path, committish, outstream.write, errstream.write,
-                   errstream.write)
+    with open_repo_closing(repo) as repo_obj:
+        c = repo_obj[committish]
+        tree = c.tree
+        for chunk in tar_stream(repo_obj.object_store,
+                repo_obj.object_store[c.tree], c.commit_time):
+            outstream.write(chunk)
 
 
 
 
 def update_server_info(repo="."):
 def update_server_info(repo="."):
@@ -215,7 +220,7 @@ def init(path=".", bare=False):
         return Repo.init(path)
         return Repo.init(path)
 
 
 
 
-def clone(source, target=None, bare=False, checkout=None, errstream=sys.stdout, outstream=None):
+def clone(source, target=None, bare=False, checkout=None, errstream=default_bytes_err_stream, outstream=None):
     """Clone a local or remote git repository.
     """Clone a local or remote git repository.
 
 
     :param source: Path or URL for source repository
     :param source: Path or URL for source repository

+ 72 - 0
dulwich/reflog.py

@@ -0,0 +1,72 @@
+# reflog.py -- Parsing and writing reflog files
+# Copyright (C) 2015 Jelmer Vernooij and others.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; version 2
+# of the License or (at your option) a later version of the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA  02110-1301, USA.
+
+"""Utilities for reading and generating reflogs.
+"""
+
+import collections
+
+from dulwich.objects import (
+    format_timezone,
+    parse_timezone,
+    ZERO_SHA,
+    )
+
+Entry = collections.namedtuple('Entry', ['old_sha', 'new_sha', 'committer',
+    'timestamp', 'timezone', 'message'])
+
+
+def format_reflog_line(old_sha, new_sha, committer, timestamp, timezone, message):
+    """Generate a single reflog line.
+
+    :param old_sha: Old Commit SHA
+    :param new_sha: New Commit SHA
+    :param committer: Committer name and e-mail
+    :param timestamp: Timestamp
+    :param timezone: Timezone
+    :param message: Message
+    """
+    if old_sha is None:
+        old_sha = ZERO_SHA
+    return (old_sha + b' ' + new_sha + b' ' + committer + b' ' +
+            str(timestamp).encode('ascii') + b' ' +
+            format_timezone(timezone) + b'\t' + message)
+
+
+def parse_reflog_line(line):
+    """Parse a reflog line.
+
+    :param line: Line to parse
+    :return: Tuple of (old_sha, new_sha, committer, timestamp, timezone,
+        message)
+    """
+    (begin, message) = line.split(b'\t', 1)
+    (old_sha, new_sha, rest) = begin.split(b' ', 2)
+    (committer, timestamp_str, timezone_str) = rest.rsplit(b' ', 2)
+    return Entry(old_sha, new_sha, committer, int(timestamp_str),
+                 parse_timezone(timezone_str)[0], message)
+
+
+def read_reflog(f):
+    """Read reflog.
+
+    :param f: File-like object
+    :returns: Iterator over Entry objects
+    """
+    for l in f:
+        yield parse_reflog_line(l)

+ 2 - 0
dulwich/refs.py

@@ -149,6 +149,8 @@ class RefsContainer(object):
         keys = self.keys(base)
         keys = self.keys(base)
         if base is None:
         if base is None:
             base = b''
             base = b''
+        else:
+            base = base.rstrip(b'/')
         for key in keys:
         for key in keys:
             try:
             try:
                 ret[key] = self[(base + b'/' + key).strip(b'/')]
                 ret[key] = self[(base + b'/' + key).strip(b'/')]

+ 24 - 7
dulwich/repo.py

@@ -82,6 +82,7 @@ from dulwich.refs import (
 import warnings
 import warnings
 
 
 
 
+CONTROLDIR = '.git'
 OBJECTDIR = 'objects'
 OBJECTDIR = 'objects'
 REFSDIR = 'refs'
 REFSDIR = 'refs'
 REFSDIR_TAGS = 'tags'
 REFSDIR_TAGS = 'tags'
@@ -631,6 +632,21 @@ class BaseRepo(object):
         return c.id
         return c.id
 
 
 
 
+
+def read_gitfile(f):
+    """Read a ``.git`` file.
+
+    The first line of the file should start with "gitdir: "
+
+    :param f: File-like object to read from
+    :return: A path
+    """
+    cs = f.read()
+    if not cs.startswith("gitdir: "):
+        raise ValueError("Expected file to start with 'gitdir: '")
+    return cs[len("gitdir: "):].rstrip("\n")
+
+
 class Repo(BaseRepo):
 class Repo(BaseRepo):
     """A git repository backed by local disk.
     """A git repository backed by local disk.
 
 
@@ -641,17 +657,18 @@ class Repo(BaseRepo):
     """
     """
 
 
     def __init__(self, root):
     def __init__(self, root):
-        if os.path.isdir(os.path.join(root, ".git", OBJECTDIR)):
+        hidden_path = os.path.join(root, CONTROLDIR)
+        if os.path.isdir(os.path.join(hidden_path, OBJECTDIR)):
             self.bare = False
             self.bare = False
-            self._controldir = os.path.join(root, ".git")
+            self._controldir = hidden_path
         elif (os.path.isdir(os.path.join(root, OBJECTDIR)) and
         elif (os.path.isdir(os.path.join(root, OBJECTDIR)) and
               os.path.isdir(os.path.join(root, REFSDIR))):
               os.path.isdir(os.path.join(root, REFSDIR))):
             self.bare = True
             self.bare = True
             self._controldir = root
             self._controldir = root
-        elif (os.path.isfile(os.path.join(root, ".git"))):
-            import re
-            with open(os.path.join(root, ".git"), 'r') as f:
-                _, path = re.match('(gitdir: )(.+$)', f.read()).groups()
+        elif os.path.isfile(hidden_path):
+            self.bare = False
+            with open(hidden_path, 'r') as f:
+                path = read_gitfile(f)
             self.bare = False
             self.bare = False
             self._controldir = os.path.join(root, path)
             self._controldir = os.path.join(root, path)
         else:
         else:
@@ -910,7 +927,7 @@ class Repo(BaseRepo):
         """
         """
         if mkdir:
         if mkdir:
             os.mkdir(path)
             os.mkdir(path)
-        controldir = os.path.join(path, ".git")
+        controldir = os.path.join(path, CONTROLDIR)
         os.mkdir(controldir)
         os.mkdir(controldir)
         cls._init_maybe_bare(controldir, False)
         cls._init_maybe_bare(controldir, False)
         return cls(path)
         return cls(path)

+ 28 - 4
dulwich/server.py

@@ -207,6 +207,16 @@ class Handler(object):
         self.backend = backend
         self.backend = backend
         self.proto = proto
         self.proto = proto
         self.http_req = http_req
         self.http_req = http_req
+
+    def handle(self):
+        raise NotImplementedError(self.handle)
+
+
+class PackHandler(Handler):
+    """Protocol handler for packs."""
+
+    def __init__(self, backend, proto, http_req=None):
+        super(PackHandler, self).__init__(backend, proto, http_req)
         self._client_capabilities = None
         self._client_capabilities = None
         # Flags needed for the no-done capability
         # Flags needed for the no-done capability
         self._done_received = False
         self._done_received = False
@@ -254,12 +264,14 @@ class Handler(object):
         self._done_received = True
         self._done_received = True
 
 
 
 
-class UploadPackHandler(Handler):
+
+class UploadPackHandler(PackHandler):
     """Protocol handler for uploading a pack to the server."""
     """Protocol handler for uploading a pack to the server."""
 
 
     def __init__(self, backend, args, proto, http_req=None,
     def __init__(self, backend, args, proto, http_req=None,
                  advertise_refs=False):
                  advertise_refs=False):
-        Handler.__init__(self, backend, proto, http_req=http_req)
+        super(UploadPackHandler, self).__init__(backend, proto,
+            http_req=http_req)
         self.repo = backend.open_repository(args[0])
         self.repo = backend.open_repository(args[0])
         self._graph_walker = None
         self._graph_walker = None
         self.advertise_refs = advertise_refs
         self.advertise_refs = advertise_refs
@@ -829,12 +841,13 @@ class MultiAckDetailedGraphWalkerImpl(object):
         return True
         return True
 
 
 
 
-class ReceivePackHandler(Handler):
+class ReceivePackHandler(PackHandler):
     """Protocol handler for downloading a pack from the client."""
     """Protocol handler for downloading a pack from the client."""
 
 
     def __init__(self, backend, args, proto, http_req=None,
     def __init__(self, backend, args, proto, http_req=None,
                  advertise_refs=False):
                  advertise_refs=False):
-        Handler.__init__(self, backend, proto, http_req=http_req)
+        super(ReceivePackHandler, self).__init__(backend, proto,
+            http_req=http_req)
         self.repo = backend.open_repository(args[0])
         self.repo = backend.open_repository(args[0])
         self.advertise_refs = advertise_refs
         self.advertise_refs = advertise_refs
 
 
@@ -958,10 +971,21 @@ class ReceivePackHandler(Handler):
             self._report_status(status)
             self._report_status(status)
 
 
 
 
+class UploadArchiveHandler(Handler):
+
+    def __init__(self, backend, proto, http_req=None):
+        super(UploadArchiveHandler, self).__init__(backend, proto, http_req)
+
+    def handle(self):
+        # TODO(jelmer)
+        raise NotImplementedError(self.handle)
+
+
 # Default handler classes for git services.
 # Default handler classes for git services.
 DEFAULT_HANDLERS = {
 DEFAULT_HANDLERS = {
   b'git-upload-pack': UploadPackHandler,
   b'git-upload-pack': UploadPackHandler,
   b'git-receive-pack': ReceivePackHandler,
   b'git-receive-pack': ReceivePackHandler,
+#  b'git-upload-archive': UploadArchiveHandler,
   }
   }
 
 
 
 

+ 2 - 0
dulwich/tests/__init__.py

@@ -115,6 +115,7 @@ class BlackboxTestCase(TestCase):
 
 
 def self_test_suite():
 def self_test_suite():
     names = [
     names = [
+        'archive',
         'blackbox',
         'blackbox',
         'client',
         'client',
         'config',
         'config',
@@ -134,6 +135,7 @@ def self_test_suite():
         'patch',
         'patch',
         'porcelain',
         'porcelain',
         'protocol',
         'protocol',
+        'reflog',
         'refs',
         'refs',
         'repository',
         'repository',
         'server',
         'server',

+ 2 - 1
dulwich/tests/compat/test_client.py

@@ -324,8 +324,9 @@ class TestSSHVendor(object):
 
 
     @staticmethod
     @staticmethod
     def run_command(host, command, username=None, port=None):
     def run_command(host, command, username=None, port=None):
-        cmd, path = command
+        cmd, path = command.split(b' ')
         cmd = cmd.split(b'-', 1)
         cmd = cmd.split(b'-', 1)
+        path = path.replace(b"'", b"")
         p = subprocess.Popen(cmd + [path], bufsize=0, env=get_safe_env(), stdin=subprocess.PIPE,
         p = subprocess.Popen(cmd + [path], bufsize=0, env=get_safe_env(), stdin=subprocess.PIPE,
                              stdout=subprocess.PIPE, stderr=subprocess.PIPE)
                              stdout=subprocess.PIPE, stderr=subprocess.PIPE)
         return client.SubprocessWrapper(p)
         return client.SubprocessWrapper(p)

+ 1 - 0
dulwich/tests/compat/utils.py

@@ -124,6 +124,7 @@ def run_git(args, git_path=_DEFAULT_GIT, input=None, capture_stdout=False,
     """
     """
 
 
     env = get_safe_env(popen_kwargs.pop('env', None))
     env = get_safe_env(popen_kwargs.pop('env', None))
+    env['LC_ALL'] = env['LANG'] = 'C'
 
 
     args = [git_path] + args
     args = [git_path] + args
     popen_kwargs['stdin'] = subprocess.PIPE
     popen_kwargs['stdin'] = subprocess.PIPE

+ 67 - 0
dulwich/tests/test_archive.py

@@ -0,0 +1,67 @@
+# test_archive.py -- tests for archive
+# Copyright (C) 2015 Jelmer Vernooij <jelmer@jelmer.uk>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; version 2
+# of the License or (at your option) a later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA  02110-1301, USA.
+
+"""Tests for archive support."""
+
+from io import BytesIO
+import sys
+import tarfile
+
+from dulwich.archive import tar_stream
+from dulwich.object_store import (
+    MemoryObjectStore,
+    )
+from dulwich.objects import (
+    Blob,
+    Tree,
+    )
+from dulwich.tests import (
+    TestCase,
+    )
+from dulwich.tests.utils import (
+    build_commit_graph,
+    )
+
+
+class ArchiveTests(TestCase):
+
+    def test_empty(self):
+        if sys.version_info[:2] <= (2, 6):
+            self.skipTest("archive creation known failing on Python2.6")
+        store = MemoryObjectStore()
+        c1, c2, c3 = build_commit_graph(store, [[1], [2, 1], [3, 1, 2]])
+        tree = store[c3.tree]
+        stream = b''.join(tar_stream(store, tree, 10))
+        out = BytesIO(stream)
+        tf = tarfile.TarFile(fileobj=out)
+        self.addCleanup(tf.close)
+        self.assertEqual([], tf.getnames())
+
+    def test_simple(self):
+        self.skipTest("known to fail on python2.6 and 3.4; needs debugging")
+        store = MemoryObjectStore()
+        b1 = Blob.from_string(b"somedata")
+        store.add_object(b1)
+        t1 = Tree()
+        t1.add(b"somename", 0o100644, b1.id)
+        store.add_object(t1)
+        stream = b''.join(tar_stream(store, t1, 10))
+        out = BytesIO(stream)
+        tf = tarfile.TarFile(fileobj=out)
+        self.addCleanup(tf.close)
+        self.assertEqual(["somename"], tf.getnames())

+ 6 - 8
dulwich/tests/test_client.py

@@ -31,7 +31,6 @@ from dulwich.client import (
     LocalGitClient,
     LocalGitClient,
     TraditionalGitClient,
     TraditionalGitClient,
     TCPGitClient,
     TCPGitClient,
-    SubprocessGitClient,
     SSHGitClient,
     SSHGitClient,
     HttpGitClient,
     HttpGitClient,
     ReportStatusParser,
     ReportStatusParser,
@@ -502,8 +501,7 @@ class TestSSHVendor(object):
         self.port = None
         self.port = None
 
 
     def run_command(self, host, command, username=None, port=None):
     def run_command(self, host, command, username=None, port=None):
-        if (type(command) is not list or
-            not all([isinstance(b, bytes) for b in command])):
+        if not isinstance(command, bytes):
             raise TypeError(command)
             raise TypeError(command)
 
 
         self.host = host
         self.host = host
@@ -535,19 +533,19 @@ class SSHGitClientTests(TestCase):
         client.get_ssh_vendor = self.real_vendor
         client.get_ssh_vendor = self.real_vendor
 
 
     def test_default_command(self):
     def test_default_command(self):
-        self.assertEqual([b'git-upload-pack'],
+        self.assertEqual(b'git-upload-pack',
                 self.client._get_cmd_path(b'upload-pack'))
                 self.client._get_cmd_path(b'upload-pack'))
 
 
     def test_alternative_command_path(self):
     def test_alternative_command_path(self):
         self.client.alternative_paths[b'upload-pack'] = (
         self.client.alternative_paths[b'upload-pack'] = (
             b'/usr/lib/git/git-upload-pack')
             b'/usr/lib/git/git-upload-pack')
-        self.assertEqual([b'/usr/lib/git/git-upload-pack'],
+        self.assertEqual(b'/usr/lib/git/git-upload-pack',
             self.client._get_cmd_path(b'upload-pack'))
             self.client._get_cmd_path(b'upload-pack'))
 
 
     def test_alternative_command_path_spaces(self):
     def test_alternative_command_path_spaces(self):
         self.client.alternative_paths[b'upload-pack'] = (
         self.client.alternative_paths[b'upload-pack'] = (
             b'/usr/lib/git/git-upload-pack -ibla')
             b'/usr/lib/git/git-upload-pack -ibla')
-        self.assertEqual([b'/usr/lib/git/git-upload-pack', b'-ibla'],
+        self.assertEqual(b"/usr/lib/git/git-upload-pack -ibla",
             self.client._get_cmd_path(b'upload-pack'))
             self.client._get_cmd_path(b'upload-pack'))
 
 
     def test_connect(self):
     def test_connect(self):
@@ -560,10 +558,10 @@ class SSHGitClientTests(TestCase):
         client._connect(b"command", b"/path/to/repo")
         client._connect(b"command", b"/path/to/repo")
         self.assertEqual(b"username", server.username)
         self.assertEqual(b"username", server.username)
         self.assertEqual(1337, server.port)
         self.assertEqual(1337, server.port)
-        self.assertEqual([b"git-command", b"/path/to/repo"], server.command)
+        self.assertEqual(b"git-command '/path/to/repo'", server.command)
 
 
         client._connect(b"relative-command", b"/~/path/to/repo")
         client._connect(b"relative-command", b"/~/path/to/repo")
-        self.assertEqual([b"git-relative-command", b"~/path/to/repo"],
+        self.assertEqual(b"git-relative-command '~/path/to/repo'",
                           server.command)
                           server.command)
 
 
 
 

+ 47 - 5
dulwich/tests/test_objectspec.py

@@ -71,18 +71,60 @@ class ParseCommitRangeTests(TestCase):
 
 
 
 
 class ParseRefTests(TestCase):
 class ParseRefTests(TestCase):
-
     def test_nonexistent(self):
     def test_nonexistent(self):
         r = {}
         r = {}
         self.assertRaises(KeyError, parse_ref, r, b"thisdoesnotexist")
         self.assertRaises(KeyError, parse_ref, r, b"thisdoesnotexist")
 
 
-    def test_head(self):
+    def test_ambiguous_ref(self):
+        r = {b"ambig1": 'bla',
+             b"refs/ambig1": 'bla',
+             b"refs/tags/ambig1": 'bla',
+             b"refs/heads/ambig1": 'bla',
+             b"refs/remotes/ambig1": 'bla',
+             b"refs/remotes/ambig1/HEAD": "bla"}
+        self.assertEqual(b"ambig1", parse_ref(r, b"ambig1"))
+
+    def test_ambiguous_ref2(self):
+        r = {b"refs/ambig2": 'bla',
+             b"refs/tags/ambig2": 'bla',
+             b"refs/heads/ambig2": 'bla',
+             b"refs/remotes/ambig2": 'bla',
+             b"refs/remotes/ambig2/HEAD": "bla"}
+        self.assertEqual(b"refs/ambig2", parse_ref(r, b"ambig2"))
+
+    def test_ambiguous_tag(self):
+        r = {b"refs/tags/ambig3": 'bla',
+             b"refs/heads/ambig3": 'bla',
+             b"refs/remotes/ambig3": 'bla',
+             b"refs/remotes/ambig3/HEAD": "bla"}
+        self.assertEqual(b"refs/tags/ambig3", parse_ref(r, b"ambig3"))
+
+    def test_ambiguous_head(self):
+        r = {b"refs/heads/ambig4": 'bla',
+             b"refs/remotes/ambig4": 'bla',
+             b"refs/remotes/ambig4/HEAD": "bla"}
+        self.assertEqual(b"refs/heads/ambig4", parse_ref(r, b"ambig4"))
+
+    def test_ambiguous_remote(self):
+        r = {b"refs/remotes/ambig5": 'bla',
+             b"refs/remotes/ambig5/HEAD": "bla"}
+        self.assertEqual(b"refs/remotes/ambig5", parse_ref(r, b"ambig5"))
+
+    def test_ambiguous_remote_head(self):
+        r = {b"refs/remotes/ambig6/HEAD": "bla"}
+        self.assertEqual(b"refs/remotes/ambig6/HEAD", parse_ref(r, b"ambig6"))
+
+    def test_heads_full(self):
         r = {b"refs/heads/foo": "bla"}
         r = {b"refs/heads/foo": "bla"}
-        self.assertEqual(b"refs/heads/foo", parse_ref(r, b"foo"))
+        self.assertEqual(b"refs/heads/foo", parse_ref(r, b"refs/heads/foo"))
 
 
-    def test_full(self):
+    def test_heads_partial(self):
         r = {b"refs/heads/foo": "bla"}
         r = {b"refs/heads/foo": "bla"}
-        self.assertEqual(b"refs/heads/foo", parse_ref(r, b"refs/heads/foo"))
+        self.assertEqual(b"refs/heads/foo", parse_ref(r, b"heads/foo"))
+
+    def test_tags_partial(self):
+        r = {b"refs/tags/foo": "bla"}
+        self.assertEqual(b"refs/tags/foo", parse_ref(r, b"tags/foo"))
 
 
 
 
 class ParseRefsTests(TestCase):
 class ParseRefsTests(TestCase):

+ 0 - 3
dulwich/tests/test_porcelain.py

@@ -40,7 +40,6 @@ from dulwich.repo import Repo
 from dulwich.tests import (
 from dulwich.tests import (
     TestCase,
     TestCase,
     )
     )
-from dulwich.tests.compat.utils import require_git_version
 from dulwich.tests.utils import (
 from dulwich.tests.utils import (
     build_commit_graph,
     build_commit_graph,
     make_object,
     make_object,
@@ -64,8 +63,6 @@ class ArchiveTests(PorcelainTestCase):
     """Tests for the archive command."""
     """Tests for the archive command."""
 
 
     def test_simple(self):
     def test_simple(self):
-        # TODO(jelmer): Remove this once dulwich has its own implementation of archive.
-        require_git_version((1, 5, 0))
         c1, c2, c3 = build_commit_graph(self.repo.object_store, [[1], [2, 1], [3, 1, 2]])
         c1, c2, c3 = build_commit_graph(self.repo.object_store, [[1], [2, 1], [3, 1, 2]])
         self.repo.refs[b"refs/heads/master"] = c3.id
         self.repo.refs[b"refs/heads/master"] = c3.id
         out = BytesIO()
         out = BytesIO()

+ 69 - 0
dulwich/tests/test_reflog.py

@@ -0,0 +1,69 @@
+# test_reflog.py -- tests for reflog.py
+# encoding: utf-8
+# Copyright (C) 2015 Jelmer Vernooij <jelmer@samba.org>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; version 2
+# of the License or (at your option) any later version of
+# the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA  02110-1301, USA.
+
+"""Tests for dulwich.reflog."""
+
+
+from dulwich.reflog import (
+    format_reflog_line,
+    parse_reflog_line,
+    )
+
+from dulwich.tests import (
+    TestCase,
+    )
+
+
+class ReflogLineTests(TestCase):
+
+    def test_format(self):
+        self.assertEqual(
+            b'0000000000000000000000000000000000000000 '
+            b'49030649db3dfec5a9bc03e5dde4255a14499f16 Jelmer Vernooij '
+            b'<jelmer@jelmer.uk> 1446552482 +0000	'
+            b'clone: from git://jelmer.uk/samba',
+            format_reflog_line(
+                b'0000000000000000000000000000000000000000',
+                b'49030649db3dfec5a9bc03e5dde4255a14499f16',
+                b'Jelmer Vernooij <jelmer@jelmer.uk>',
+                1446552482, 0, b'clone: from git://jelmer.uk/samba'))
+
+        self.assertEqual(
+            b'0000000000000000000000000000000000000000 '
+            b'49030649db3dfec5a9bc03e5dde4255a14499f16 Jelmer Vernooij '
+            b'<jelmer@jelmer.uk> 1446552482 +0000	'
+            b'clone: from git://jelmer.uk/samba',
+            format_reflog_line(
+                None,
+                b'49030649db3dfec5a9bc03e5dde4255a14499f16',
+                b'Jelmer Vernooij <jelmer@jelmer.uk>',
+                1446552482, 0, b'clone: from git://jelmer.uk/samba'))
+
+    def test_parse(self):
+        self.assertEqual(
+                (b'0000000000000000000000000000000000000000',
+                 b'49030649db3dfec5a9bc03e5dde4255a14499f16',
+                 b'Jelmer Vernooij <jelmer@jelmer.uk>',
+                 1446552482, 0, b'clone: from git://jelmer.uk/samba'),
+                 parse_reflog_line(
+                     b'0000000000000000000000000000000000000000 '
+                     b'49030649db3dfec5a9bc03e5dde4255a14499f16 Jelmer Vernooij '
+                     b'<jelmer@jelmer.uk> 1446552482 +0000	'
+                     b'clone: from git://jelmer.uk/samba'))

+ 19 - 1
dulwich/tests/test_repository.py

@@ -125,8 +125,13 @@ class RepositoryRootTests(TestCase):
         for k, contained in test_keys:
         for k, contained in test_keys:
             self.assertEqual(k in r, contained)
             self.assertEqual(k in r, contained)
 
 
+        # Avoid deprecation warning under Py3.2+
+        if getattr(self, 'assertRaisesRegex', None):
+            assertRaisesRegexp = self.assertRaisesRegex
+        else:
+            assertRaisesRegexp = self.assertRaisesRegexp
         for k, _ in test_keys:
         for k, _ in test_keys:
-            self.assertRaisesRegexp(
+            assertRaisesRegexp(
                 TypeError, "'name' must be bytestring, not int",
                 TypeError, "'name' must be bytestring, not int",
                 r.__getitem__, 12
                 r.__getitem__, 12
             )
             )
@@ -502,6 +507,19 @@ exit 1
         self.assertTrue("post-commit hook failed: " in str(warnings_list[-1]))
         self.assertTrue("post-commit hook failed: " in str(warnings_list[-1]))
         self.assertEqual([commit_sha], r[commit_sha2].parents)
         self.assertEqual([commit_sha], r[commit_sha2].parents)
 
 
+    def test_as_dict(self):
+        def check(repo):
+            self.assertEqual(repo.refs.subkeys(b'refs/tags'), repo.refs.subkeys(b'refs/tags/'))
+            self.assertEqual(repo.refs.as_dict(b'refs/tags'), repo.refs.as_dict(b'refs/tags/'))
+            self.assertEqual(repo.refs.as_dict(b'refs/heads'), repo.refs.as_dict(b'refs/heads/'))
+
+        bare = self.open_repo('a.git')
+        tmp_dir = self.mkdtemp()
+        self.addCleanup(shutil.rmtree, tmp_dir)
+        with closing(bare.clone(tmp_dir, mkdir=False)) as nonbare:
+            check(nonbare)
+            check(bare)
+
 
 
 class BuildRepoRootTests(TestCase):
 class BuildRepoRootTests(TestCase):
     """Tests that build on-disk repos from scratch.
     """Tests that build on-disk repos from scratch.

+ 4 - 4
dulwich/tests/test_server.py

@@ -40,9 +40,9 @@ from dulwich.server import (
     Backend,
     Backend,
     DictBackend,
     DictBackend,
     FileSystemBackend,
     FileSystemBackend,
-    Handler,
     MultiAckGraphWalkerImpl,
     MultiAckGraphWalkerImpl,
     MultiAckDetailedGraphWalkerImpl,
     MultiAckDetailedGraphWalkerImpl,
+    PackHandler,
     _split_proto_line,
     _split_proto_line,
     serve_command,
     serve_command,
     _find_shallow,
     _find_shallow,
@@ -100,10 +100,10 @@ class TestProto(object):
         return lines.pop(0)
         return lines.pop(0)
 
 
 
 
-class TestGenericHandler(Handler):
+class TestGenericPackHandler(PackHandler):
 
 
     def __init__(self):
     def __init__(self):
-        Handler.__init__(self, Backend(), None)
+        PackHandler.__init__(self, Backend(), None)
 
 
     @classmethod
     @classmethod
     def capabilities(cls):
     def capabilities(cls):
@@ -118,7 +118,7 @@ class HandlerTestCase(TestCase):
 
 
     def setUp(self):
     def setUp(self):
         super(HandlerTestCase, self).setUp()
         super(HandlerTestCase, self).setUp()
-        self._handler = TestGenericHandler()
+        self._handler = TestGenericPackHandler()
 
 
     def assertSucceeds(self, func, *args, **kwargs):
     def assertSucceeds(self, func, *args, **kwargs):
         try:
         try:

+ 1 - 1
setup.py

@@ -8,7 +8,7 @@ except ImportError:
     from distutils.core import setup, Extension
     from distutils.core import setup, Extension
 from distutils.core import Distribution
 from distutils.core import Distribution
 
 
-dulwich_version_string = '0.11.2'
+dulwich_version_string = '0.12.0'
 
 
 include_dirs = []
 include_dirs = []
 # Windows MSVC support
 # Windows MSVC support