浏览代码

Import upstream version 0.20.30

Jelmer Vernooij 3 年之前
父节点
当前提交
0078a59e44

+ 1 - 0
.github/FUNDING.yml

@@ -0,0 +1 @@
+github: jelmer

+ 22 - 0
NEWS

@@ -1,3 +1,25 @@
+0.20.30	2022-01-08
+
+0.20.29	2022-01-08
+
+ * Support staging submodules.
+   (Jelmer Vernooij)
+
+ * Drop deprecated Index.iterblobs and iter_fresh_blobs.
+   (Jelmer Vernooij)
+
+ * Unify clone behaviour of ``Repo.clone`` and
+   ``porcelain.clone``, and add branch parameter for
+   clone. (Peter Rowlands, #851)
+
+0.20.28	2022-01-05
+
+ * Fix hook test on Mac OSX / Linux when dulwich is
+   not installed system-wide. (Jelmer Vernooij, #919)
+
+ * Cope with gecos being unset.
+   (Jelmer Vernooij, #917)
+
 0.20.27	2022-01-04
 
  * Allow adding files to repository in pre-commit hook.

+ 1 - 1
PKG-INFO

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

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

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

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

@@ -23,6 +23,7 @@ setup.cfg
 setup.py
 status.yaml
 tox.ini
+.github/FUNDING.yml
 .github/workflows/pythonpackage.yml
 .github/workflows/pythonpublish.yml
 bin/dul-receive-pack

+ 1 - 1
dulwich/__init__.py

@@ -22,4 +22,4 @@
 
 """Python implementation of the Git file formats and protocols."""
 
-__version__ = (0, 20, 27)
+__version__ = (0, 20, 30)

+ 10 - 34
dulwich/index.py

@@ -379,12 +379,6 @@ class Index(object):
             entry = self[path]
             yield path, entry.sha, cleanup_mode(entry.mode)
 
-    def iterblobs(self):
-        import warnings
-
-        warnings.warn("Use iterobjects() instead.", PendingDeprecationWarning)
-        return self.iterobjects()
-
     def clear(self):
         """Remove all contents from this index."""
         self._byname = {}
@@ -878,6 +872,15 @@ def _fs_to_tree_path(fs_path):
     return tree_path
 
 
+def index_entry_from_directory(st, path):
+    if os.path.exists(os.path.join(path, b".git")):
+        head = read_submodule_head(path)
+        if head is None:
+            return None
+        return index_entry_from_stat(st, head, 0, mode=S_IFGITLINK)
+    return None
+
+
 def index_entry_from_path(path, object_store=None):
     """Create an index from a filesystem path.
 
@@ -894,12 +897,7 @@ def index_entry_from_path(path, object_store=None):
     assert isinstance(path, bytes)
     st = os.lstat(path)
     if stat.S_ISDIR(st.st_mode):
-        if os.path.exists(os.path.join(path, b".git")):
-            head = read_submodule_head(path)
-            if head is None:
-                return None
-            return index_entry_from_stat(st, head, 0, mode=S_IFGITLINK)
-        return None
+        return index_entry_from_directory(st, path)
 
     if stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode):
         blob = blob_from_path_and_stat(path, st)
@@ -930,28 +928,6 @@ def iter_fresh_entries(
         yield path, entry
 
 
-def iter_fresh_blobs(index, root_path):
-    """Iterate over versions of blobs on disk referenced by index.
-
-    Don't use this function; it removes missing entries from index.
-
-    Args:
-      index: Index file
-      root_path: Root path to access from
-      include_deleted: Include deleted entries with sha and
-        mode set to None
-    Returns: Iterator over path, sha, mode
-    """
-    import warnings
-
-    warnings.warn(PendingDeprecationWarning, "Use iter_fresh_objects instead.")
-    for entry in iter_fresh_objects(index, root_path, include_deleted=True):
-        if entry[1] is None:
-            del index[entry[0]]
-        else:
-            yield entry
-
-
 def iter_fresh_objects(paths, root_path, include_deleted=False, object_store=None):
     """Iterate over versions of objecs on disk referenced by index.
 

+ 15 - 47
dulwich/porcelain.py

@@ -70,7 +70,6 @@ import datetime
 import os
 from pathlib import Path
 import posixpath
-import shutil
 import stat
 import sys
 import time
@@ -140,6 +139,7 @@ from dulwich.protocol import (
 from dulwich.refs import (
     ANNOTATED_TAG_SUFFIX,
     LOCAL_BRANCH_PREFIX,
+    LOCAL_TAG_PREFIX,
     strip_peeled_refs,
     RefsContainer,
 )
@@ -403,6 +403,7 @@ def clone(
     outstream=None,
     origin=b"origin",
     depth=None,
+    branch=None,
     **kwargs
 ):
     """Clone a local or remote git repository.
@@ -416,9 +417,10 @@ def clone(
       outstream: Optional stream to write progress to (deprecated)
       origin: Name of remote from the repository used to clone
       depth: Depth to fetch at
+      branch: Optional branch or tag to be used as HEAD in the new repository
+        instead of the cloned repository's HEAD.
     Returns: The new repository
     """
-    # TODO(jelmer): This code overlaps quite a bit with Repo.clone
     if outstream is not None:
         import warnings
 
@@ -427,7 +429,7 @@ def clone(
             DeprecationWarning,
             stacklevel=3,
         )
-        errstream = outstream
+        # TODO(jelmer): Capture logging output and stream to errstream
 
     if checkout is None:
         checkout = not bare
@@ -437,51 +439,17 @@ def clone(
     if target is None:
         target = source.split("/")[-1]
 
-    if not os.path.exists(target):
-        os.mkdir(target)
+    mkdir = not os.path.exists(target)
 
-    if bare:
-        r = Repo.init_bare(target)
-    else:
-        r = Repo.init(target)
-
-    reflog_message = b"clone: from " + source.encode("utf-8")
-    try:
-        target_config = r.get_config()
-        if not isinstance(source, bytes):
-            source = source.encode(DEFAULT_ENCODING)
-        target_config.set((b"remote", origin), b"url", source)
-        target_config.set(
-            (b"remote", origin),
-            b"fetch",
-            b"+refs/heads/*:refs/remotes/" + origin + b"/*",
-        )
-        target_config.write_to_path()
-        fetch_result = fetch(
-            r,
-            origin,
-            errstream=errstream,
-            message=reflog_message,
-            depth=depth,
-            **kwargs
+    with open_repo_closing(source) as r:
+        return r.clone(
+            target,
+            mkdir=mkdir,
+            bare=bare,
+            origin=origin,
+            checkout=checkout,
+            branch=branch,
         )
-        for key, target in fetch_result.symrefs.items():
-            r.refs.set_symbolic_ref(key, target)
-        try:
-            head = r[fetch_result.refs[b"HEAD"]]
-        except KeyError:
-            head = None
-        else:
-            r[b"HEAD"] = head.id
-        if checkout and not bare and head is not None:
-            errstream.write(b"Checking out " + head.id + b"\n")
-            r.reset_index(head.tree)
-    except BaseException:
-        shutil.rmtree(target)
-        r.close()
-        raise
-
-    return r
 
 
 def add(repo=".", paths=None):
@@ -1430,7 +1398,7 @@ def _make_branch_ref(name):
 def _make_tag_ref(name):
     if getattr(name, "encode", None):
         name = name.encode(DEFAULT_ENCODING)
-    return b"refs/tags/" + name
+    return LOCAL_TAG_PREFIX + name
 
 
 def branch_delete(repo, name):

+ 66 - 0
dulwich/refs.py

@@ -32,6 +32,7 @@ from dulwich.objects import (
     git_line,
     valid_hexsha,
     ZERO_SHA,
+    Tag,
 )
 from dulwich.file import (
     GitFile,
@@ -1203,3 +1204,68 @@ def strip_peeled_refs(refs):
         for (ref, sha) in refs.items()
         if not ref.endswith(ANNOTATED_TAG_SUFFIX)
     }
+
+
+def _set_origin_head(refs, origin, origin_head):
+    # set refs/remotes/origin/HEAD
+    origin_base = b"refs/remotes/" + origin + b"/"
+    if origin_head and origin_head.startswith(LOCAL_BRANCH_PREFIX):
+        origin_ref = origin_base + b"HEAD"
+        target_ref = origin_base + origin_head[len(LOCAL_BRANCH_PREFIX) :]
+        if target_ref in refs:
+            refs.set_symbolic_ref(origin_ref, target_ref)
+
+
+def _set_default_branch(refs, origin, origin_head, branch, ref_message):
+    origin_base = b"refs/remotes/" + origin + b"/"
+    if branch:
+        origin_ref = origin_base + branch
+        if origin_ref in refs:
+            local_ref = LOCAL_BRANCH_PREFIX + branch
+            refs.add_if_new(
+                local_ref, refs[origin_ref], ref_message
+            )
+            head_ref = local_ref
+        elif LOCAL_TAG_PREFIX + branch in refs:
+            head_ref = LOCAL_TAG_PREFIX + branch
+        else:
+            raise ValueError(
+                "%s is not a valid branch or tag" % os.fsencode(branch)
+            )
+    elif origin_head:
+        head_ref = origin_head
+        if origin_head.startswith(LOCAL_BRANCH_PREFIX):
+            origin_ref = origin_base + origin_head[len(LOCAL_BRANCH_PREFIX) :]
+        else:
+            origin_ref = origin_head
+        try:
+            refs.add_if_new(
+                head_ref, refs[origin_ref], ref_message
+            )
+        except KeyError:
+            pass
+    return head_ref
+
+
+def _set_head(refs, head_ref, ref_message):
+    if head_ref.startswith(LOCAL_TAG_PREFIX):
+        # detach HEAD at specified tag
+        head = refs[head_ref]
+        if isinstance(head, Tag):
+            _cls, obj = head.object
+            head = obj.get_object(obj).id
+        del refs[b"HEAD"]
+        refs.set_if_equals(
+            b"HEAD", None, head, message=ref_message
+        )
+    else:
+        # set HEAD to specific branch
+        try:
+            head = refs[head_ref]
+            refs.set_symbolic_ref(b"HEAD", head_ref)
+            refs.set_if_equals(
+                b"HEAD", None, head, message=ref_message
+            )
+        except KeyError:
+            head = None
+    return head

+ 88 - 43
dulwich/repo.py

@@ -87,6 +87,8 @@ from dulwich.line_ending import BlobNormalizer, TreeBlobNormalizer
 
 from dulwich.refs import (  # noqa: F401
     ANNOTATED_TAG_SUFFIX,
+    LOCAL_BRANCH_PREFIX,
+    LOCAL_TAG_PREFIX,
     check_ref_format,
     RefsContainer,
     DictRefsContainer,
@@ -96,6 +98,9 @@ from dulwich.refs import (  # noqa: F401
     read_packed_refs_with_peeled,
     write_packed_refs,
     SYMREF,
+    _set_default_branch,
+    _set_head,
+    _set_origin_head,
 )
 
 
@@ -146,7 +151,10 @@ def _get_default_identity() -> Tuple[str, str]:
         except KeyError:
             fullname = None
         else:
-            fullname = gecos.split(",")[0]
+            if gecos:
+                fullname = gecos.split(",")[0]
+            else:
+                fullname = None
     if not fullname:
         fullname = username
     email = os.environ.get("EMAIL")
@@ -1268,6 +1276,7 @@ class Repo(BaseRepo):
         from dulwich.index import (
             blob_from_path_and_stat,
             index_entry_from_stat,
+            index_entry_from_directory,
             _fs_to_tree_path,
         )
 
@@ -1292,7 +1301,16 @@ class Repo(BaseRepo):
                 except KeyError:
                     pass  # already removed
             else:
-                if not stat.S_ISREG(st.st_mode) and not stat.S_ISLNK(st.st_mode):
+                if stat.S_ISDIR(st.st_mode):
+                    entry = index_entry_from_directory(st, full_path)
+                    if entry:
+                        index[tree_path] = entry
+                    else:
+                        try:
+                            del index[tree_path]
+                        except KeyError:
+                            pass
+                elif not stat.S_ISREG(st.st_mode) and not stat.S_ISLNK(st.st_mode):
                     try:
                         del index[tree_path]
                     except KeyError:
@@ -1370,6 +1388,7 @@ class Repo(BaseRepo):
         bare=False,
         origin=b"origin",
         checkout=None,
+        branch=None,
     ):
         """Clone this repository.
 
@@ -1377,56 +1396,78 @@ class Repo(BaseRepo):
           target_path: Target path
           mkdir: Create the target directory
           bare: Whether to create a bare repository
+          checkout: Whether or not to check-out HEAD after cloning
           origin: Base name for refs in target repository
             cloned from this repository
+          branch: Optional branch or tag to be used as HEAD in the new repository
+            instead of this repository's HEAD.
         Returns: Created repository as `Repo`
         """
-        if not bare:
-            target = self.init(target_path, mkdir=mkdir)
-        else:
-            if checkout:
-                raise ValueError("checkout and bare are incompatible")
-            target = self.init_bare(target_path, mkdir=mkdir)
-        self.fetch(target)
+
         encoded_path = self.path
         if not isinstance(encoded_path, bytes):
             encoded_path = os.fsencode(encoded_path)
-        ref_message = b"clone: from " + encoded_path
-        target.refs.import_refs(
-            b"refs/remotes/" + origin,
-            self.refs.as_dict(b"refs/heads"),
-            message=ref_message,
-        )
-        target.refs.import_refs(
-            b"refs/tags", self.refs.as_dict(b"refs/tags"), message=ref_message
-        )
+
+        if mkdir:
+            os.mkdir(target_path)
+
         try:
-            target.refs.add_if_new(
-                DEFAULT_REF, self.refs[DEFAULT_REF], message=ref_message
+            target = None
+            if not bare:
+                target = Repo.init(target_path)
+                if checkout is None:
+                    checkout = True
+            else:
+                if checkout:
+                    raise ValueError("checkout and bare are incompatible")
+                target = Repo.init_bare(target_path)
+
+            target_config = target.get_config()
+            target_config.set((b"remote", origin), b"url", encoded_path)
+            target_config.set(
+                (b"remote", origin),
+                b"fetch",
+                b"+refs/heads/*:refs/remotes/" + origin + b"/*",
             )
-        except KeyError:
-            pass
-        target_config = target.get_config()
-        target_config.set(("remote", "origin"), "url", encoded_path)
-        target_config.set(
-            ("remote", "origin"),
-            "fetch",
-            "+refs/heads/*:refs/remotes/origin/*",
-        )
-        target_config.write_to_path()
-
-        # Update target head
-        head_chain, head_sha = self.refs.follow(b"HEAD")
-        if head_chain and head_sha is not None:
-            target.refs.set_symbolic_ref(b"HEAD", head_chain[-1], message=ref_message)
-            target[b"HEAD"] = head_sha
-
-            if checkout is None:
-                checkout = not bare
-            if checkout:
-                # Checkout HEAD to target dir
-                target.reset_index()
+            target_config.write_to_path()
+
+            ref_message = b"clone: from " + encoded_path
+            self.fetch(target)
+            target.refs.import_refs(
+                b"refs/remotes/" + origin,
+                self.refs.as_dict(b"refs/heads"),
+                message=ref_message,
+            )
+            target.refs.import_refs(
+                b"refs/tags", self.refs.as_dict(b"refs/tags"), message=ref_message
+            )
+
+            head_chain, origin_sha = self.refs.follow(b"HEAD")
+            origin_head = head_chain[-1] if head_chain else None
+            if origin_sha and not origin_head:
+                # set detached HEAD
+                target.refs[b"HEAD"] = origin_sha
 
+            _set_origin_head(target.refs, origin, origin_head)
+            head_ref = _set_default_branch(
+                target.refs, origin, origin_head, branch, ref_message
+            )
+
+            # Update target head
+            if head_ref:
+                head = _set_head(target.refs, head_ref, ref_message)
+            else:
+                head = None
+
+            if checkout and head is not None:
+                target.reset_index()
+        except BaseException:
+            if target is not None:
+                target.close()
+            if mkdir:
+                import shutil
+                shutil.rmtree(target_path)
+            raise
         return target
 
     def reset_index(self, tree=None):
@@ -1442,7 +1483,11 @@ class Repo(BaseRepo):
         )
 
         if tree is None:
-            tree = self[b"HEAD"].tree
+            head = self[b"HEAD"]
+            if isinstance(head, Tag):
+                _cls, obj = head.object
+                head = self.get_object(obj)
+            tree = head.tree
         config = self.get_config()
         honor_filemode = config.get_boolean(b"core", b"filemode", os.name != "nt")
         if config.get_boolean(b"core", b"core.protectNTFS", os.name == "nt"):

+ 0 - 24
dulwich/tests/test_index.py

@@ -30,7 +30,6 @@ import stat
 import struct
 import sys
 import tempfile
-import warnings
 
 from dulwich.index import (
     Index,
@@ -64,9 +63,6 @@ from dulwich.tests import (
     TestCase,
     skipIf,
 )
-from dulwich.tests.utils import (
-    setup_warning_catcher,
-)
 
 
 def can_symlink():
@@ -109,26 +105,6 @@ class SimpleIndexTestCase(IndexTestCase):
             list(self.get_simple_index("index").iterobjects()),
         )
 
-    def test_iterblobs(self):
-        warnings.simplefilter("always", UserWarning)
-        self.addCleanup(warnings.resetwarnings)
-        warnings_list, restore_warnings = setup_warning_catcher()
-        self.addCleanup(restore_warnings)
-
-        self.assertEqual(
-            [(b"bla", b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", 33188)],
-            list(self.get_simple_index("index").iterblobs()),
-        )
-
-        expected_warning = PendingDeprecationWarning("Use iterobjects() instead.")
-        for w in warnings_list:
-            if type(w) == type(expected_warning) and w.args == expected_warning.args:
-                break
-        else:
-            raise AssertionError(
-                "Expected warning %r not in %r" % (expected_warning, warnings_list)
-            )
-
     def test_getitem(self):
         self.assertEqual(
             (

+ 12 - 4
dulwich/tests/test_porcelain.py

@@ -630,9 +630,12 @@ class CloneTests(PorcelainTestCase):
         r.close()
 
     def test_source_broken(self):
-        target_path = tempfile.mkdtemp()
-        self.assertRaises(Exception, porcelain.clone, "/nonexistant/repo", target_path)
-        self.assertFalse(os.path.exists(target_path))
+        with tempfile.TemporaryDirectory() as parent:
+            target_path = os.path.join(parent, "target")
+            self.assertRaises(
+                Exception, porcelain.clone, "/nonexistant/repo", target_path
+            )
+            self.assertFalse(os.path.exists(target_path))
 
     def test_fetch_symref(self):
         f1_1 = make_object(Blob, data=b"f1")
@@ -652,7 +655,10 @@ class CloneTests(PorcelainTestCase):
         self.assertEqual(0, len(target_repo.open_index()))
         self.assertEqual(c1.id, target_repo.refs[b"refs/heads/else"])
         self.assertEqual(c1.id, target_repo.refs[b"HEAD"])
-        self.assertEqual({b"HEAD": b"refs/heads/else"}, target_repo.refs.get_symrefs())
+        self.assertEqual(
+            {b"HEAD": b"refs/heads/else", b"refs/remotes/origin/HEAD": b"refs/remotes/origin/else"},
+            target_repo.refs.get_symrefs(),
+        )
 
 
 class InitTests(TestCase):
@@ -2385,6 +2391,8 @@ class FetchTests(PorcelainTestCase):
             for k, v in remote_refs.items()
             if k.startswith(local_ref_prefix)
         }
+        if b"HEAD" in locally_known_remote_refs and b"HEAD" in remote_refs:
+            normalized_remote_refs[b"HEAD"] = remote_refs[b"HEAD"]
 
         self.assertEqual(locally_known_remote_refs, normalized_remote_refs)
 

+ 55 - 2
dulwich/tests/test_repository.py

@@ -385,6 +385,7 @@ class RepositoryRootTests(TestCase):
                 {
                     b"HEAD": b"a90fa2d900a17e99b433217e988c4eb4a2e9a097",
                     b"refs/remotes/origin/master": b"a90fa2d900a17e99b433217e988c4eb4a2e9a097",
+                    b"refs/remotes/origin/HEAD": b"a90fa2d900a17e99b433217e988c4eb4a2e9a097",
                     b"refs/heads/master": b"a90fa2d900a17e99b433217e988c4eb4a2e9a097",
                     b"refs/tags/mytag": b"28237f4dc30d0d462658d6b937b08a0f0b6ef55a",
                     b"refs/tags/mytag-packed": b"b0931cadc54336e78a1d980420e3268903b57a50",
@@ -451,6 +452,48 @@ class RepositoryRootTests(TestCase):
             ValueError, r.clone, tmp_dir, mkdir=False, checkout=True, bare=True
         )
 
+    def test_clone_branch(self):
+        r = self.open_repo("a.git")
+        r.refs[b"refs/heads/mybranch"] = b"28237f4dc30d0d462658d6b937b08a0f0b6ef55a"
+        tmp_dir = self.mkdtemp()
+        self.addCleanup(shutil.rmtree, tmp_dir)
+        with r.clone(tmp_dir, mkdir=False, branch=b"mybranch") as t:
+            # HEAD should point to specified branch and not origin HEAD
+            chain, sha = t.refs.follow(b"HEAD")
+            self.assertEqual(chain[-1], b"refs/heads/mybranch")
+            self.assertEqual(sha, b"28237f4dc30d0d462658d6b937b08a0f0b6ef55a")
+            self.assertEqual(
+                t.refs[b"refs/remotes/origin/HEAD"],
+                b"a90fa2d900a17e99b433217e988c4eb4a2e9a097",
+            )
+
+    def test_clone_tag(self):
+        r = self.open_repo("a.git")
+        tmp_dir = self.mkdtemp()
+        self.addCleanup(shutil.rmtree, tmp_dir)
+        with r.clone(tmp_dir, mkdir=False, branch=b"mytag") as t:
+            # HEAD should be detached (and not a symbolic ref) at tag
+            self.assertEqual(
+                t.refs.read_ref(b"HEAD"),
+                b"28237f4dc30d0d462658d6b937b08a0f0b6ef55a",
+            )
+            self.assertEqual(
+                t.refs[b"refs/remotes/origin/HEAD"],
+                b"a90fa2d900a17e99b433217e988c4eb4a2e9a097",
+            )
+
+    def test_clone_invalid_branch(self):
+        r = self.open_repo("a.git")
+        tmp_dir = self.mkdtemp()
+        self.addCleanup(shutil.rmtree, tmp_dir)
+        self.assertRaises(
+            ValueError,
+            r.clone,
+            tmp_dir,
+            mkdir=False,
+            branch=b"mybranch",
+        )
+
     def test_merge_history(self):
         r = self.open_repo("simple_merge.git")
         shas = [e.commit.id for e in r.get_walker()]
@@ -648,7 +691,7 @@ exit 0
 
         pre_commit_contents = """#!%(executable)s
 import sys
-sys.path.extend(':'.join(%(path)s))
+sys.path.extend(%(path)r)
 from dulwich.repo import Repo
 
 with open('foo', 'w') as f:
@@ -656,7 +699,9 @@ with open('foo', 'w') as f:
 
 r = Repo('.')
 r.stage(['foo'])
-""" % {'executable': sys.executable, 'path': repr(sys.path)}
+""" % {
+            'executable': sys.executable,
+            'path': [os.path.join(os.path.dirname(__file__), '..', '..')] + sys.path}
 
         repo_dir = os.path.join(self.mkdtemp())
         self.addCleanup(shutil.rmtree, repo_dir)
@@ -1266,6 +1311,7 @@ class BuildRepoRootTests(TestCase):
         os.remove(os.path.join(r.path, "a"))
         r.stage(["a"])
         r.stage(["a"])  # double-stage a deleted path
+        self.assertEqual([], list(r.open_index()))
 
     def test_stage_directory(self):
         r = self._repo
@@ -1273,6 +1319,13 @@ class BuildRepoRootTests(TestCase):
         r.stage(["c"])
         self.assertEqual([b"a"], list(r.open_index()))
 
+    def test_stage_submodule(self):
+        r = self._repo
+        s = Repo.init(os.path.join(r.path, "sub"), mkdir=True)
+        s.do_commit(b'message')
+        r.stage(["sub"])
+        self.assertEqual([b"a", b"sub"], list(r.open_index()))
+
     def test_unstage_midify_file_with_dir(self):
         os.mkdir(os.path.join(self._repo.path, 'new_dir'))
         full_path = os.path.join(self._repo.path, 'new_dir', 'foo')

+ 1 - 1
releaser.conf

@@ -2,7 +2,7 @@
 news_file: "NEWS"
 timeout_days: 5
 tag_name: "dulwich-$VERSION"
-verify_command: "make check"
+verify_command: "flake8 && make check"
 update_version {
   path: "setup.py"
   match: "^dulwich_version_string = '(.*)'$"

+ 1 - 1
setup.py

@@ -23,7 +23,7 @@ if sys.version_info < (3, 6):
         'For 2.7 support, please install a version prior to 0.20')
 
 
-dulwich_version_string = '0.20.27'
+dulwich_version_string = '0.20.30'
 
 
 class DulwichDistribution(Distribution):