Przeglądaj źródła

Import upstream version 0.20.35

Jelmer Vernooij 3 lat temu
rodzic
commit
93095ab357
51 zmienionych plików z 628 dodań i 273 usunięć
  1. 1 1
      .github/workflows/pythonpackage.yml
  2. 0 2
      MANIFEST.in
  3. 5 0
      Makefile
  4. 25 0
      NEWS
  5. 1 1
      PKG-INFO
  6. 1 1
      dulwich.egg-info/PKG-INFO
  7. 1 0
      dulwich.egg-info/SOURCES.txt
  8. 1 1
      dulwich/__init__.py
  9. 8 8
      dulwich/cli.py
  10. 6 4
      dulwich/client.py
  11. 93 19
      dulwich/config.py
  12. 6 10
      dulwich/contrib/diffstat.py
  13. 145 0
      dulwich/contrib/requests_vendor.py
  14. 3 3
      dulwich/diff_tree.py
  15. 2 2
      dulwich/fastexport.py
  16. 0 5
      dulwich/hooks.py
  17. 3 0
      dulwich/ignore.py
  18. 29 3
      dulwich/index.py
  19. 28 28
      dulwich/line_ending.py
  20. 4 3
      dulwich/object_store.py
  21. 1 1
      dulwich/objects.py
  22. 3 3
      dulwich/objectspec.py
  23. 11 12
      dulwich/pack.py
  24. 16 8
      dulwich/porcelain.py
  25. 2 2
      dulwich/refs.py
  26. 21 8
      dulwich/repo.py
  27. 1 1
      dulwich/server.py
  28. 6 6
      dulwich/tests/__init__.py
  29. 1 1
      dulwich/tests/compat/test_client.py
  30. 26 0
      dulwich/tests/compat/test_repository.py
  31. 2 2
      dulwich/tests/compat/test_server.py
  32. 33 32
      dulwich/tests/test_client.py
  33. 17 6
      dulwich/tests/test_config.py
  34. 2 2
      dulwich/tests/test_fastexport.py
  35. 1 1
      dulwich/tests/test_file.py
  36. 11 2
      dulwich/tests/test_ignore.py
  37. 8 8
      dulwich/tests/test_lru_cache.py
  38. 4 3
      dulwich/tests/test_missing_obj_finder.py
  39. 7 7
      dulwich/tests/test_object_store.py
  40. 12 12
      dulwich/tests/test_objects.py
  41. 10 9
      dulwich/tests/test_pack.py
  42. 6 6
      dulwich/tests/test_patch.py
  43. 36 24
      dulwich/tests/test_porcelain.py
  44. 8 8
      dulwich/tests/test_refs.py
  45. 7 7
      dulwich/tests/test_repository.py
  46. 1 1
      dulwich/tests/test_server.py
  47. 4 4
      dulwich/tests/test_utils.py
  48. 3 3
      dulwich/tests/test_web.py
  49. 4 2
      dulwich/web.py
  50. 1 0
      releaser.conf
  51. 1 1
      setup.py

+ 1 - 1
.github/workflows/pythonpackage.yml

@@ -44,7 +44,7 @@ jobs:
       if: "matrix.os != 'windows-latest' && matrix.python-version != 'pypy3'"
     - name: Install mypy
       run: |
-        pip install -U mypy types-paramiko types-certifi
+        pip install -U mypy types-paramiko types-certifi types-requests
       if: "matrix.python-version != 'pypy3'"
     - name: Style checks
       run: |

+ 0 - 2
MANIFEST.in

@@ -14,6 +14,4 @@ recursive-include examples *.py
 graft dulwich/tests/data
 include tox.ini
 include dulwich.cfg
-include appveyor.yml
 include .testr.conf
-include .travis.yml

+ 5 - 0
Makefile

@@ -67,3 +67,8 @@ coverage:
 
 coverage-html: coverage
 	$(COVERAGE) html
+
+.PHONY: apidocs
+
+apidocs:
+	pydoctor --docformat=google dulwich --project-url=https://www.dulwich.io/

+ 25 - 0
NEWS

@@ -1,3 +1,28 @@
+0.20.35	2022-03-20
+
+ * Document the ``path`` attribute for ``Repo``.
+   (Jelmer Vernooij, #854)
+
+0.20.34	2022-03-14
+
+ * Add support for multivars in configuration.
+   (Jelmer Vernooij, #718)
+
+0.20.33	2022-03-05
+
+ * Fix handling of escaped characters in ignore patterns.
+   (Jelmer Vernooij, #930)
+
+ * Add ``dulwich.contrib.requests_vendor``. (epopcon)
+
+ * Ensure git config is available in a linked working tree.
+   (Jesse Cureton, #926)
+
+0.20.32	2022-01-24
+
+ * Properly close result repository during test.
+   (Jelmer Vernooij, #928)
+
 0.20.31	2022-01-21
 
  * Add GitClient.clone(). (Jelmer Vernooij, #920)

+ 1 - 1
PKG-INFO

@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: dulwich
-Version: 0.20.31
+Version: 0.20.35
 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.31
+Version: 0.20.35
 Summary: Python Git Library
 Home-page: https://www.dulwich.io/
 Author: Jelmer Vernooij

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

@@ -115,6 +115,7 @@ dulwich/contrib/__init__.py
 dulwich/contrib/diffstat.py
 dulwich/contrib/paramiko_vendor.py
 dulwich/contrib/release_robot.py
+dulwich/contrib/requests_vendor.py
 dulwich/contrib/swift.py
 dulwich/contrib/test_paramiko_vendor.py
 dulwich/contrib/test_release_robot.py

+ 1 - 1
dulwich/__init__.py

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

+ 8 - 8
dulwich/cli.py

@@ -40,8 +40,8 @@ from dulwich import porcelain
 from dulwich.client import get_transport_and_path
 from dulwich.errors import ApplyDeltaError
 from dulwich.index import Index
+from dulwich.objectspec import parse_commit
 from dulwich.pack import Pack, sha_to_hex
-from dulwich.patch import write_tree_diff
 from dulwich.repo import Repo
 
 
@@ -172,15 +172,15 @@ class cmd_diff(Command):
     def run(self, args):
         opts, args = getopt(args, "", [])
 
-        if args == []:
-            print("Usage: dulwich diff COMMITID")
-            sys.exit(1)
-
         r = Repo(".")
-        commit_id = args[0]
-        commit = r[commit_id]
+        if args == []:
+            commit_id = b'HEAD'
+        else:
+            commit_id = args[0]
+        commit = parse_commit(r, commit_id)
         parent_commit = r[commit.parents[0]]
-        write_tree_diff(sys.stdout, r.object_store, parent_commit.tree, commit.tree)
+        porcelain.diff_tree(
+            r, parent_commit.tree, commit.tree, outstream=sys.stdout.buffer)
 
 
 class cmd_dump_pack(Command):

+ 6 - 4
dulwich/client.py

@@ -497,7 +497,7 @@ class GitClient(object):
         raise NotImplementedError(self.send_pack)
 
     def clone(self, path, target_path, mkdir: bool = True, bare=False, origin="origin",
-              checkout=None, branch=None, depth=None):
+              checkout=None, branch=None, progress=None, depth=None):
         """Clone a repository."""
         from .refs import _set_origin_head, _set_default_branch, _set_head
         from .repo import Repo
@@ -532,7 +532,7 @@ class GitClient(object):
             target_config.write_to_path()
 
             ref_message = b"clone: from " + encoded_path
-            result = self.fetch(path, target, depth=depth)
+            result = self.fetch(path, target, progress=progress, depth=depth)
             _import_remote_refs(
                 target.refs, origin, result.refs, message=ref_message)
 
@@ -1782,8 +1782,8 @@ def default_urllib3_manager(   # noqa: C901
     Honour detected proxy configurations.
 
     Args:
-      config: dulwich.config.ConfigDict` instance with Git configuration.
-      kwargs: Additional arguments for urllib3.ProxyManager
+      config: `dulwich.config.ConfigDict` instance with Git configuration.
+      override_kwargs: Additional arguments for `urllib3.ProxyManager`
 
     Returns:
       `pool_manager_cls` (defaults to `urllib3.ProxyManager`) instance for
@@ -2154,6 +2154,8 @@ class Urllib3HttpGitClient(AbstractHttpGitClient):
             basic_auth = urllib3.util.make_headers(basic_auth=credentials)
             self.pool_manager.headers.update(basic_auth)
 
+        self.config = config
+
         super(Urllib3HttpGitClient, self).__init__(
             base_url=base_url, dumb=dumb, **kwargs)
 

+ 93 - 19
dulwich/config.py

@@ -28,13 +28,10 @@ TODO:
 
 import os
 import sys
+import warnings
 
 from typing import BinaryIO, Tuple, Optional
 
-from collections import (
-    OrderedDict,
-)
-
 try:
     from collections.abc import (
         Iterable,
@@ -62,7 +59,12 @@ def lower_key(key):
     return key
 
 
-class CaseInsensitiveDict(OrderedDict):
+class CaseInsensitiveOrderedMultiDict(MutableMapping):
+
+    def __init__(self):
+        self._real = []
+        self._keyed = {}
+
     @classmethod
     def make(cls, dict_in=None):
 
@@ -82,15 +84,34 @@ class CaseInsensitiveDict(OrderedDict):
 
         return out
 
-    def __setitem__(self, key, value, **kwargs):
-        key = lower_key(key)
+    def __len__(self):
+        return len(self._keyed)
 
-        super(CaseInsensitiveDict, self).__setitem__(key, value, **kwargs)
+    def keys(self):
+        return self._keyed.keys()
 
-    def __getitem__(self, item):
-        key = lower_key(item)
+    def items(self):
+        return iter(self._real)
+
+    def __iter__(self):
+        return self._keyed.__iter__()
+
+    def values(self):
+        return self._keyed.values()
+
+    def __setitem__(self, key, value):
+        self._real.append((key, value))
+        self._keyed[lower_key(key)] = value
+
+    def __delitem__(self, key):
+        key = lower_key(key)
+        del self._keyed[key]
+        for i, (actual, unused_value) in reversed(list(enumerate(self._real))):
+            if lower_key(actual) == key:
+                del self._real[i]
 
-        return super(CaseInsensitiveDict, self).__getitem__(key)
+    def __getitem__(self, item):
+        return self._keyed[lower_key(item)]
 
     def get(self, key, default=SENTINAL):
         try:
@@ -103,6 +124,12 @@ class CaseInsensitiveDict(OrderedDict):
 
         return default
 
+    def get_all(self, key):
+        key = lower_key(key)
+        for actual, value in self._real:
+            if lower_key(actual) == key:
+                yield value
+
     def setdefault(self, key, default=SENTINAL):
         try:
             return self[key]
@@ -120,7 +147,7 @@ class Config(object):
 
         Args:
           section: Tuple with section name and optional subsection namee
-          subsection: Subsection name
+          name: Variable name
         Returns:
           Contents of the setting
         Raises:
@@ -128,6 +155,19 @@ class Config(object):
         """
         raise NotImplementedError(self.get)
 
+    def get_multivar(self, section, name):
+        """Retrieve the contents of a multivar configuration setting.
+
+        Args:
+          section: Tuple with section name and optional subsection namee
+          name: Variable name
+        Returns:
+          Contents of the setting as iterable
+        Raises:
+          KeyError: if the value is not set
+        """
+        raise NotImplementedError(self.get_multivar)
+
     def get_boolean(self, section, name, default=None):
         """Retrieve a configuration setting as boolean.
 
@@ -157,10 +197,20 @@ class Config(object):
           section: Tuple with section name and optional subsection namee
           name: Name of the configuration value, including section
             and optional subsection
-           value: value of the setting
+          value: value of the setting
         """
         raise NotImplementedError(self.set)
 
+    def items(self, section):
+        """Iterate over the configuration pairs for a specific section.
+
+        Args:
+          section: Tuple with section name and optional subsection namee
+        Returns:
+          Iterator over (name, value) pairs
+        """
+        raise NotImplementedError(self.items)
+
     def iteritems(self, section):
         """Iterate over the configuration pairs for a specific section.
 
@@ -169,14 +219,27 @@ class Config(object):
         Returns:
           Iterator over (name, value) pairs
         """
-        raise NotImplementedError(self.iteritems)
+        warnings.warn(
+            "Use %s.items instead." % type(self).__name__,
+            DeprecationWarning,
+            stacklevel=3,
+        )
+        return self.items(section)
 
     def itersections(self):
+        warnings.warn(
+            "Use %s.items instead." % type(self).__name__,
+            DeprecationWarning,
+            stacklevel=3,
+        )
+        return self.sections()
+
+    def sections(self):
         """Iterate over the sections.
 
         Returns: Iterator over section tuples
         """
-        raise NotImplementedError(self.itersections)
+        raise NotImplementedError(self.sections)
 
     def has_section(self, name):
         """Check if a specified section exists.
@@ -186,7 +249,7 @@ class Config(object):
         Returns:
           boolean indicating whether the section exists
         """
-        return name in self.itersections()
+        return name in self.sections()
 
 
 class ConfigDict(Config, MutableMapping):
@@ -197,7 +260,7 @@ class ConfigDict(Config, MutableMapping):
         if encoding is None:
             encoding = sys.getdefaultencoding()
         self.encoding = encoding
-        self._values = CaseInsensitiveDict.make(values)
+        self._values = CaseInsensitiveOrderedMultiDict.make(values)
 
     def __repr__(self):
         return "%s(%r)" % (self.__class__.__name__, self._values)
@@ -246,6 +309,17 @@ class ConfigDict(Config, MutableMapping):
 
         return section, name
 
+    def get_multivar(self, section, name):
+        section, name = self._check_section_and_name(section, name)
+
+        if len(section) > 1:
+            try:
+                return self._values[section][name]
+            except KeyError:
+                pass
+
+        return self._values[(section[0],)].get_all(name)
+
     def get(self, section, name):
         section, name = self._check_section_and_name(section, name)
 
@@ -265,10 +339,10 @@ class ConfigDict(Config, MutableMapping):
 
         self._values.setdefault(section)[name] = value
 
-    def iteritems(self, section):
+    def items(self, section):
         return self._values.get(section).items()
 
-    def itersections(self):
+    def sections(self):
         return self._values.keys()
 
 

+ 6 - 10
dulwich/contrib/diffstat.py

@@ -56,15 +56,11 @@ _GIT_UNCHANGED_START = b" "
 
 
 def _parse_patch(lines):
-    """An internal routine to parse a git style diff or patch to generate
-       diff stats
+    """Parse a git style diff or patch to generate diff stats.
+
     Args:
-      lines: list of byte strings "lines" from the diff to be parsed
-    Returns: A tuple (names, nametypes, counts) of three lists:
-             names = list of repo relative file paths
-             nametypes - list of booolean values indicating if file
-                         is binary (True means binary file)
-             counts = list of tuples of (added, deleted) counts for that file
+      lines: list of byte string lines from the diff to be parsed
+    Returns: A tuple (names, is_binary, counts) of three lists
     """
     names = []
     nametypes = []
@@ -323,14 +319,14 @@ index 3b41fd80..64914c78 100644
  2. open Sigil.app to the normal nearly blank template epub it generates when opened
  3. use Plugins->Manage Plugins menu and make sure the "Use Bundled Python" checkbox is checked
  4. use the "Add Plugin" button to navigate to and add testplugin.zip and then hit "Okay" to exit the Manage Plugins Dialog
-"""  # noqa: E501 W293
+"""
 
     testoutput = b""" docs/qt512.7_remove_bad_workaround.patch            | 15 ++++++++++++
  docs/testplugin_v017.zip                            | Bin
  ci_scripts/macgddeploy.py => ci_scripts/gddeploy.py |  0 
  docs/qt512.6_backport_009abcd_fix.patch             | 26 ---------------------
  docs/Building_Sigil_On_MacOSX.txt                   |  2 +-
- 5 files changed, 16 insertions(+), 27 deletions(-)"""  # noqa: W291
+ 5 files changed, 16 insertions(+), 27 deletions(-)"""
 
     # return 0 on success otherwise return -1
     result = diffstat(selftest.split(b"\n"))

+ 145 - 0
dulwich/contrib/requests_vendor.py

@@ -0,0 +1,145 @@
+# requests_vendor.py -- requests implementation of the AbstractHttpGitClient interface
+# Copyright (C) 2022 Eden Shalit <epopcop@gmail.com>
+#
+# Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
+# General Public License as public by the Free Software Foundation; version 2.0
+# or (at your option) any later version. You can redistribute it and/or
+# modify it under the terms of either of these two licenses.
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# You should have received a copy of the licenses; if not, see
+# <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
+# and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
+# License, Version 2.0.
+
+
+"""Requests HTTP client support for Dulwich.
+
+To use this implementation as the HTTP implementation in Dulwich, override
+the dulwich.client.HttpGitClient attribute:
+
+  >>> from dulwich import client as _mod_client
+  >>> from dulwich.contrib.requests_vendor import RequestsHttpGitClient
+  >>> _mod_client.HttpGitClient = RequestsHttpGitClient
+
+This implementation is experimental and does not have any tests.
+"""
+from io import BytesIO
+
+from requests import Session
+
+from dulwich.client import AbstractHttpGitClient, HTTPUnauthorized, HTTPProxyUnauthorized, default_user_agent_string
+from dulwich.errors import NotGitRepository, GitProtocolError
+
+
+class RequestsHttpGitClient(AbstractHttpGitClient):
+    def __init__(
+            self,
+            base_url,
+            dumb=None,
+            config=None,
+            username=None,
+            password=None,
+            **kwargs
+    ):
+        self._username = username
+        self._password = password
+
+        self.session = get_session(config)
+
+        if username is not None:
+            self.session.auth = (username, password)
+
+        super(RequestsHttpGitClient, self).__init__(
+            base_url=base_url, dumb=dumb, **kwargs)
+
+    def _http_request(self, url, headers=None, data=None, allow_compression=False):
+        req_headers = self.session.headers.copy()
+        if headers is not None:
+            req_headers.update(headers)
+
+        if allow_compression:
+            req_headers["Accept-Encoding"] = "gzip"
+        else:
+            req_headers["Accept-Encoding"] = "identity"
+
+        if data:
+            resp = self.session.post(url, headers=req_headers, data=data)
+        else:
+            resp = self.session.get(url, headers=req_headers)
+
+        if resp.status_code == 404:
+            raise NotGitRepository()
+        if resp.status_code == 401:
+            raise HTTPUnauthorized(resp.headers.get("WWW-Authenticate"), url)
+        if resp.status_code == 407:
+            raise HTTPProxyUnauthorized(resp.headers.get("Proxy-Authenticate"), url)
+        if resp.status_code != 200:
+            raise GitProtocolError(
+                "unexpected http resp %d for %s" % (resp.status_code, url)
+            )
+
+        # Add required fields as stated in AbstractHttpGitClient._http_request
+        resp.content_type = resp.headers.get("Content-Type")
+        resp.redirect_location = ""
+        if resp.history:
+            resp.redirect_location = resp.url
+
+        read = BytesIO(resp.content).read
+
+        return resp, read
+
+
+def get_session(config):
+    session = Session()
+    session.headers.update({"Pragma": "no-cache"})
+
+    proxy_server = user_agent = ca_certs = ssl_verify = None
+
+    if config is not None:
+        try:
+            proxy_server = config.get(b"http", b"proxy")
+            if isinstance(proxy_server, bytes):
+                proxy_server = proxy_server.decode()
+        except KeyError:
+            pass
+
+        try:
+            user_agent = config.get(b"http", b"useragent")
+            if isinstance(user_agent, bytes):
+                user_agent = user_agent.decode()
+        except KeyError:
+            pass
+
+        try:
+            ssl_verify = config.get_boolean(b"http", b"sslVerify")
+        except KeyError:
+            ssl_verify = True
+
+        try:
+            ca_certs = config.get(b"http", b"sslCAInfo")
+            if isinstance(ca_certs, bytes):
+                ca_certs = ca_certs.decode()
+        except KeyError:
+            ca_certs = None
+
+    if user_agent is None:
+        user_agent = default_user_agent_string()
+    session.headers.update({"User-agent": user_agent})
+
+    if ca_certs:
+        session.verify = ca_certs
+    elif ssl_verify is False:
+        session.verify = ssl_verify
+
+    if proxy_server:
+        session.proxies.update({
+            "http": proxy_server,
+            "https": proxy_server
+        })
+    return session

+ 3 - 3
dulwich/diff_tree.py

@@ -130,7 +130,7 @@ def walk_trees(store, tree1_id, tree2_id, prune_identical=False):
       store: An ObjectStore for looking up objects.
       tree1_id: The SHA of the first Tree object to iterate, or None.
       tree2_id: The SHA of the second Tree object to iterate, or None.
-      param prune_identical: If True, identical subtrees will not be walked.
+      prune_identical: If True, identical subtrees will not be walked.
     Returns:
       Iterator over Pairs of TreeEntry objects for each pair of entries
         in the trees and their subtrees recursively. If an entry exists in one
@@ -345,8 +345,8 @@ def _common_bytes(blocks1, blocks2):
     """Count the number of common bytes in two block count dicts.
 
     Args:
-      block1: The first dict of block hashcode -> total bytes.
-      block2: The second dict of block hashcode -> total bytes.
+      blocks1: The first dict of block hashcode -> total bytes.
+      blocks2: The second dict of block hashcode -> total bytes.
     Returns:
       The number of bytes in common between blocks1 and blocks2. This is
       only approximate due to possible hash collisions.

+ 2 - 2
dulwich/fastexport.py

@@ -30,14 +30,14 @@ from dulwich.objects import (
     Tag,
     ZERO_SHA,
 )
-from fastimport import (  # noqa: E402
+from fastimport import (
     commands,
     errors as fastimport_errors,
     parser,
     processor,
 )
 
-import stat  # noqa: E402
+import stat
 
 
 def split_email(text):

+ 0 - 5
dulwich/hooks.py

@@ -134,11 +134,6 @@ class PostCommitShellHook(ShellHook):
 
 class CommitMsgShellHook(ShellHook):
     """commit-msg shell hook
-
-    Args:
-      args[0]: commit message
-    Returns:
-      new commit message or None
     """
 
     def __init__(self, controldir):

+ 3 - 0
dulwich/ignore.py

@@ -52,6 +52,9 @@ def _translate_segment(segment: bytes) -> bytes:
             res += b"[^/]*"
         elif c == b"?":
             res += b"[^/]"
+        elif c == b"\\":
+            res += re.escape(segment[i : i + 1])
+            i += 1
         elif c == b"[":
             j = i
             if j < n and segment[j : j + 1] == b"!":

+ 29 - 3
dulwich/index.py

@@ -579,7 +579,7 @@ def build_file_from_blob(
     """Build a file or symlink on disk based on a Git object.
 
     Args:
-      obj: The git object
+      blob: The git object
       mode: File mode
       target_path: Path to write to
       honor_filemode: An optional flag to honor core.filemode setting in
@@ -718,7 +718,7 @@ def blob_from_path_and_mode(fs_path, mode, tree_encoding="utf-8"):
 
     Args:
       fs_path: Full file system path to file
-      st: A stat object
+      mode: File mode
     Returns: A `Blob` object
     """
     assert isinstance(fs_path, bytes)
@@ -916,7 +916,7 @@ def iter_fresh_entries(
     Args:
       paths: Paths to iterate over
       root_path: Root path to access from
-      store: Optional store to save new blobs in
+      object_store: Optional store to save new blobs in
     Returns: Iterator over path, index_entry
     """
     for path in paths:
@@ -958,3 +958,29 @@ def refresh_index(index, root_path):
     """
     for path, entry in iter_fresh_entries(index, root_path):
         index[path] = path
+
+
+class locked_index(object):
+    """Lock the index while making modifications.
+
+    Works as a context manager.
+    """
+    def __init__(self, path):
+        self._path = path
+
+    def __enter__(self):
+        self._file = GitFile(self._path, "wb")
+        self._index = Index(self._path)
+        return self._index
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        if exc_type is not None:
+            self._file.abort()
+            return
+        try:
+            f = SHA1Writer(self._file)
+            write_index_dict(f, self._index._byname)
+        except BaseException:
+            self._file.abort()
+        else:
+            f.close()

+ 28 - 28
dulwich/line_ending.py

@@ -17,7 +17,7 @@
 # and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
 # License, Version 2.0.
 #
-""" All line-ending related functions, from conversions to config processing
+"""All line-ending related functions, from conversions to config processing
 
 Line-ending normalization is a complex beast. Here is some notes and details
 about how it seems to work.
@@ -25,10 +25,10 @@ about how it seems to work.
 The normalization is a two-fold process that happens at two moments:
 
 - When reading a file from the index and to the working directory. For example
-  when doing a `git clone` or `git checkout` call. We call this process the
+  when doing a ``git clone`` or ``git checkout`` call. We call this process the
   read filter in this module.
 - When writing a file to the index from the working directory. For example
-  when doing a `git add` call. We call this process the write filter in this
+  when doing a ``git add`` call. We call this process the write filter in this
   module.
 
 Note that when checking status (getting unstaged changes), whether or not
@@ -51,47 +51,47 @@ The code for this heuristic is here:
 https://git.kernel.org/pub/scm/git/git.git/tree/convert.c#n46
 
 Dulwich have an implementation with a slightly different heuristic, the
-`is_binary` function in `dulwich.patch`.
+`dulwich.patch.is_binary` function.
 
 The binary detection heuristic implementation is close to the one in JGit:
 https://github.com/eclipse/jgit/blob/f6873ffe522bbc3536969a3a3546bf9a819b92bf/org.eclipse.jgit/src/org/eclipse/jgit/diff/RawText.java#L300
 
 There is multiple variables that impact the normalization.
 
-First, a repository can contains a `.gitattributes` file (or more than one...)
+First, a repository can contains a ``.gitattributes`` file (or more than one...)
 that can further customize the operation on some file patterns, for example:
 
-    *.txt text
+    \\*.txt text
 
-Force all `.txt` files to be treated as text files and to have their lines
+Force all ``.txt`` files to be treated as text files and to have their lines
 endings normalized.
 
-    *.jpg -text
+    \\*.jpg -text
 
-Force all `.jpg` files to be treated as binary files and to not have their
+Force all ``.jpg`` files to be treated as binary files and to not have their
 lines endings converted.
 
-    *.vcproj text eol=crlf
+    \\*.vcproj text eol=crlf
 
-Force all `.vcproj` files to be treated as text files and to have their lines
-endings converted into `CRLF` in working directory no matter the native EOL of
+Force all ``.vcproj`` files to be treated as text files and to have their lines
+endings converted into ``CRLF`` in working directory no matter the native EOL of
 the platform.
 
-    *.sh text eol=lf
+    \\*.sh text eol=lf
 
-Force all `.sh` files to be treated as text files and to have their lines
-endings converted into `LF` in working directory no matter the native EOL of
+Force all ``.sh`` files to be treated as text files and to have their lines
+endings converted into ``LF`` in working directory no matter the native EOL of
 the platform.
 
-If the `eol` attribute is not defined, Git uses the `core.eol` configuration
+If the ``eol`` attribute is not defined, Git uses the ``core.eol`` configuration
 value described later.
 
-    * text=auto
+    \\* text=auto
 
 Force all files to be scanned by the text file heuristic detection and to have
 their line endings normalized in case they are detected as text files.
 
-Git also have a obsolete attribute named `crlf` that can be translated to the
+Git also have a obsolete attribute named ``crlf`` that can be translated to the
 corresponding text attribute value.
 
 Then there are some configuration option (that can be defined at the
@@ -100,30 +100,30 @@ repository or user level):
 - core.autocrlf
 - core.eol
 
-`core.autocrlf` is taken into account for all files that doesn't have a `text`
-attribute defined in `.gitattributes`; it takes three possible values:
+``core.autocrlf`` is taken into account for all files that doesn't have a ``text``
+attribute defined in ``.gitattributes``; it takes three possible values:
 
-    - `true`: This forces all files on the working directory to have CRLF
+    - ``true``: This forces all files on the working directory to have CRLF
       line-endings in the working directory and convert line-endings to LF
       when writing to the index. When autocrlf is set to true, eol value is
       ignored.
-    - `input`: Quite similar to the `true` value but only force the write
+    - ``input``: Quite similar to the ``true`` value but only force the write
       filter, ie line-ending of new files added to the index will get their
       line-endings converted to LF.
-    - `false` (default): No normalization is done.
+    - ``false`` (default): No normalization is done.
 
-`core.eol` is the top-level configuration to define the line-ending to use
+``core.eol`` is the top-level configuration to define the line-ending to use
 when applying the read_filer. It takes three possible values:
 
-    - `lf`: When normalization is done, force line-endings to be `LF` in the
+    - ``lf``: When normalization is done, force line-endings to be ``LF`` in the
       working directory.
-    - `crlf`: When normalization is done, force line-endings to be `CRLF` in
+    - ``crlf``: When normalization is done, force line-endings to be ``CRLF`` in
       the working directory.
-    - `native` (default): When normalization is done, force line-endings to be
+    - ``native`` (default): When normalization is done, force line-endings to be
       the platform's native line ending.
 
 One thing to remember is when line-ending normalization is done on a file, Git
-always normalize line-ending to `LF` when writing to the index.
+always normalize line-ending to ``LF`` when writing to the index.
 
 There are sources that seems to indicate that Git won't do line-ending
 normalization when a file contains mixed line-endings. I think this logic

+ 4 - 3
dulwich/object_store.py

@@ -158,7 +158,7 @@ class BaseObjectStore(object):
         """Add pack data to this object store.
 
         Args:
-          num_items: Number of items to add
+          count: Number of items to add
           pack_data: Iterator over pack data tuples
         """
         if count == 0:
@@ -1350,8 +1350,9 @@ class MissingObjectFinder(object):
 class ObjectStoreGraphWalker(object):
     """Graph walker that finds what commits are missing from an object store.
 
-    :ivar heads: Revisions without descendants in the local repo
-    :ivar get_parents: Function to retrieve parents in the local repo
+    Attributes:
+      heads: Revisions without descendants in the local repo
+      get_parents: Function to retrieve parents in the local repo
     """
 
     def __init__(self, local_heads, get_parents, shallow=None):

+ 1 - 1
dulwich/objects.py

@@ -222,7 +222,7 @@ def check_time(time_seconds):
     This will raise an exception if the time is not valid.
 
     Args:
-      time_info: author/committer/tagger info
+      time_seconds: time in seconds
 
     """
     # Prevent overflow error

+ 3 - 3
dulwich/objectspec.py

@@ -94,7 +94,7 @@ def parse_reftuple(lh_container, rh_container, refspec, force=False):
 
     Args:
       lh_container: A RefsContainer object
-      hh_container: A RefsContainer object
+      rh_container: A RefsContainer object
       refspec: A string
     Returns: A tuple with left and right ref
     Raises:
@@ -132,7 +132,7 @@ def parse_reftuples(
 
     Args:
       lh_container: A RefsContainer object
-      hh_container: A RefsContainer object
+      rh_container: A RefsContainer object
       refspecs: A list of refspecs or a string
       force: Force overwriting for all reftuples
     Returns: A list of refs
@@ -211,7 +211,7 @@ def parse_commit(repo, committish):
 
     Args:
       repo: A` Repo` object
-      commitish: A string referring to a single commit.
+      committish: A string referring to a single commit.
     Returns: A Commit object
     Raises:
       KeyError: When the reference commits can not be found

+ 11 - 12
dulwich/pack.py

@@ -66,15 +66,15 @@ else:
 if sys.platform == "Plan9":
     has_mmap = False
 
-from dulwich.errors import (  # noqa: E402
+from dulwich.errors import (
     ApplyDeltaError,
     ChecksumMismatch,
 )
-from dulwich.file import GitFile  # noqa: E402
-from dulwich.lru_cache import (  # noqa: E402
+from dulwich.file import GitFile
+from dulwich.lru_cache import (
     LRUSizeCache,
 )
-from dulwich.objects import (  # noqa: E402
+from dulwich.objects import (
     ShaFile,
     hex_to_sha,
     sha_to_hex,
@@ -274,7 +274,7 @@ def load_pack_index(path):
     """Load an index file by path.
 
     Args:
-      filename: Path to the index file
+      path: Path to the index file
     Returns: A PackIndex loaded from the given path
     """
     with GitFile(path, "rb") as f:
@@ -1599,9 +1599,8 @@ def write_pack(
 
     Args:
       filename: Path to the new pack file (without .pack extension)
-      objects: Iterable of (object, path) tuples to write.
-        Should provide __len__
-      window_size: Delta window size
+      objects: (object, path) tuple iterable to write. Should provide __len__
+      delta_window_size: Delta window size
       deltify: Whether to deltify pack objects
       compression_level: the zlib compression level
     Returns: Tuple with checksum of pack file and index file
@@ -1688,10 +1687,10 @@ def write_pack_objects(
 
     Args:
       f: File to write to
-      objects: Iterable of (object, path) tuples to write.
-        Should provide __len__
-      window_size: Sliding window size for searching for deltas;
-                        Set to None for default window size.
+      objects: Iterable of (object, path) tuples to write. Should provide
+         __len__
+      delta_window_size: Sliding window size for searching for deltas;
+                         Set to None for default window size.
       deltify: Whether to deltify objects
       compression_level: the zlib compression level to use
     Returns: Dict mapping id -> (offset, crc32 checksum), pack checksum

+ 16 - 8
dulwich/porcelain.py

@@ -448,6 +448,7 @@ def clone(
         origin=origin,
         checkout=checkout,
         branch=branch,
+        progress=errstream.write,
         depth=depth,
     )
 
@@ -508,7 +509,7 @@ def _is_subdir(subdir, parentdir):
 def clean(repo=".", target_dir=None):
     """Remove any untracked files from the target directory recursively
 
-    Equivalent to running `git clean -fd` in target_dir.
+    Equivalent to running ``git clean -fd`` in target_dir.
 
     Args:
       repo: Repository where the files may be tracked
@@ -831,7 +832,7 @@ def show(
             show_object(r, o, decode, outstream)
 
 
-def diff_tree(repo, old_tree, new_tree, outstream=sys.stdout):
+def diff_tree(repo, old_tree, new_tree, outstream=default_bytes_out_stream):
     """Compares the content and mode of blobs found via two tree objects.
 
     Args:
@@ -1107,7 +1108,7 @@ def pull(
     Args:
       repo: Path to repository
       remote_location: Location of the remote
-      refspec: refspecs to fetch
+      refspecs: refspecs to fetch
       outstream: A stream file to write to output
       errstream: A stream file to write to errors
     """
@@ -1159,7 +1160,7 @@ def status(repo=".", ignored=False):
 
     Args:
       repo: Path to repository or repository object
-      ignored: Whether to include ignored files in `untracked`
+      ignored: Whether to include ignored files in untracked
     Returns: GitStatus tuple,
         staged -  dict with lists of staged paths (diff index/HEAD)
         unstaged -  list of unstaged paths (diff index/working-tree)
@@ -1591,7 +1592,7 @@ def ls_tree(
 
     Args:
       repo: Path to the repository
-      tree_ish: Tree id to list
+      treeish: Tree id to list
       outstream: Output stream (defaults to stdout)
       recursive: Whether to recursively list files
       name_only: Only print item name
@@ -1662,7 +1663,7 @@ def update_head(repo, target, detached=False, new_branch=None):
 
     Args:
       repo: Path to the repository
-      detach: Create a detached head
+      detached: Create a detached head
       target: Branch or committish to switch to
       new_branch: New branch to create
     """
@@ -1780,11 +1781,18 @@ def ls_files(repo):
         return sorted(r.open_index())
 
 
+def find_unique_abbrev(object_store, object_id):
+    """For now, just return 7 characters."""
+    # TODO(jelmer): Add some logic here to return a number of characters that
+    # scales relative with the size of the repository
+    return object_id.decode("ascii")[:7]
+
+
 def describe(repo):
     """Describe the repository version.
 
     Args:
-      projdir: git repository root
+      repo: git repository
     Returns: a string description of the current git revision
 
     Examples: "gabcdefh", "v0.1" or "v0.1-5-gabcdefh".
@@ -1817,7 +1825,7 @@ def describe(repo):
 
         # If there are no tags, return the current commit
         if len(sorted_tags) == 0:
-            return "g{}".format(r[r.head()].id.decode("ascii")[:7])
+            return "g{}".format(find_unique_abbrev(r.object_store, r[r.head()].id))
 
         # We're now 0 commits from the top
         commit_count = 0

+ 2 - 2
dulwich/refs.py

@@ -350,13 +350,13 @@ class RefsContainer(object):
         """
         raise NotImplementedError(self.set_if_equals)
 
-    def add_if_new(self, name, ref):
+    def add_if_new(self, name, ref, committer=None, timestamp=None,
+                   timezone=None, message=None):
         """Add a new reference only if it does not already exist.
 
         Args:
           name: Ref name
           ref: Ref value
-          message: Message for reflog
         """
         raise NotImplementedError(self.add_if_new)
 

+ 21 - 8
dulwich/repo.py

@@ -147,8 +147,8 @@ def _get_default_identity() -> Tuple[str, str]:
         fullname = None
     else:
         try:
-            gecos = pwd.getpwnam(username).pw_gecos
-        except KeyError:
+            gecos = pwd.getpwnam(username).pw_gecos  # type: ignore
+        except (KeyError, AttributeError):
             fullname = None
         else:
             if gecos:
@@ -327,9 +327,10 @@ class ParentsProvider(object):
 class BaseRepo(object):
     """Base class for a git repository.
 
-    :ivar object_store: Dictionary-like object for accessing
+    Attributes:
+      object_store: Dictionary-like object for accessing
         the objects
-    :ivar refs: Dictionary-like object with the refs in this
+      refs: Dictionary-like object with the refs in this
         repository
     """
 
@@ -878,7 +879,7 @@ class BaseRepo(object):
     ):
         """Create a new commit.
 
-        If not specified, `committer` and `author` default to
+        If not specified, committer and author default to
         get_user_identity(..., 'COMMITTER')
         and get_user_identity(..., 'AUTHOR') respectively.
 
@@ -1044,6 +1045,16 @@ class Repo(BaseRepo):
     the path of the repository.
 
     To create a new repository, use the Repo.init class method.
+
+    Note that a repository object may hold on to resources such
+    as file handles for performance reasons; call .close() to free
+    up those resources.
+
+    Attributes:
+
+      path (str): Path to the working copy (if it exists) or repository control
+        directory (if the repository is bare)
+      bare (bool): Whether this is a bare repository
     """
 
     def __init__(self, root, object_store=None, bare=None):
@@ -1170,8 +1181,8 @@ class Repo(BaseRepo):
         For a main working tree, it is identical to controldir().
 
         For a linked working tree, it is the control directory of the
-        main working tree."""
-
+        main working tree.
+        """
         return self._commondir
 
     def _determine_file_mode(self):
@@ -1389,6 +1400,7 @@ class Repo(BaseRepo):
         origin=b"origin",
         checkout=None,
         branch=None,
+        progress=None,
         depth=None,
     ):
         """Clone this repository.
@@ -1402,6 +1414,7 @@ class Repo(BaseRepo):
             cloned from this repository
           branch: Optional branch or tag to be used as HEAD in the new repository
             instead of this repository's HEAD.
+          progress: Optional progress function
           depth: Depth at which to fetch
         Returns: Created repository as `Repo`
         """
@@ -1512,7 +1525,7 @@ class Repo(BaseRepo):
         """
         from dulwich.config import ConfigFile
 
-        path = os.path.join(self._controldir, "config")
+        path = os.path.join(self._commondir, "config")
         try:
             return ConfigFile.from_path(path)
         except FileNotFoundError:

+ 1 - 1
dulwich/server.py

@@ -70,7 +70,7 @@ from dulwich.objects import (
 from dulwich.pack import (
     write_pack_objects,
 )
-from dulwich.protocol import (  # noqa: F401
+from dulwich.protocol import (
     BufferedPktLineWriter,
     capability_agent,
     CAPABILITIES_REF,

+ 6 - 6
dulwich/tests/__init__.py

@@ -145,12 +145,12 @@ def self_test_suite():
 
 
 def tutorial_test_suite():
-    import dulwich.client  # noqa: F401
-    import dulwich.config  # noqa: F401
-    import dulwich.index  # noqa: F401
-    import dulwich.reflog  # noqa: F401
-    import dulwich.repo  # noqa: F401
-    import dulwich.server  # noqa: F401
+    import dulwich.client
+    import dulwich.config
+    import dulwich.index
+    import dulwich.reflog
+    import dulwich.repo
+    import dulwich.server
     import dulwich.patch  # noqa: F401
 
     tutorial = [

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

@@ -337,7 +337,7 @@ class DulwichClientTestBase(object):
             c = self._client()
             self.assertEqual(dest.refs[b"refs/heads/abranch"], dummy_commit)
             c.send_pack(self._build_path("/dest"), lambda _: sendrefs, gen_pack)
-            self.assertFalse(b"refs/heads/abranch" in dest.refs)
+            self.assertNotIn(b"refs/heads/abranch", dest.refs)
 
     def test_send_new_branch_empty_pack(self):
         with repo.Repo(os.path.join(self.gitroot, "dest")) as dest:

+ 26 - 0
dulwich/tests/compat/test_repository.py

@@ -192,6 +192,32 @@ class WorkingTreeTestCase(ObjectStoreTestCase):
         self.assertEqual(worktrees[0][1], "(bare)")
         self.assertTrue(os.path.samefile(worktrees[0][0], self._mainworktree_repo.path))
 
+    def test_git_worktree_config(self):
+        """Test that git worktree config parsing matches the git CLI's behavior."""
+        # Set some config value in the main repo using the git CLI
+        require_git_version((2, 7, 0))
+        test_name = "Jelmer"
+        test_email = "jelmer@apache.org"
+        run_git_or_fail(["config", "user.name", test_name], cwd=self._repo.path)
+        run_git_or_fail(["config", "user.email", test_email], cwd=self._repo.path)
+
+        worktree_cfg = self._worktree_repo.get_config()
+        main_cfg = self._repo.get_config()
+
+        # Assert that both the worktree repo and main repo have the same view of the config,
+        # and that the config matches what we set with the git cli
+        self.assertEqual(worktree_cfg, main_cfg)
+        for c in [worktree_cfg, main_cfg]:
+            self.assertEqual(test_name.encode(), c.get((b"user",), b"name"))
+            self.assertEqual(test_email.encode(), c.get((b"user",), b"email"))
+
+        # Read the config values in the worktree with the git cli and assert they match
+        # the dulwich-parsed configs
+        output_name = run_git_or_fail(["config", "user.name"], cwd=self._mainworktree_repo.path).decode().rstrip("\n")
+        output_email = run_git_or_fail(["config", "user.email"], cwd=self._mainworktree_repo.path).decode().rstrip("\n")
+        self.assertEqual(test_name, output_name)
+        self.assertEqual(test_email, output_email)
+
 
 class InitNewWorkingDirectoryTestCase(WorkingTreeTestCase):
     """Test compatibility of Repo.init_new_working_directory."""

+ 2 - 2
dulwich/tests/compat/test_server.py

@@ -59,7 +59,7 @@ class GitServerTestCase(ServerTests, CompatTestCase):
     def _check_server(self, dul_server):
         receive_pack_handler_cls = dul_server.handlers[b"git-receive-pack"]
         caps = receive_pack_handler_cls.capabilities()
-        self.assertFalse(b"side-band-64k" in caps)
+        self.assertNotIn(b"side-band-64k", caps)
 
     def _start_server(self, repo):
         backend = DictBackend({b"/": repo})
@@ -94,4 +94,4 @@ class GitServerSideBand64kTestCase(GitServerTestCase):
     def _check_server(self, server):
         receive_pack_handler_cls = server.handlers[b"git-receive-pack"]
         caps = receive_pack_handler_cls.capabilities()
-        self.assertTrue(b"side-band-64k" in caps)
+        self.assertIn(b"side-band-64k", caps)

+ 33 - 32
dulwich/tests/test_client.py

@@ -427,21 +427,21 @@ class GitClientTests(TestCase):
 class TestGetTransportAndPath(TestCase):
     def test_tcp(self):
         c, path = get_transport_and_path("git://foo.com/bar/baz")
-        self.assertTrue(isinstance(c, TCPGitClient))
+        self.assertIsInstance(c, TCPGitClient)
         self.assertEqual("foo.com", c._host)
         self.assertEqual(TCP_GIT_PORT, c._port)
         self.assertEqual("/bar/baz", path)
 
     def test_tcp_port(self):
         c, path = get_transport_and_path("git://foo.com:1234/bar/baz")
-        self.assertTrue(isinstance(c, TCPGitClient))
+        self.assertIsInstance(c, TCPGitClient)
         self.assertEqual("foo.com", c._host)
         self.assertEqual(1234, c._port)
         self.assertEqual("/bar/baz", path)
 
     def test_git_ssh_explicit(self):
         c, path = get_transport_and_path("git+ssh://foo.com/bar/baz")
-        self.assertTrue(isinstance(c, SSHGitClient))
+        self.assertIsInstance(c, SSHGitClient)
         self.assertEqual("foo.com", c.host)
         self.assertEqual(None, c.port)
         self.assertEqual(None, c.username)
@@ -449,7 +449,7 @@ class TestGetTransportAndPath(TestCase):
 
     def test_ssh_explicit(self):
         c, path = get_transport_and_path("ssh://foo.com/bar/baz")
-        self.assertTrue(isinstance(c, SSHGitClient))
+        self.assertIsInstance(c, SSHGitClient)
         self.assertEqual("foo.com", c.host)
         self.assertEqual(None, c.port)
         self.assertEqual(None, c.username)
@@ -457,20 +457,20 @@ class TestGetTransportAndPath(TestCase):
 
     def test_ssh_port_explicit(self):
         c, path = get_transport_and_path("git+ssh://foo.com:1234/bar/baz")
-        self.assertTrue(isinstance(c, SSHGitClient))
+        self.assertIsInstance(c, SSHGitClient)
         self.assertEqual("foo.com", c.host)
         self.assertEqual(1234, c.port)
         self.assertEqual("/bar/baz", path)
 
     def test_username_and_port_explicit_unknown_scheme(self):
         c, path = get_transport_and_path("unknown://git@server:7999/dply/stuff.git")
-        self.assertTrue(isinstance(c, SSHGitClient))
+        self.assertIsInstance(c, SSHGitClient)
         self.assertEqual("unknown", c.host)
         self.assertEqual("//git@server:7999/dply/stuff.git", path)
 
     def test_username_and_port_explicit(self):
         c, path = get_transport_and_path("ssh://git@server:7999/dply/stuff.git")
-        self.assertTrue(isinstance(c, SSHGitClient))
+        self.assertIsInstance(c, SSHGitClient)
         self.assertEqual("git", c.username)
         self.assertEqual("server", c.host)
         self.assertEqual(7999, c.port)
@@ -478,7 +478,7 @@ class TestGetTransportAndPath(TestCase):
 
     def test_ssh_abspath_doubleslash(self):
         c, path = get_transport_and_path("git+ssh://foo.com//bar/baz")
-        self.assertTrue(isinstance(c, SSHGitClient))
+        self.assertIsInstance(c, SSHGitClient)
         self.assertEqual("foo.com", c.host)
         self.assertEqual(None, c.port)
         self.assertEqual(None, c.username)
@@ -486,14 +486,14 @@ class TestGetTransportAndPath(TestCase):
 
     def test_ssh_port(self):
         c, path = get_transport_and_path("git+ssh://foo.com:1234/bar/baz")
-        self.assertTrue(isinstance(c, SSHGitClient))
+        self.assertIsInstance(c, SSHGitClient)
         self.assertEqual("foo.com", c.host)
         self.assertEqual(1234, c.port)
         self.assertEqual("/bar/baz", path)
 
     def test_ssh_implicit(self):
         c, path = get_transport_and_path("foo:/bar/baz")
-        self.assertTrue(isinstance(c, SSHGitClient))
+        self.assertIsInstance(c, SSHGitClient)
         self.assertEqual("foo", c.host)
         self.assertEqual(None, c.port)
         self.assertEqual(None, c.username)
@@ -501,7 +501,7 @@ class TestGetTransportAndPath(TestCase):
 
     def test_ssh_host(self):
         c, path = get_transport_and_path("foo.com:/bar/baz")
-        self.assertTrue(isinstance(c, SSHGitClient))
+        self.assertIsInstance(c, SSHGitClient)
         self.assertEqual("foo.com", c.host)
         self.assertEqual(None, c.port)
         self.assertEqual(None, c.username)
@@ -509,7 +509,7 @@ class TestGetTransportAndPath(TestCase):
 
     def test_ssh_user_host(self):
         c, path = get_transport_and_path("user@foo.com:/bar/baz")
-        self.assertTrue(isinstance(c, SSHGitClient))
+        self.assertIsInstance(c, SSHGitClient)
         self.assertEqual("foo.com", c.host)
         self.assertEqual(None, c.port)
         self.assertEqual("user", c.username)
@@ -517,7 +517,7 @@ class TestGetTransportAndPath(TestCase):
 
     def test_ssh_relpath(self):
         c, path = get_transport_and_path("foo:bar/baz")
-        self.assertTrue(isinstance(c, SSHGitClient))
+        self.assertIsInstance(c, SSHGitClient)
         self.assertEqual("foo", c.host)
         self.assertEqual(None, c.port)
         self.assertEqual(None, c.username)
@@ -525,7 +525,7 @@ class TestGetTransportAndPath(TestCase):
 
     def test_ssh_host_relpath(self):
         c, path = get_transport_and_path("foo.com:bar/baz")
-        self.assertTrue(isinstance(c, SSHGitClient))
+        self.assertIsInstance(c, SSHGitClient)
         self.assertEqual("foo.com", c.host)
         self.assertEqual(None, c.port)
         self.assertEqual(None, c.username)
@@ -533,7 +533,7 @@ class TestGetTransportAndPath(TestCase):
 
     def test_ssh_user_host_relpath(self):
         c, path = get_transport_and_path("user@foo.com:bar/baz")
-        self.assertTrue(isinstance(c, SSHGitClient))
+        self.assertIsInstance(c, SSHGitClient)
         self.assertEqual("foo.com", c.host)
         self.assertEqual(None, c.port)
         self.assertEqual("user", c.username)
@@ -541,25 +541,25 @@ class TestGetTransportAndPath(TestCase):
 
     def test_local(self):
         c, path = get_transport_and_path("foo.bar/baz")
-        self.assertTrue(isinstance(c, LocalGitClient))
+        self.assertIsInstance(c, LocalGitClient)
         self.assertEqual("foo.bar/baz", path)
 
     @skipIf(sys.platform != "win32", "Behaviour only happens on windows.")
     def test_local_abs_windows_path(self):
         c, path = get_transport_and_path("C:\\foo.bar\\baz")
-        self.assertTrue(isinstance(c, LocalGitClient))
+        self.assertIsInstance(c, LocalGitClient)
         self.assertEqual("C:\\foo.bar\\baz", path)
 
     def test_error(self):
         # Need to use a known urlparse.uses_netloc URL scheme to get the
         # expected parsing of the URL on Python versions less than 2.6.5
         c, path = get_transport_and_path("prospero://bar/baz")
-        self.assertTrue(isinstance(c, SSHGitClient))
+        self.assertIsInstance(c, SSHGitClient)
 
     def test_http(self):
         url = "https://github.com/jelmer/dulwich"
         c, path = get_transport_and_path(url)
-        self.assertTrue(isinstance(c, HttpGitClient))
+        self.assertIsInstance(c, HttpGitClient)
         self.assertEqual("/jelmer/dulwich", path)
 
     def test_http_auth(self):
@@ -567,7 +567,7 @@ class TestGetTransportAndPath(TestCase):
 
         c, path = get_transport_and_path(url)
 
-        self.assertTrue(isinstance(c, HttpGitClient))
+        self.assertIsInstance(c, HttpGitClient)
         self.assertEqual("/jelmer/dulwich", path)
         self.assertEqual("user", c._username)
         self.assertEqual("passwd", c._password)
@@ -577,7 +577,7 @@ class TestGetTransportAndPath(TestCase):
 
         c, path = get_transport_and_path(url, username="user2", password="blah")
 
-        self.assertTrue(isinstance(c, HttpGitClient))
+        self.assertIsInstance(c, HttpGitClient)
         self.assertEqual("/jelmer/dulwich", path)
         self.assertEqual("user2", c._username)
         self.assertEqual("blah", c._password)
@@ -587,7 +587,7 @@ class TestGetTransportAndPath(TestCase):
 
         c, path = get_transport_and_path(url, username="user2", password="blah")
 
-        self.assertTrue(isinstance(c, HttpGitClient))
+        self.assertIsInstance(c, HttpGitClient)
         self.assertEqual("/jelmer/dulwich", path)
         self.assertEqual("user", c._username)
         self.assertEqual("passwd", c._password)
@@ -597,7 +597,7 @@ class TestGetTransportAndPath(TestCase):
 
         c, path = get_transport_and_path(url)
 
-        self.assertTrue(isinstance(c, HttpGitClient))
+        self.assertIsInstance(c, HttpGitClient)
         self.assertEqual("/jelmer/dulwich", path)
         self.assertIs(None, c._username)
         self.assertIs(None, c._password)
@@ -606,21 +606,21 @@ class TestGetTransportAndPath(TestCase):
 class TestGetTransportAndPathFromUrl(TestCase):
     def test_tcp(self):
         c, path = get_transport_and_path_from_url("git://foo.com/bar/baz")
-        self.assertTrue(isinstance(c, TCPGitClient))
+        self.assertIsInstance(c, TCPGitClient)
         self.assertEqual("foo.com", c._host)
         self.assertEqual(TCP_GIT_PORT, c._port)
         self.assertEqual("/bar/baz", path)
 
     def test_tcp_port(self):
         c, path = get_transport_and_path_from_url("git://foo.com:1234/bar/baz")
-        self.assertTrue(isinstance(c, TCPGitClient))
+        self.assertIsInstance(c, TCPGitClient)
         self.assertEqual("foo.com", c._host)
         self.assertEqual(1234, c._port)
         self.assertEqual("/bar/baz", path)
 
     def test_ssh_explicit(self):
         c, path = get_transport_and_path_from_url("git+ssh://foo.com/bar/baz")
-        self.assertTrue(isinstance(c, SSHGitClient))
+        self.assertIsInstance(c, SSHGitClient)
         self.assertEqual("foo.com", c.host)
         self.assertEqual(None, c.port)
         self.assertEqual(None, c.username)
@@ -628,14 +628,14 @@ class TestGetTransportAndPathFromUrl(TestCase):
 
     def test_ssh_port_explicit(self):
         c, path = get_transport_and_path_from_url("git+ssh://foo.com:1234/bar/baz")
-        self.assertTrue(isinstance(c, SSHGitClient))
+        self.assertIsInstance(c, SSHGitClient)
         self.assertEqual("foo.com", c.host)
         self.assertEqual(1234, c.port)
         self.assertEqual("/bar/baz", path)
 
     def test_ssh_homepath(self):
         c, path = get_transport_and_path_from_url("git+ssh://foo.com/~/bar/baz")
-        self.assertTrue(isinstance(c, SSHGitClient))
+        self.assertIsInstance(c, SSHGitClient)
         self.assertEqual("foo.com", c.host)
         self.assertEqual(None, c.port)
         self.assertEqual(None, c.username)
@@ -643,7 +643,7 @@ class TestGetTransportAndPathFromUrl(TestCase):
 
     def test_ssh_port_homepath(self):
         c, path = get_transport_and_path_from_url("git+ssh://foo.com:1234/~/bar/baz")
-        self.assertTrue(isinstance(c, SSHGitClient))
+        self.assertIsInstance(c, SSHGitClient)
         self.assertEqual("foo.com", c.host)
         self.assertEqual(1234, c.port)
         self.assertEqual("/~/bar/baz", path)
@@ -671,7 +671,7 @@ class TestGetTransportAndPathFromUrl(TestCase):
     def test_http(self):
         url = "https://github.com/jelmer/dulwich"
         c, path = get_transport_and_path_from_url(url)
-        self.assertTrue(isinstance(c, HttpGitClient))
+        self.assertIsInstance(c, HttpGitClient)
         self.assertEqual("https://github.com", c.get_url(b"/"))
         self.assertEqual("/jelmer/dulwich", path)
 
@@ -679,12 +679,12 @@ class TestGetTransportAndPathFromUrl(TestCase):
         url = "https://github.com:9090/jelmer/dulwich"
         c, path = get_transport_and_path_from_url(url)
         self.assertEqual("https://github.com:9090", c.get_url(b"/"))
-        self.assertTrue(isinstance(c, HttpGitClient))
+        self.assertIsInstance(c, HttpGitClient)
         self.assertEqual("/jelmer/dulwich", path)
 
     def test_file(self):
         c, path = get_transport_and_path_from_url("file:///home/jelmer/foo")
-        self.assertTrue(isinstance(c, LocalGitClient))
+        self.assertIsInstance(c, LocalGitClient)
         self.assertEqual("/home/jelmer/foo", path)
 
 
@@ -848,6 +848,7 @@ class LocalGitClientTests(TestCase):
         target = tempfile.mkdtemp()
         self.addCleanup(shutil.rmtree, target)
         result_repo = c.clone(s.path, target, mkdir=False)
+        self.addCleanup(result_repo.close)
         expected = dict(s.get_refs())
         expected[b'refs/remotes/origin/HEAD'] = expected[b'HEAD']
         expected[b'refs/remotes/origin/master'] = expected[b'refs/heads/master']

+ 17 - 6
dulwich/tests/test_config.py

@@ -108,6 +108,11 @@ class ConfigFileTests(TestCase):
         self.assertEqual(b"bar", cf.get((b"core",), b"foo"))
         self.assertEqual(b"bar", cf.get((b"core", b"foo"), b"foo"))
 
+    def test_from_file_multiple(self):
+        cf = self.from_file(b"[core]\nfoo = bar\nfoo = blah\n")
+        self.assertEqual([b"bar", b"blah"], list(cf.get_multivar((b"core",), b"foo")))
+        self.assertEqual([], list(cf.get_multivar((b"core", ), b"blah")))
+
     def test_from_file_utf8_bom(self):
         text = "[core]\nfoo = b\u00e4r\n".encode("utf-8-sig")
         cf = self.from_file(text)
@@ -156,6 +161,12 @@ class ConfigFileTests(TestCase):
         cf = self.from_file(b"[branch.foo]\nfoo = bar\n")
         self.assertEqual(b"bar", cf.get((b"branch", b"foo"), b"foo"))
 
+    def test_write_preserve_multivar(self):
+        cf = self.from_file(b"[core]\nfoo = bar\nfoo = blah\n")
+        f = BytesIO()
+        cf.write_to_file(f)
+        self.assertEqual(b"[core]\n\tfoo = bar\n\tfoo = blah\n", f.getvalue())
+
     def test_write_to_file_empty(self):
         c = ConfigFile()
         f = BytesIO()
@@ -257,24 +268,24 @@ class ConfigDictTests(TestCase):
         cd[b"a"] = b"b"
         self.assertEqual(cd[b"a"], b"b")
 
-    def test_iteritems(self):
+    def test_items(self):
         cd = ConfigDict()
         cd.set((b"core",), b"foo", b"bla")
         cd.set((b"core2",), b"foo", b"bloe")
 
-        self.assertEqual([(b"foo", b"bla")], list(cd.iteritems((b"core",))))
+        self.assertEqual([(b"foo", b"bla")], list(cd.items((b"core",))))
 
-    def test_iteritems_nonexistant(self):
+    def test_items_nonexistant(self):
         cd = ConfigDict()
         cd.set((b"core2",), b"foo", b"bloe")
 
-        self.assertEqual([], list(cd.iteritems((b"core",))))
+        self.assertEqual([], list(cd.items((b"core",))))
 
-    def test_itersections(self):
+    def test_sections(self):
         cd = ConfigDict()
         cd.set((b"core2",), b"foo", b"bloe")
 
-        self.assertEqual([(b"core2",)], list(cd.itersections()))
+        self.assertEqual([(b"core2",)], list(cd.sections()))
 
 
 class StackedConfigTests(TestCase):

+ 2 - 2
dulwich/tests/test_fastexport.py

@@ -197,8 +197,8 @@ M 100644 :1 a
             )
         )
         self.assertEqual(2, len(markers))
-        self.assertTrue(isinstance(self.repo[markers[b"1"]], Blob))
-        self.assertTrue(isinstance(self.repo[markers[b"2"]], Commit))
+        self.assertIsInstance(self.repo[markers[b"1"]], Blob)
+        self.assertIsInstance(self.repo[markers[b"2"]], Commit)
 
     def test_file_add(self):
         from fastimport import commands

+ 1 - 1
dulwich/tests/test_file.py

@@ -112,7 +112,7 @@ class GitFileTests(TestCase):
 
     def test_readonly(self):
         f = GitFile(self.path("foo"), "rb")
-        self.assertTrue(isinstance(f, io.IOBase))
+        self.assertIsInstance(f, io.IOBase)
         self.assertEqual(b"foo contents", f.read())
         self.assertEqual(b"", f.read())
         f.seek(4)

+ 11 - 2
dulwich/tests/test_ignore.py

@@ -82,6 +82,9 @@ TRANSLATE_TESTS = [
     (b"**/bla.c", b"(?ms)(.*/)?bla\\.c/?\\Z"),
     (b"foo/**/bar", b"(?ms)foo(/.*)?\\/bar/?\\Z"),
     (b"foo/bar/*", b"(?ms)foo\\/bar\\/[^/]+/?\\Z"),
+    (b"/foo\\[bar\\]", b"(?ms)foo\\[bar\\]/?\\Z"),
+    (b"/foo[bar]", b"(?ms)foo[bar]/?\\Z"),
+    (b"/foo[0-9]", b"(?ms)foo[0-9]/?\\Z"),
 ]
 
 
@@ -113,7 +116,7 @@ class ReadIgnorePatterns(TestCase):
 with trailing whitespace 
 with escaped trailing whitespace\\ 
 """
-        )  # noqa: W291
+        )
         self.assertEqual(
             list(read_ignore_patterns(f)),
             [
@@ -183,6 +186,12 @@ class IgnoreFilterTests(TestCase):
         self.assertFalse(filter.is_ignored(b"foo/bar/"))
         self.assertFalse(filter.is_ignored(b"foo/bar/bloe"))
 
+    def test_regex_special(self):
+        # See https://github.com/dulwich/dulwich/issues/930#issuecomment-1026166429
+        filter = IgnoreFilter([b"/foo\\[bar\\]", b"/foo"])
+        self.assertTrue(filter.is_ignored("foo"))
+        self.assertTrue(filter.is_ignored("foo[bar]"))
+
 
 class IgnoreFilterStackTests(TestCase):
     def test_stack_first(self):
@@ -239,7 +248,7 @@ class IgnoreFilterManagerTests(TestCase):
 
         with open(os.path.join(repo.path, 'foo', 'bar'), 'wb') as f:
             f.write(b'IGNORED')
-        
+
         m = IgnoreFilterManager.from_repo(repo)
         self.assertTrue(m.is_ignored('foo/bar'))
 

+ 8 - 8
dulwich/tests/test_lru_cache.py

@@ -43,18 +43,18 @@ class TestLRUCache(TestCase):
     def test_missing(self):
         cache = lru_cache.LRUCache(max_cache=10)
 
-        self.assertFalse("foo" in cache)
+        self.assertNotIn("foo", cache)
         self.assertRaises(KeyError, cache.__getitem__, "foo")
 
         cache["foo"] = "bar"
         self.assertEqual("bar", cache["foo"])
-        self.assertTrue("foo" in cache)
-        self.assertFalse("bar" in cache)
+        self.assertIn("foo", cache)
+        self.assertNotIn("bar", cache)
 
     def test_map_None(self):
         # Make sure that we can properly map None as a key.
         cache = lru_cache.LRUCache(max_cache=10)
-        self.assertFalse(None in cache)
+        self.assertNotIn(None, cache)
         cache[None] = 1
         self.assertEqual(1, cache[None])
         cache[None] = 2
@@ -80,8 +80,8 @@ class TestLRUCache(TestCase):
         # With a max cache of 1, adding 'baz' should pop out 'foo'
         cache["baz"] = "biz"
 
-        self.assertFalse("foo" in cache)
-        self.assertTrue("baz" in cache)
+        self.assertNotIn("foo", cache)
+        self.assertIn("baz", cache)
 
         self.assertEqual("biz", cache["baz"])
 
@@ -97,7 +97,7 @@ class TestLRUCache(TestCase):
         # This must kick out 'foo' because it was the last accessed
         cache["nub"] = "in"
 
-        self.assertFalse("foo" in cache)
+        self.assertNotIn("foo", cache)
 
     def test_cleanup(self):
         """Test that we can use a cleanup function."""
@@ -236,7 +236,7 @@ class TestLRUCache(TestCase):
         self.assertEqual(20, cache.get(2))
         self.assertEqual(None, cache.get(3))
         obj = object()
-        self.assertTrue(obj is cache.get(3, obj))
+        self.assertIs(obj, cache.get(3, obj))
         self.assertEqual([2, 1], [n.key for n in cache._walk_lru()])
         self.assertEqual(10, cache.get(1))
         self.assertEqual([1, 2], [n.key for n in cache._walk_lru()])

+ 4 - 3
dulwich/tests/test_missing_obj_finder.py

@@ -43,9 +43,10 @@ class MissingObjectFinderTest(TestCase):
 
     def assertMissingMatch(self, haves, wants, expected):
         for sha, path in self.store.find_missing_objects(haves, wants, set()):
-            self.assertTrue(
-                sha in expected,
-                "(%s,%s) erroneously reported as missing" % (sha, path),
+            self.assertIn(
+                sha,
+                expected,
+                "(%s,%s) erroneously reported as missing" % (sha, path)
             )
             expected.remove(sha)
 

+ 7 - 7
dulwich/tests/test_object_store.py

@@ -138,7 +138,7 @@ class ObjectStoreTests(object):
         self.assertRaises(KeyError, lambda: self.store[b"a" * 40])
 
     def test_contains_nonexistant(self):
-        self.assertFalse((b"a" * 40) in self.store)
+        self.assertNotIn(b"a" * 40, self.store)
 
     def test_add_objects_empty(self):
         self.store.add_objects([])
@@ -165,7 +165,7 @@ class ObjectStoreTests(object):
     def test_add_object(self):
         self.store.add_object(testobject)
         self.assertEqual(set([testobject.id]), set(self.store))
-        self.assertTrue(testobject.id in self.store)
+        self.assertIn(testobject.id, self.store)
         r = self.store[testobject.id]
         self.assertEqual(r, testobject)
 
@@ -173,7 +173,7 @@ class ObjectStoreTests(object):
         data = [(testobject, "mypath")]
         self.store.add_objects(data)
         self.assertEqual(set([testobject.id]), set(self.store))
-        self.assertTrue(testobject.id in self.store)
+        self.assertIn(testobject.id, self.store)
         r = self.store[testobject.id]
         self.assertEqual(r, testobject)
 
@@ -593,15 +593,15 @@ class TreeLookupPathTests(TestCase):
 
     def test_lookup_blob(self):
         o_id = tree_lookup_path(self.get_object, self.tree_id, b"a")[1]
-        self.assertTrue(isinstance(self.store[o_id], Blob))
+        self.assertIsInstance(self.store[o_id], Blob)
 
     def test_lookup_tree(self):
         o_id = tree_lookup_path(self.get_object, self.tree_id, b"ad")[1]
-        self.assertTrue(isinstance(self.store[o_id], Tree))
+        self.assertIsInstance(self.store[o_id], Tree)
         o_id = tree_lookup_path(self.get_object, self.tree_id, b"ad/bd")[1]
-        self.assertTrue(isinstance(self.store[o_id], Tree))
+        self.assertIsInstance(self.store[o_id], Tree)
         o_id = tree_lookup_path(self.get_object, self.tree_id, b"ad/bd/")[1]
-        self.assertTrue(isinstance(self.store[o_id], Tree))
+        self.assertIsInstance(self.store[o_id], Tree)
 
     def test_lookup_submodule(self):
         tree_lookup_path(self.get_object, self.tree_id, b"d")[1]

+ 12 - 12
dulwich/tests/test_objects.py

@@ -267,7 +267,7 @@ class BlobReadTests(TestCase):
     def test_stub_sha(self):
         sha = b"5" * 40
         c = make_commit(id=sha, message=b"foo")
-        self.assertTrue(isinstance(c, Commit))
+        self.assertIsInstance(c, Commit)
         self.assertEqual(sha, c.id)
         self.assertNotEqual(sha, c.sha())
 
@@ -333,7 +333,7 @@ class CommitSerializationTests(TestCase):
 
     def test_encoding(self):
         c = self.make_commit(encoding=b"iso8859-1")
-        self.assertTrue(b"encoding iso8859-1\n" in c.as_raw_string())
+        self.assertIn(b"encoding iso8859-1\n", c.as_raw_string())
 
     def test_short_timestamp(self):
         c = self.make_commit(commit_time=30)
@@ -373,11 +373,11 @@ class CommitSerializationTests(TestCase):
 
     def test_timezone(self):
         c = self.make_commit(commit_timezone=(5 * 60))
-        self.assertTrue(b" +0005\n" in c.as_raw_string())
+        self.assertIn(b" +0005\n", c.as_raw_string())
 
     def test_neg_timezone(self):
         c = self.make_commit(commit_timezone=(-1 * 3600))
-        self.assertTrue(b" -0100\n" in c.as_raw_string())
+        self.assertIn(b" -0100\n", c.as_raw_string())
 
     def test_deserialize(self):
         c = self.make_commit()
@@ -434,7 +434,7 @@ gpgsig -----BEGIN PGP SIGNATURE-----
 Merge ../b
 """,
             commit.as_raw_string(),
-        )  # noqa: W291,W293
+        )
 
     def test_serialize_mergetag(self):
         tag = make_object(
@@ -472,7 +472,7 @@ mergetag object a38d6181ff27824c79fc7df825164a212eff6a3f
 Merge ../b
 """,
             commit.as_raw_string(),
-        )  # noqa: W291,W293
+        )
 
     def test_serialize_mergetags(self):
         tag = make_object(
@@ -523,7 +523,7 @@ mergetag object a38d6181ff27824c79fc7df825164a212eff6a3f
 Merge ../b
 """,
             commit.as_raw_string(),
-        )  # noqa: W291,W293
+        )
 
     def test_deserialize_mergetag(self):
         tag = make_object(
@@ -756,7 +756,7 @@ gpgsig -----BEGIN PGP SIGNATURE-----
 
 foo
 """
-        )  # noqa: W291,W293
+        )
         self.assertEqual(b"foo\n", c.message)
         self.assertEqual([], c.extra)
         self.assertEqual(
@@ -801,7 +801,7 @@ gpgsig -----BEGIN PGP SIGNATURE-----
 
 3.3.0 version bump and docs
 """
-        )  # noqa: W291,W293
+        )
         self.assertEqual([], c.extra)
         self.assertEqual(
             b"""\
@@ -904,7 +904,7 @@ class TreeTests(ShaFileCheckTests):
 
         actual = do_sort(_TREE_ITEMS)
         self.assertEqual(_SORTED_TREE_ITEMS, actual)
-        self.assertTrue(isinstance(actual[0], TreeEntry))
+        self.assertIsInstance(actual[0], TreeEntry)
 
         # C/Python implementations may differ in specific error types, but
         # should all error on invalid inputs.
@@ -1295,9 +1295,9 @@ class ShaFileCopyTests(TestCase):
         oclass = object_class(orig.type_num)
 
         copy = orig.copy()
-        self.assertTrue(isinstance(copy, oclass))
+        self.assertIsInstance(copy, oclass)
         self.assertEqual(copy, orig)
-        self.assertTrue(copy is not orig)
+        self.assertIsNot(copy, orig)
 
     def test_commit_copy(self):
         attrs = {

+ 10 - 9
dulwich/tests/test_pack.py

@@ -390,7 +390,7 @@ class TestPack(PackTests):
 
     def test_contains(self):
         with self.get_pack(pack1_sha) as p:
-            self.assertTrue(tree_sha in p)
+            self.assertIn(tree_sha, p)
 
     def test_get(self):
         with self.get_pack(pack1_sha) as p:
@@ -527,9 +527,9 @@ class TestPack(PackTests):
             objs = {o.id: o for o in p.iterobjects()}
             self.assertEqual(3, len(objs))
             self.assertEqual(sorted(objs), sorted(p.index))
-            self.assertTrue(isinstance(objs[a_sha], Blob))
-            self.assertTrue(isinstance(objs[tree_sha], Tree))
-            self.assertTrue(isinstance(objs[commit_sha], Commit))
+            self.assertIsInstance(objs[a_sha], Blob)
+            self.assertIsInstance(objs[tree_sha], Tree)
+            self.assertIsInstance(objs[commit_sha], Commit)
 
 
 class TestThinPack(PackTests):
@@ -703,7 +703,7 @@ class BaseTestPackIndexWriting(object):
             if self._has_crc32_checksum:
                 self.assertEqual(my_crc, actual_crc)
             else:
-                self.assertTrue(actual_crc is None)
+                self.assertIsNone(actual_crc)
 
     def test_single(self):
         entry_sha = hex_to_sha("6f670c0fb53f9463760b7295fbb814e965fb20c8")
@@ -721,7 +721,7 @@ class BaseTestPackIndexWriting(object):
             if self._has_crc32_checksum:
                 self.assertEqual(my_crc, actual_crc)
             else:
-                self.assertTrue(actual_crc is None)
+                self.assertIsNone(actual_crc)
 
 
 class BaseTestFilePackIndexWriting(BaseTestPackIndexWriting):
@@ -992,9 +992,10 @@ class DeltaChainIteratorTests(TestCase):
     def get_raw_no_repeat(self, bin_sha):
         """Wrapper around store.get_raw that doesn't allow repeat lookups."""
         hex_sha = sha_to_hex(bin_sha)
-        self.assertFalse(
-            hex_sha in self.fetched,
-            "Attempted to re-fetch object %s" % hex_sha,
+        self.assertNotIn(
+            hex_sha,
+            self.fetched,
+            "Attempted to re-fetch object %s" % hex_sha
         )
         self.fetched.add(hex_sha)
         return self.store.get_raw(hex_sha)

+ 6 - 6
dulwich/tests/test_patch.py

@@ -93,7 +93,7 @@ Subject: [PATCH 1/2] Remove executable bit from prey.ico (triggers a warning).
 
 -- 
 1.7.0.4
-"""  # noqa: W291
+"""
         c, diff, version = git_am_patch_split(StringIO(text.decode("utf-8")), "utf-8")
         self.assertEqual(b"Jelmer Vernooij <jelmer@samba.org>", c.committer)
         self.assertEqual(b"Jelmer Vernooij <jelmer@samba.org>", c.author)
@@ -125,7 +125,7 @@ Subject: [PATCH 1/2] Remove executable bit from prey.ico (triggers a warning).
 
 -- 
 1.7.0.4
-"""  # noqa: W291
+"""
         c, diff, version = git_am_patch_split(BytesIO(text))
         self.assertEqual(b"Jelmer Vernooij <jelmer@samba.org>", c.committer)
         self.assertEqual(b"Jelmer Vernooij <jelmer@samba.org>", c.author)
@@ -160,7 +160,7 @@ Subject:  [Dulwich-users] [PATCH] Added unit tests for
 
 -- 
 1.7.0.4
-"""  # noqa: W291
+"""
         c, diff, version = git_am_patch_split(BytesIO(text), "utf-8")
         self.assertEqual(
             b"""\
@@ -192,7 +192,7 @@ From: Jelmer Vernooij <jelmer@debian.org>
 
 -- 
 1.7.0.4
-"""  # noqa: W291
+"""
         c, diff, version = git_am_patch_split(BytesIO(text), "utf-8")
         self.assertEqual(b"Jelmer Vernooij <jelmer@debian.org>", c.author)
         self.assertEqual(
@@ -242,7 +242,7 @@ diff --git a/dulwich/tests/test_patch.py b/dulwich/tests/test_patch.py
  
  
  class DiffTests(TestCase):
-"""  # noqa: W291,W293
+"""
         text = (
             """\
 From dulwich-users-bounces+jelmer=samba.org@lists.launchpad.net \
@@ -265,7 +265,7 @@ More help   : https://help.launchpad.net/ListHelp
 
 """
             % expected_diff
-        )  # noqa: W291
+        )
         c, diff, version = git_am_patch_split(BytesIO(text))
         self.assertEqual(expected_diff, diff)
         self.assertEqual(None, version)

+ 36 - 24
dulwich/tests/test_porcelain.py

@@ -327,7 +327,7 @@ class CommitTests(PorcelainTestCase):
             author=b"Joe <joe@example.com>",
             committer=b"Bob <bob@example.com>",
         )
-        self.assertTrue(isinstance(sha, bytes))
+        self.assertIsInstance(sha, bytes)
         self.assertEqual(len(sha), 40)
 
     def test_unicode(self):
@@ -341,7 +341,7 @@ class CommitTests(PorcelainTestCase):
             author="Joe <joe@example.com>",
             committer="Bob <bob@example.com>",
         )
-        self.assertTrue(isinstance(sha, bytes))
+        self.assertIsInstance(sha, bytes)
         self.assertEqual(len(sha), 40)
 
     def test_no_verify(self):
@@ -395,7 +395,7 @@ class CommitTests(PorcelainTestCase):
             committer="Bob <bob@example.com>",
             no_verify=True,
         )
-        self.assertTrue(isinstance(sha, bytes))
+        self.assertIsInstance(sha, bytes)
         self.assertEqual(len(sha), 40)
 
 
@@ -519,8 +519,8 @@ class CloneTests(PorcelainTestCase):
         target_repo = Repo(target_path)
         self.assertEqual(0, len(target_repo.open_index()))
         self.assertEqual(c3.id, target_repo.refs[b"refs/tags/foo"])
-        self.assertTrue(b"f1" not in os.listdir(target_path))
-        self.assertTrue(b"f2" not in os.listdir(target_path))
+        self.assertNotIn(b"f1", os.listdir(target_path))
+        self.assertNotIn(b"f2", os.listdir(target_path))
         c = r.get_config()
         encoded_path = self.repo.path
         if not isinstance(encoded_path, bytes):
@@ -551,8 +551,8 @@ class CloneTests(PorcelainTestCase):
             self.assertEqual(r.path, target_path)
         with Repo(target_path) as r:
             self.assertEqual(r.head(), c3.id)
-        self.assertTrue("f1" in os.listdir(target_path))
-        self.assertTrue("f2" in os.listdir(target_path))
+        self.assertIn("f1", os.listdir(target_path))
+        self.assertIn("f2", os.listdir(target_path))
 
     def test_bare_local_with_checkout(self):
         f1_1 = make_object(Blob, data=b"f1")
@@ -575,8 +575,8 @@ class CloneTests(PorcelainTestCase):
         with Repo(target_path) as r:
             r.head()
             self.assertRaises(NoIndexPresent, r.open_index)
-        self.assertFalse(b"f1" in os.listdir(target_path))
-        self.assertFalse(b"f2" in os.listdir(target_path))
+        self.assertNotIn(b"f1", os.listdir(target_path))
+        self.assertNotIn(b"f2", os.listdir(target_path))
 
     def test_no_checkout_with_bare(self):
         f1_1 = make_object(Blob, data=b"f1")
@@ -1094,7 +1094,7 @@ class CommitTreeTests(PorcelainTestCase):
             author=b"Joe <joe@example.com>",
             committer=b"Jane <jane@example.com>",
         )
-        self.assertTrue(isinstance(sha, bytes))
+        self.assertIsInstance(sha, bytes)
         self.assertEqual(len(sha), 40)
 
 
@@ -1135,7 +1135,7 @@ class TagCreateSignTests(PorcelainGpgTestCase):
         tags = self.repo.refs.as_dict(b"refs/tags")
         self.assertEqual(list(tags.keys()), [b"tryme"])
         tag = self.repo[b"refs/tags/tryme"]
-        self.assertTrue(isinstance(tag, Tag))
+        self.assertIsInstance(tag, Tag)
         self.assertEqual(b"foo <foo@bar.com>", tag.tagger)
         self.assertEqual(b"bar\n", tag.message)
         self.assertLess(time.time() - tag.tag_time, 5)
@@ -1178,7 +1178,7 @@ class TagCreateSignTests(PorcelainGpgTestCase):
         tags = self.repo.refs.as_dict(b"refs/tags")
         self.assertEqual(list(tags.keys()), [b"tryme"])
         tag = self.repo[b"refs/tags/tryme"]
-        self.assertTrue(isinstance(tag, Tag))
+        self.assertIsInstance(tag, Tag)
         self.assertEqual(b"foo <foo@bar.com>", tag.tagger)
         self.assertEqual(b"bar\n", tag.message)
         self.assertLess(time.time() - tag.tag_time, 5)
@@ -1205,7 +1205,7 @@ class TagCreateTests(PorcelainTestCase):
         tags = self.repo.refs.as_dict(b"refs/tags")
         self.assertEqual(list(tags.keys()), [b"tryme"])
         tag = self.repo[b"refs/tags/tryme"]
-        self.assertTrue(isinstance(tag, Tag))
+        self.assertIsInstance(tag, Tag)
         self.assertEqual(b"foo <foo@bar.com>", tag.tagger)
         self.assertEqual(b"bar\n", tag.message)
         self.assertLess(time.time() - tag.tag_time, 5)
@@ -1255,9 +1255,9 @@ class TagDeleteTests(PorcelainTestCase):
         [c1] = build_commit_graph(self.repo.object_store, [[1]])
         self.repo[b"HEAD"] = c1.id
         porcelain.tag_create(self.repo, b"foo")
-        self.assertTrue(b"foo" in porcelain.tag_list(self.repo))
+        self.assertIn(b"foo", porcelain.tag_list(self.repo))
         porcelain.tag_delete(self.repo, b"foo")
-        self.assertFalse(b"foo" in porcelain.tag_list(self.repo))
+        self.assertNotIn(b"foo", porcelain.tag_list(self.repo))
 
 
 class ResetTests(PorcelainTestCase):
@@ -2208,7 +2208,7 @@ class ReceivePackTests(PorcelainTestCase):
         outlines = outf.getvalue().splitlines()
         self.assertEqual(
             [
-                b"0091319b56ce3aee2d489f759736a79cc552c9bb86d9 HEAD\x00 report-status "  # noqa: E501
+                b"0091319b56ce3aee2d489f759736a79cc552c9bb86d9 HEAD\x00 report-status "
                 b"delete-refs quiet ofs-delta side-band-64k "
                 b"no-done symref=HEAD:refs/heads/master",
                 b"003f319b56ce3aee2d489f759736a79cc552c9bb86d9 refs/heads/master",
@@ -2254,17 +2254,17 @@ class BranchDeleteTests(PorcelainTestCase):
         [c1] = build_commit_graph(self.repo.object_store, [[1]])
         self.repo[b"HEAD"] = c1.id
         porcelain.branch_create(self.repo, b"foo")
-        self.assertTrue(b"foo" in porcelain.branch_list(self.repo))
+        self.assertIn(b"foo", porcelain.branch_list(self.repo))
         porcelain.branch_delete(self.repo, b"foo")
-        self.assertFalse(b"foo" in porcelain.branch_list(self.repo))
+        self.assertNotIn(b"foo", porcelain.branch_list(self.repo))
 
     def test_simple_unicode(self):
         [c1] = build_commit_graph(self.repo.object_store, [[1]])
         self.repo[b"HEAD"] = c1.id
         porcelain.branch_create(self.repo, "foo")
-        self.assertTrue(b"foo" in porcelain.branch_list(self.repo))
+        self.assertIn(b"foo", porcelain.branch_list(self.repo))
         porcelain.branch_delete(self.repo, "foo")
-        self.assertFalse(b"foo" in porcelain.branch_list(self.repo))
+        self.assertNotIn(b"foo", porcelain.branch_list(self.repo))
 
 
 class FetchTests(PorcelainTestCase):
@@ -2301,7 +2301,7 @@ class FetchTests(PorcelainTestCase):
             committer=b"test2 <email>",
         )
 
-        self.assertFalse(self.repo[b"HEAD"].id in target_repo)
+        self.assertNotIn(self.repo[b"HEAD"].id, target_repo)
         target_repo.close()
 
         # Fetch changes into the cloned repo
@@ -2312,7 +2312,7 @@ class FetchTests(PorcelainTestCase):
 
         # Check the target repo for pushed changes
         with Repo(target_path) as r:
-            self.assertTrue(self.repo[b"HEAD"].id in r)
+            self.assertIn(self.repo[b"HEAD"].id, r)
 
     def test_with_remote_name(self):
         remote_name = "origin"
@@ -2351,7 +2351,7 @@ class FetchTests(PorcelainTestCase):
             committer=b"test2 <email>",
         )
 
-        self.assertFalse(self.repo[b"HEAD"].id in target_repo)
+        self.assertNotIn(self.repo[b"HEAD"].id, target_repo)
 
         target_config = target_repo.get_config()
         target_config.set(
@@ -2370,7 +2370,7 @@ class FetchTests(PorcelainTestCase):
         # Check the target repo for pushed changes, as well as updates
         # for the refs
         with Repo(target_path) as r:
-            self.assertTrue(self.repo[b"HEAD"].id in r)
+            self.assertIn(self.repo[b"HEAD"].id, r)
             self.assertNotEqual(self.repo.get_refs(), target_refs)
 
     def assert_correct_remote_refs(
@@ -2813,3 +2813,15 @@ class WriteTreeTests(PorcelainTestCase):
 class ActiveBranchTests(PorcelainTestCase):
     def test_simple(self):
         self.assertEqual(b"master", porcelain.active_branch(self.repo))
+
+
+class FindUniqueAbbrevTests(PorcelainTestCase):
+
+    def test_simple(self):
+        c1, c2, c3 = build_commit_graph(
+            self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
+        )
+        self.repo.refs[b"HEAD"] = c3.id
+        self.assertEqual(
+            c1.id.decode('ascii')[:7],
+            porcelain.find_unique_abbrev(self.repo.object_store, c1.id))

+ 8 - 8
dulwich/tests/test_refs.py

@@ -274,7 +274,7 @@ class RefsContainerTests(object):
 
     def test_set_symbolic_ref_overwrite(self):
         nines = b"9" * 40
-        self.assertFalse(b"refs/heads/symbolic" in self._refs)
+        self.assertNotIn(b"refs/heads/symbolic", self._refs)
         self._refs[b"refs/heads/symbolic"] = nines
         self.assertEqual(nines, self._refs.read_loose_ref(b"refs/heads/symbolic"))
         self._refs.set_symbolic_ref(b"refs/heads/symbolic", b"refs/heads/master")
@@ -298,8 +298,8 @@ class RefsContainerTests(object):
         )
 
     def test_contains(self):
-        self.assertTrue(b"refs/heads/master" in self._refs)
-        self.assertFalse(b"refs/heads/bar" in self._refs)
+        self.assertIn(b"refs/heads/master", self._refs)
+        self.assertNotIn(b"refs/heads/bar", self._refs)
 
     def test_delitem(self):
         self.assertEqual(
@@ -321,7 +321,7 @@ class RefsContainerTests(object):
             )
         )
         self.assertTrue(self._refs.remove_if_equals(b"refs/tags/refs-0.2", ZERO_SHA))
-        self.assertFalse(b"refs/tags/refs-0.2" in self._refs)
+        self.assertNotIn(b"refs/tags/refs-0.2", self._refs)
 
     def test_import_refs_name(self):
         self._refs[
@@ -526,7 +526,7 @@ class DiskRefsContainerTests(RefsContainerTests, TestCase):
 
         nines = b"9" * 40
         self.assertEqual(b"ref: refs/heads/master", refs.read_ref(b"HEAD"))
-        self.assertFalse(b"refs/heads/master" in refs)
+        self.assertNotIn(b"refs/heads/master", refs)
         self.assertTrue(refs.add_if_new(b"HEAD", nines))
         self.assertEqual(b"ref: refs/heads/master", refs.read_ref(b"HEAD"))
         self.assertEqual(nines, refs[b"HEAD"])
@@ -556,7 +556,7 @@ class DiskRefsContainerTests(RefsContainerTests, TestCase):
         RefsContainerTests.test_delitem(self)
         ref_file = os.path.join(self._refs.path, b"refs", b"heads", b"master")
         self.assertFalse(os.path.exists(ref_file))
-        self.assertFalse(b"refs/heads/master" in self._refs.get_packed_refs())
+        self.assertNotIn(b"refs/heads/master", self._refs.get_packed_refs())
 
     def test_delitem_symbolic(self):
         self.assertEqual(b"ref: refs/heads/master", self._refs.read_loose_ref(b"HEAD"))
@@ -745,8 +745,8 @@ class InfoRefsContainerTests(TestCase):
 
     def test_contains(self):
         refs = InfoRefsContainer(BytesIO(_TEST_REFS_SERIALIZED))
-        self.assertTrue(b"refs/heads/master" in refs)
-        self.assertFalse(b"refs/heads/bar" in refs)
+        self.assertIn(b"refs/heads/master", refs)
+        self.assertNotIn(b"refs/heads/bar", refs)
 
     def test_get_peeled(self):
         refs = InfoRefsContainer(BytesIO(_TEST_REFS_SERIALIZED))

+ 7 - 7
dulwich/tests/test_repository.py

@@ -75,12 +75,12 @@ class CreateRepositoryTests(TestCase):
         barestr = b"bare = " + str(expect_bare).lower().encode("ascii")
         with repo.get_named_file("config") as f:
             config_text = f.read()
-            self.assertTrue(barestr in config_text, "%r" % config_text)
+            self.assertIn(barestr, config_text, "%r" % config_text)
         expect_filemode = sys.platform != "win32"
         barestr = b"filemode = " + str(expect_filemode).lower().encode("ascii")
         with repo.get_named_file("config") as f:
             config_text = f.read()
-            self.assertTrue(barestr in config_text, "%r" % config_text)
+            self.assertIn(barestr, config_text, "%r" % config_text)
 
         if isinstance(repo, Repo):
             expected_mode = '0o100644' if expect_filemode else '0o100666'
@@ -224,12 +224,12 @@ class RepositoryRootTests(TestCase):
 
     def test_contains_object(self):
         r = self.open_repo("a.git")
-        self.assertTrue(r.head() in r)
-        self.assertFalse(b"z" * 40 in r)
+        self.assertIn(r.head(), r)
+        self.assertNotIn(b"z" * 40, r)
 
     def test_contains_ref(self):
         r = self.open_repo("a.git")
-        self.assertTrue(b"HEAD" in r)
+        self.assertIn(b"HEAD", r)
 
     def test_get_no_description(self):
         r = self.open_repo("a.git")
@@ -249,7 +249,7 @@ class RepositoryRootTests(TestCase):
 
     def test_contains_missing(self):
         r = self.open_repo("a.git")
-        self.assertFalse(b"bar" in r)
+        self.assertNotIn(b"bar", r)
 
     def test_get_peeled(self):
         # unpacked ref
@@ -1210,7 +1210,7 @@ class BuildRepoRootTests(TestCase):
         self.assertEqual(self._root_commit, r[b"HEAD"].id)
         self.assertEqual(commit_sha, r[b"refs/heads/new_branch"].id)
         self.assertEqual([], r[commit_sha].parents)
-        self.assertTrue(b"refs/heads/new_branch" in r)
+        self.assertIn(b"refs/heads/new_branch", r)
 
         new_branch_head = commit_sha
 

+ 1 - 1
dulwich/tests/test_server.py

@@ -148,7 +148,7 @@ class HandlerTestCase(TestCase):
 
         # ignore innocuous but unknown capabilities
         self.assertRaises(GitProtocolError, set_caps, [b"cap2", b"ignoreme"])
-        self.assertFalse(b"ignoreme" in self._handler.capabilities())
+        self.assertNotIn(b"ignoreme", self._handler.capabilities())
         self._handler.innocuous_capabilities = lambda: (b"ignoreme",)
         self.assertSucceeds(set_caps, [b"cap2", b"ignoreme"])
 

+ 4 - 4
dulwich/tests/test_utils.py

@@ -43,20 +43,20 @@ class BuildCommitGraphTest(TestCase):
     def test_linear(self):
         c1, c2 = build_commit_graph(self.store, [[1], [2, 1]])
         for obj_id in [c1.id, c2.id, c1.tree, c2.tree]:
-            self.assertTrue(obj_id in self.store)
+            self.assertIn(obj_id, self.store)
         self.assertEqual([], c1.parents)
         self.assertEqual([c1.id], c2.parents)
         self.assertEqual(c1.tree, c2.tree)
         self.assertEqual([], self.store[c1.tree].items())
-        self.assertTrue(c2.commit_time > c1.commit_time)
+        self.assertGreater(c2.commit_time, c1.commit_time)
 
     def test_merge(self):
         c1, c2, c3, c4 = build_commit_graph(
             self.store, [[1], [2, 1], [3, 1], [4, 2, 3]]
         )
         self.assertEqual([c2.id, c3.id], c4.parents)
-        self.assertTrue(c4.commit_time > c2.commit_time)
-        self.assertTrue(c4.commit_time > c3.commit_time)
+        self.assertGreater(c4.commit_time, c2.commit_time)
+        self.assertGreater(c4.commit_time, c3.commit_time)
 
     def test_missing_parent(self):
         self.assertRaises(

+ 3 - 3
dulwich/tests/test_web.py

@@ -131,7 +131,7 @@ class WebTestCase(TestCase):
         return None
 
     def assertContentTypeEquals(self, expected):
-        self.assertTrue(("Content-Type", expected) in self._headers)
+        self.assertIn(("Content-Type", expected), self._headers)
 
 
 def _test_backend(objects, refs=None, named_files=None):
@@ -355,7 +355,7 @@ class SmartHandlersTestCase(WebTestCase):
         mat = re.search(".*", "/git-evil-handler")
         content = list(handle_service_request(self._req, "backend", mat))
         self.assertEqual(HTTP_FORBIDDEN, self._status)
-        self.assertFalse(b"git-evil-handler" in b"".join(content))
+        self.assertNotIn(b"git-evil-handler", b"".join(content))
         self.assertFalse(self._req.cached)
 
     def _run_handle_service_request(self, content_length=None):
@@ -396,7 +396,7 @@ class SmartHandlersTestCase(WebTestCase):
 
         mat = re.search(".*", "/git-evil-pack")
         content = list(get_info_refs(self._req, Backend(), mat))
-        self.assertFalse(b"git-evil-handler" in b"".join(content))
+        self.assertNotIn(b"git-evil-handler", b"".join(content))
         self.assertEqual(HTTP_FORBIDDEN, self._status)
         self.assertFalse(self._req.cached)
 

+ 4 - 2
dulwich/web.py

@@ -286,7 +286,8 @@ def handle_service_request(req, backend, mat):
 class HTTPGitRequest(object):
     """Class encapsulating the state of a single git HTTP request.
 
-    :ivar environ: the WSGI environment for the request.
+    Attributes:
+      environ: the WSGI environment for the request.
     """
 
     def __init__(self, environ, start_response, dumb: bool = False, handlers=None):
@@ -358,7 +359,8 @@ class HTTPGitRequest(object):
 class HTTPGitApplication(object):
     """Class encapsulating the state of a git WSGI application.
 
-    :ivar backend: the Backend object backing this application
+    Attributes:
+      backend: the Backend object backing this application
     """
 
     services = {

+ 1 - 0
releaser.conf

@@ -3,6 +3,7 @@ news_file: "NEWS"
 timeout_days: 5
 tag_name: "dulwich-$VERSION"
 verify_command: "flake8 && make check"
+github_url: "https://github.com/dulwich/dulwich"
 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.31'
+dulwich_version_string = '0.20.35'
 
 
 class DulwichDistribution(Distribution):