Browse Source

Imported Upstream version 0.12.0

Jelmer Vernooij 9 years ago
parent
commit
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
 
+Tox configuration is also present as well as a Travis configuration file.
+
 String Types
 ------------
 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 HACKING
 include CONTRIBUTING
+include TODO
 include setup.cfg
 include dulwich/stdint.h
 recursive-include docs conf.py *.txt Makefile make.bat
@@ -13,3 +14,4 @@ graft dulwich/tests/data
 include tox.ini
 include dulwich.cfg
 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
 
  IMPROVEMENTS

+ 1 - 1
PKG-INFO

@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: dulwich
-Version: 0.11.2
+Version: 0.12.0
 Summary: Python Git Library
 Home-page: https://www.dulwich.io/
 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
    remote
    tag
+   porcelain
    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
 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
 Name: dulwich
-Version: 0.11.2
+Version: 0.12.0
 Summary: Python Git Library
 Home-page: https://www.dulwich.io/
 Author: Jelmer Vernooij

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

@@ -1,3 +1,4 @@
+.testr.conf
 AUTHORS
 CONTRIBUTING
 COPYING
@@ -5,6 +6,7 @@ MANIFEST.in
 Makefile
 NEWS
 README.md
+TODO
 appveyor.yml
 dulwich.cfg
 setup.cfg
@@ -25,6 +27,7 @@ docs/tutorial/file-format.txt
 docs/tutorial/index.txt
 docs/tutorial/introduction.txt
 docs/tutorial/object-store.txt
+docs/tutorial/porcelain.txt
 docs/tutorial/remote.txt
 docs/tutorial/repo.txt
 docs/tutorial/tag.txt
@@ -33,6 +36,7 @@ dulwich/_compat.py
 dulwich/_diff_tree.c
 dulwich/_objects.c
 dulwich/_pack.c
+dulwich/archive.py
 dulwich/client.py
 dulwich/config.py
 dulwich/diff_tree.py
@@ -51,6 +55,7 @@ dulwich/pack.py
 dulwich/patch.py
 dulwich/porcelain.py
 dulwich/protocol.py
+dulwich/reflog.py
 dulwich/refs.py
 dulwich/repo.py
 dulwich/server.py
@@ -60,7 +65,6 @@ 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/paramiko_vendor.py
@@ -68,6 +72,7 @@ dulwich/contrib/swift.py
 dulwich/contrib/test_swift.py
 dulwich/contrib/test_swift_smoke.py
 dulwich/tests/__init__.py
+dulwich/tests/test_archive.py
 dulwich/tests/test_blackbox.py
 dulwich/tests/test_client.py
 dulwich/tests/test_config.py
@@ -87,6 +92,7 @@ dulwich/tests/test_pack.py
 dulwich/tests/test_patch.py
 dulwich/tests/test_porcelain.py
 dulwich/tests/test_protocol.py
+dulwich/tests/test_reflog.py
 dulwich/tests/test_refs.py
 dulwich/tests/test_repository.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."""
 
-__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)
 #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,
 	*defaultdict_cls = NULL, *int_cls = NULL;
 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);
 		}
 
+#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,
 			new_path_len, PyTuple_GET_ITEM(old_entry, 1), sha);
+#endif
 		PyMem_Free(new_path);
 		if (!result[i]) {
 			goto error;
@@ -152,16 +173,18 @@ static int entry_path_cmp(PyObject *entry1, PyObject *entry2)
 	path1 = PyObject_GetAttrString(entry1, "path");
 	if (!path1)
 		goto done;
+
 	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;
 	}
 
 	path2 = PyObject_GetAttrString(entry2, "path");
 	if (!path2)
 		goto done;
+
 	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;
 	}
 
@@ -182,7 +205,11 @@ static PyObject *py_merge_entries(PyObject *self, PyObject *args)
 	char *path_str;
 	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))
+#endif
 		return NULL;
 
 	entries1 = tree_entries(path_str, path_len, tree1, &n1);
@@ -390,12 +417,28 @@ static PyMethodDef py_diff_tree_methods[] = {
 	{ NULL, NULL, 0, NULL }
 };
 
-PyMODINIT_FUNC
-init_diff_tree(void)
+static PyObject *
+moduleinit(void)
 {
 	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);
+#endif
 	if (!m)
 		goto error;
 
@@ -437,11 +480,8 @@ init_diff_tree(void)
 	}
 
 	Py_DECREF(diff_tree_mod);
-#if PY_MAJOR_VERSION < 3
-	return;
-#else
-	return NULL;
-#endif
+
+	return m;
 
 error:
 	Py_XDECREF(objects_mod);
@@ -450,9 +490,19 @@ error:
 	Py_XDECREF(block_size_obj);
 	Py_XDECREF(defaultdict_cls);
 	Py_XDECREF(int_cls);
-#if PY_MAJOR_VERSION < 3
-	return;
-#else
 	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;
 #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__)
 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;
 	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,
 	                                 &text, &len, &py_strict))
+#endif
 		return NULL;
 	strict = py_strict ?  PyObject_IsTrue(py_strict) : 0;
 	/* 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 }
 };
 
-PyMODINIT_FUNC
-init_objects(void)
+static PyObject *
+moduleinit(void)
 {
 	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
-	  return NULL;
+	m = Py_InitModule3("_objects", py_objects_methods, NULL);
 #endif
+	if (m == NULL) {
+		return NULL;
 	}
 
 	errors_mod = PyImport_ImportModule("dulwich.errors");
 	if (errors_mod == NULL) {
-#if PY_MAJOR_VERSION < 3
-	  return;
-#else
-	  return NULL;
-#endif
+		return NULL;
 	}
 
 	object_format_exception_cls = PyObject_GetAttrString(
 		errors_mod, "ObjectFormatException");
 	Py_DECREF(errors_mod);
 	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
 	 * imported at at the very bottom of objects.py. */
 	objects_mod = PyImport_ImportModule("dulwich.objects");
 	if (objects_mod == NULL) {
-#if PY_MAJOR_VERSION < 3
-	  return;
-#else
-	  return NULL;
-#endif
+		return NULL;
 	}
 
 	tree_entry_cls = PyObject_GetAttrString(objects_mod, "TreeEntry");
 	Py_DECREF(objects_mod);
 	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 <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 int py_is_sha(PyObject *sha)
@@ -193,8 +206,13 @@ static PyObject *py_bisect_find_sha(PyObject *self, PyObject *args)
 	char *sha;
 	int sha_len;
 	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;
 
 	if (sha_len != 20) {
@@ -239,20 +257,53 @@ static PyMethodDef py_pack_methods[] = {
 	{ NULL, NULL, 0, NULL }
 };
 
-void init_pack(void)
+static PyObject *
+moduleinit(void)
 {
 	PyObject *m;
 	PyObject *errors_module;
 
 	errors_module = PyImport_ImportModule("dulwich.errors");
 	if (errors_module == NULL)
-		return;
+		return NULL;
 
 	PyExc_ApplyDeltaError = PyObject_GetAttrString(errors_module, "ApplyDeltaError");
+	Py_DECREF(errors_module);
 	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);
+#endif
 	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
 import dulwich
 import select
-import shlex
 import socket
 import subprocess
 import sys
@@ -230,12 +229,20 @@ class GitClient(object):
         :param determine_wants: Optional function to determine what refs
             to fetch
         :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:
             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:
             result = self.fetch_pack(
                 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 pack_data: Callback called for each bit of data in the pack
         :param progress: Callback for progress reports (strings)
+        :return: Dictionary with all remote refs (not just those fetched)
         """
         raise NotImplementedError(self.fetch_pack)
 
@@ -542,6 +550,7 @@ class TraditionalGitClient(GitClient):
         :param graph_walker: Object with next() and ack().
         :param pack_data: Callback called for each bit of data in the pack
         :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)
         with proto:
@@ -662,7 +671,7 @@ class SubprocessWrapper(object):
         self.write = proc.stdin.write
 
     def can_read(self):
-        if subprocess.mswindows:
+        if sys.platform == 'win32':
             from msvcrt import get_osfhandle
             from win32pipe import PeekNamedPipe
             handle = get_osfhandle(self.proc.stdout.fileno())
@@ -784,7 +793,7 @@ class LocalGitClient(GitClient):
         :param determine_wants: Optional function to determine what refs
             to fetch
         :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
         with closing(Repo(path)) as r:
@@ -799,6 +808,7 @@ class LocalGitClient(GitClient):
         :param graph_walker: Object with next() and ack().
         :param pack_data: Callback called for each bit of data in the pack
         :param progress: Callback for progress reports (strings)
+        :return: Dictionary with all remote refs (not just those fetched)
         """
         from dulwich.repo import Repo
         with closing(Repo(path)) as r:
@@ -850,8 +860,7 @@ 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])):
+        if not isinstance(command, bytes):
             raise TypeError(command)
 
         #FIXME: This has no way to deal with passwords..
@@ -861,7 +870,7 @@ class SubprocessSSHVendor(SSHVendor):
         if username is not None:
             host = '%s@%s' % (username, host)
         args.append(host)
-        proc = subprocess.Popen(args + command,
+        proc = subprocess.Popen(args + [command],
                                 stdin=subprocess.PIPE,
                                 stdout=subprocess.PIPE)
         return SubprocessWrapper(proc)
@@ -896,11 +905,7 @@ class SSHGitClient(TraditionalGitClient):
     def _get_cmd_path(self, 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'))]
+        return cmd
 
     def _connect(self, cmd, path):
         if type(cmd) is not bytes:
@@ -909,7 +914,7 @@ class SSHGitClient(TraditionalGitClient):
             raise TypeError(path)
         if path.startswith(b"/~"):
             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(
             self.host, argv, port=self.port, username=self.username)
         return (Protocol(con.read, con.write, con.close,
@@ -1057,7 +1062,7 @@ class HttpGitClient(GitClient):
         :param graph_walker: Object with next() and ack().
         :param pack_data: Callback called for each bit of data in the pack
         :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)
         refs, server_capabilities = self._discover_references(

+ 2 - 47
dulwich/contrib/paramiko_vendor.py

@@ -108,40 +108,6 @@ class _ParamikoWrapper(object):
         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):
 
     def __init__(self):
@@ -149,8 +115,7 @@ class ParamikoSSHVendor(object):
 
     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])):
+        if not isinstance(command, bytes):
             raise TypeError(command)
         # Paramiko needs an explicit port. None is not valid
         if port is None:
@@ -166,18 +131,8 @@ class ParamikoSSHVendor(object):
         # Open SSH 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
-        channel.exec_command(quoted_command)
+        channel.exec_command(command)
 
         return _ParamikoWrapper(
             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
     """
     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:
             return ref
     else:
@@ -133,4 +141,21 @@ def parse_commit_range(repo, committishs):
     :raise ValueError: If the range can not be parsed
     """
     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 time
 
+from dulwich.archive import (
+    tar_stream,
+    )
 from dulwich.client import (
     get_transport_and_path,
-    SubprocessGitClient,
     )
 from dulwich.errors import (
     SendPackError,
@@ -95,6 +97,10 @@ from dulwich.server import (
 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):
     """Encode a path as bytestring."""
     # TODO(jelmer): Use something other than ascii?
@@ -126,25 +132,24 @@ def open_repo_closing(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):
     """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 outstream: Output stream (defaults to stdout)
     :param errstream: Error stream (defaults to stderr)
     """
 
-    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,
-                   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="."):
@@ -215,7 +220,7 @@ def init(path=".", bare=False):
         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.
 
     :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)
         if base is None:
             base = b''
+        else:
+            base = base.rstrip(b'/')
         for key in keys:
             try:
                 ret[key] = self[(base + b'/' + key).strip(b'/')]

+ 24 - 7
dulwich/repo.py

@@ -82,6 +82,7 @@ from dulwich.refs import (
 import warnings
 
 
+CONTROLDIR = '.git'
 OBJECTDIR = 'objects'
 REFSDIR = 'refs'
 REFSDIR_TAGS = 'tags'
@@ -631,6 +632,21 @@ class BaseRepo(object):
         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):
     """A git repository backed by local disk.
 
@@ -641,17 +657,18 @@ class Repo(BaseRepo):
     """
 
     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._controldir = os.path.join(root, ".git")
+            self._controldir = hidden_path
         elif (os.path.isdir(os.path.join(root, OBJECTDIR)) and
               os.path.isdir(os.path.join(root, REFSDIR))):
             self.bare = True
             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._controldir = os.path.join(root, path)
         else:
@@ -910,7 +927,7 @@ class Repo(BaseRepo):
         """
         if mkdir:
             os.mkdir(path)
-        controldir = os.path.join(path, ".git")
+        controldir = os.path.join(path, CONTROLDIR)
         os.mkdir(controldir)
         cls._init_maybe_bare(controldir, False)
         return cls(path)

+ 28 - 4
dulwich/server.py

@@ -207,6 +207,16 @@ class Handler(object):
         self.backend = backend
         self.proto = proto
         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
         # Flags needed for the no-done capability
         self._done_received = False
@@ -254,12 +264,14 @@ class Handler(object):
         self._done_received = True
 
 
-class UploadPackHandler(Handler):
+
+class UploadPackHandler(PackHandler):
     """Protocol handler for uploading a pack to the server."""
 
     def __init__(self, backend, args, proto, http_req=None,
                  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._graph_walker = None
         self.advertise_refs = advertise_refs
@@ -829,12 +841,13 @@ class MultiAckDetailedGraphWalkerImpl(object):
         return True
 
 
-class ReceivePackHandler(Handler):
+class ReceivePackHandler(PackHandler):
     """Protocol handler for downloading a pack from the client."""
 
     def __init__(self, backend, args, proto, http_req=None,
                  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.advertise_refs = advertise_refs
 
@@ -958,10 +971,21 @@ class ReceivePackHandler(Handler):
             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_HANDLERS = {
   b'git-upload-pack': UploadPackHandler,
   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():
     names = [
+        'archive',
         'blackbox',
         'client',
         'config',
@@ -134,6 +135,7 @@ def self_test_suite():
         'patch',
         'porcelain',
         'protocol',
+        'reflog',
         'refs',
         'repository',
         'server',

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

@@ -324,8 +324,9 @@ class TestSSHVendor(object):
 
     @staticmethod
     def run_command(host, command, username=None, port=None):
-        cmd, path = command
+        cmd, path = command.split(b' ')
         cmd = cmd.split(b'-', 1)
+        path = path.replace(b"'", b"")
         p = subprocess.Popen(cmd + [path], bufsize=0, env=get_safe_env(), stdin=subprocess.PIPE,
                              stdout=subprocess.PIPE, stderr=subprocess.PIPE)
         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['LC_ALL'] = env['LANG'] = 'C'
 
     args = [git_path] + args
     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,
     TraditionalGitClient,
     TCPGitClient,
-    SubprocessGitClient,
     SSHGitClient,
     HttpGitClient,
     ReportStatusParser,
@@ -502,8 +501,7 @@ 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])):
+        if not isinstance(command, bytes):
             raise TypeError(command)
 
         self.host = host
@@ -535,19 +533,19 @@ class SSHGitClientTests(TestCase):
         client.get_ssh_vendor = self.real_vendor
 
     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'))
 
     def test_alternative_command_path(self):
         self.client.alternative_paths[b'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'))
 
     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.assertEqual(b"/usr/lib/git/git-upload-pack -ibla",
             self.client._get_cmd_path(b'upload-pack'))
 
     def test_connect(self):
@@ -560,10 +558,10 @@ class SSHGitClientTests(TestCase):
         client._connect(b"command", b"/path/to/repo")
         self.assertEqual(b"username", server.username)
         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")
-        self.assertEqual([b"git-relative-command", b"~/path/to/repo"],
+        self.assertEqual(b"git-relative-command '~/path/to/repo'",
                           server.command)
 
 

+ 47 - 5
dulwich/tests/test_objectspec.py

@@ -71,18 +71,60 @@ class ParseCommitRangeTests(TestCase):
 
 
 class ParseRefTests(TestCase):
-
     def test_nonexistent(self):
         r = {}
         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"}
-        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"}
-        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):

+ 0 - 3
dulwich/tests/test_porcelain.py

@@ -40,7 +40,6 @@ from dulwich.repo import Repo
 from dulwich.tests import (
     TestCase,
     )
-from dulwich.tests.compat.utils import require_git_version
 from dulwich.tests.utils import (
     build_commit_graph,
     make_object,
@@ -64,8 +63,6 @@ class ArchiveTests(PorcelainTestCase):
     """Tests for the archive command."""
 
     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]])
         self.repo.refs[b"refs/heads/master"] = c3.id
         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:
             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:
-            self.assertRaisesRegexp(
+            assertRaisesRegexp(
                 TypeError, "'name' must be bytestring, not int",
                 r.__getitem__, 12
             )
@@ -502,6 +507,19 @@ exit 1
         self.assertTrue("post-commit hook failed: " in str(warnings_list[-1]))
         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):
     """Tests that build on-disk repos from scratch.

+ 4 - 4
dulwich/tests/test_server.py

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

+ 1 - 1
setup.py

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