Commit Diff


commit - 0c434308d1ee1b1b9abd345f147baa4ddb578585
commit + e13cc98a4f8ca8afc857730719164e4608c28d3f
blob - 2762e93d176e655d090d0063bd9b2fa9c837f649
blob + c68de5a472fda8cf8f731d472dae6ec625d41c94
--- .github/workflows/pythonpackage.yml
+++ .github/workflows/pythonpackage.yml
@@ -1,6 +1,10 @@
 name: Python package
 
-on: [push, pull_request]
+on:
+  push:
+  pull_request:
+  schedule:
+    - cron: '0 6 * * *'  # Daily 6AM UTC build
 
 jobs:
   build:
@@ -9,7 +13,7 @@ jobs:
     strategy:
       matrix:
         os: [ubuntu-latest, macos-latest, windows-latest]
-        python-version: [3.5, 3.6, 3.7, 3.8, 3.9, pypy3]
+        python-version: [3.5, 3.6, 3.7, 3.8, 3.9, 3.10-dev, pypy3]
         exclude:
           # sqlite3 exit handling seems to get in the way
           - os: macos-latest
@@ -46,7 +50,7 @@ jobs:
       if: "matrix.os != 'windows-latest' && matrix.python-version != 'pypy3'"
     - name: Install mypy
       run: |
-        pip install -U mypy
+        pip install -U mypy types-paramiko types-certifi
       if: "matrix.python-version != 'pypy3'"
     - name: Style checks
       run: |
blob - 7eaeae907ad187a5b602325d2be7510490c74357
blob + 953bbaf1c3d45903f5987567909163d9d9385983
--- NEWS
+++ NEWS
@@ -1,3 +1,24 @@
+0.20.25	2021-08-23
+
+ * Fix ``dulwich`` script when installed via setup.py.
+   (Dan Villiom Podlaski Christiansen)
+
+ * Make default file mask consistent
+   with Git. (Dan Villiom Podlaski Christiansen, #884)
+
+0.20.24	2021-07-18
+
+ * config: disregard UTF-8 BOM when reading file.
+   (Dan Villiom Podlaski Christiansen)
+
+ * Skip lines with spaces only in .gitignore. (Andrey Torsunov, #878)
+
+ * Add a separate HTTPProxyUnauthorized exception for 407 errors.
+   (Jelmer Vernooij, #822)
+
+ * Split out a AbstractHTTPGitClient class.
+   (Jelmer Vernooij)
+
 0.20.23	2021-05-24
 
  * Fix installation of GPG during package publishing.
@@ -282,7 +303,7 @@
  BUG FIXES
 
  * Avoid ``PermissionError``, since it is Python3-specific.
-  (Jelmer Vernooij)
+   (Jelmer Vernooij)
 
  * Fix regression that added a dependency on C git for the
    test suite. (Jelmer Vernooij, #720)
blob - e54aeeed9312dfef00c8d803064cdd5db15255df
blob + f42454ceef107f4fb2a83f9dfedde4aad6197efb
--- PKG-INFO
+++ PKG-INFO
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: dulwich
-Version: 0.20.23
+Version: 0.20.25
 Summary: Python Git Library
 Home-page: https://www.dulwich.io/
 Author: Jelmer Vernooij
@@ -9,102 +9,6 @@ License: Apachev2 or later or GPLv2
 Project-URL: Bug Tracker, https://github.com/dulwich/dulwich/issues
 Project-URL: Repository, https://www.dulwich.io/code/
 Project-URL: GitHub, https://github.com/dulwich/dulwich
-Description: Dulwich
-        =======
-        
-        This is the Dulwich project.
-        
-        It aims to provide an interface to git repos (both local and remote) that
-        doesn't call out to git directly but instead uses pure Python.
-        
-        **Main website**: <https://www.dulwich.io/>
-        
-        **License**: Apache License, version 2 or GNU General Public License, version 2 or later.
-        
-        The project is named after the part of London that Mr. and Mrs. Git live in
-        in the particular Monty Python sketch.
-        
-        Installation
-        ------------
-        
-        By default, Dulwich' setup.py will attempt to build and install the optional C
-        extensions. The reason for this is that they significantly improve the performance
-        since some low-level operations that are executed often are much slower in CPython.
-        
-        If you don't want to install the C bindings, specify the --pure argument to setup.py::
-        
-            $ python setup.py --pure install
-        
-        or if you are installing from pip::
-        
-            $ pip install dulwich --global-option="--pure"
-        
-        Note that you can also specify --global-option in a
-        `requirements.txt <https://pip.pypa.io/en/stable/reference/pip_install/#requirement-specifiers>`_
-        file, e.g. like this::
-        
-            dulwich --global-option=--pure
-        
-        Getting started
-        ---------------
-        
-        Dulwich comes with both a lower-level API and higher-level plumbing ("porcelain").
-        
-        For example, to use the lower level API to access the commit message of the
-        last commit::
-        
-            >>> from dulwich.repo import Repo
-            >>> r = Repo('.')
-            >>> r.head()
-            '57fbe010446356833a6ad1600059d80b1e731e15'
-            >>> c = r[r.head()]
-            >>> c
-            <Commit 015fc1267258458901a94d228e39f0a378370466>
-            >>> c.message
-            'Add note about encoding.\n'
-        
-        And to print it using porcelain::
-        
-            >>> from dulwich import porcelain
-            >>> porcelain.log('.', max_entries=1)
-            --------------------------------------------------
-            commit: 57fbe010446356833a6ad1600059d80b1e731e15
-            Author: Jelmer Vernooij <jelmer@jelmer.uk>
-            Date:   Sat Apr 29 2017 23:57:34 +0000
-        
-            Add note about encoding.
-        
-        Further documentation
-        ---------------------
-        
-        The dulwich documentation can be found in docs/ and built by running ``make
-        doc``. It can also be found `on the web <https://www.dulwich.io/docs/>`_.
-        
-        Help
-        ----
-        
-        There is a *#dulwich* IRC channel on the `OFTC <https://www.oftc.net/>`_, and
-        `dulwich-announce <https://groups.google.com/forum/#!forum/dulwich-announce>`_
-        and `dulwich-discuss <https://groups.google.com/forum/#!forum/dulwich-discuss>`_
-        mailing lists.
-        
-        Contributing
-        ------------
-        
-        For a full list of contributors, see the git logs or `AUTHORS <AUTHORS>`_.
-        
-        If you'd like to contribute to Dulwich, see the `CONTRIBUTING <CONTRIBUTING.rst>`_
-        file and `list of open issues <https://github.com/dulwich/dulwich/issues>`_.
-        
-        Supported versions of Python
-        ----------------------------
-        
-        At the moment, Dulwich supports (and is tested on) CPython 3.5 and later and
-        Pypy.
-        
-        The latest release series to support Python 2.x was the 0.19 series. See
-        the 0.19 branch in the Dulwich git repository.
-        
 Keywords: git vcs
 Platform: UNKNOWN
 Classifier: Development Status :: 4 - Beta
@@ -124,3 +28,103 @@ Provides-Extra: fastimport
 Provides-Extra: https
 Provides-Extra: pgp
 Provides-Extra: watch
+License-File: COPYING
+License-File: AUTHORS
+
+Dulwich
+=======
+
+This is the Dulwich project.
+
+It aims to provide an interface to git repos (both local and remote) that
+doesn't call out to git directly but instead uses pure Python.
+
+**Main website**: <https://www.dulwich.io/>
+
+**License**: Apache License, version 2 or GNU General Public License, version 2 or later.
+
+The project is named after the part of London that Mr. and Mrs. Git live in
+in the particular Monty Python sketch.
+
+Installation
+------------
+
+By default, Dulwich' setup.py will attempt to build and install the optional C
+extensions. The reason for this is that they significantly improve the performance
+since some low-level operations that are executed often are much slower in CPython.
+
+If you don't want to install the C bindings, specify the --pure argument to setup.py::
+
+    $ python setup.py --pure install
+
+or if you are installing from pip::
+
+    $ pip install dulwich --global-option="--pure"
+
+Note that you can also specify --global-option in a
+`requirements.txt <https://pip.pypa.io/en/stable/reference/pip_install/#requirement-specifiers>`_
+file, e.g. like this::
+
+    dulwich --global-option=--pure
+
+Getting started
+---------------
+
+Dulwich comes with both a lower-level API and higher-level plumbing ("porcelain").
+
+For example, to use the lower level API to access the commit message of the
+last commit::
+
+    >>> from dulwich.repo import Repo
+    >>> r = Repo('.')
+    >>> r.head()
+    '57fbe010446356833a6ad1600059d80b1e731e15'
+    >>> c = r[r.head()]
+    >>> c
+    <Commit 015fc1267258458901a94d228e39f0a378370466>
+    >>> c.message
+    'Add note about encoding.\n'
+
+And to print it using porcelain::
+
+    >>> from dulwich import porcelain
+    >>> porcelain.log('.', max_entries=1)
+    --------------------------------------------------
+    commit: 57fbe010446356833a6ad1600059d80b1e731e15
+    Author: Jelmer Vernooij <jelmer@jelmer.uk>
+    Date:   Sat Apr 29 2017 23:57:34 +0000
+
+    Add note about encoding.
+
+Further documentation
+---------------------
+
+The dulwich documentation can be found in docs/ and built by running ``make
+doc``. It can also be found `on the web <https://www.dulwich.io/docs/>`_.
+
+Help
+----
+
+There is a *#dulwich* IRC channel on the `OFTC <https://www.oftc.net/>`_, and
+`dulwich-announce <https://groups.google.com/forum/#!forum/dulwich-announce>`_
+and `dulwich-discuss <https://groups.google.com/forum/#!forum/dulwich-discuss>`_
+mailing lists.
+
+Contributing
+------------
+
+For a full list of contributors, see the git logs or `AUTHORS <AUTHORS>`_.
+
+If you'd like to contribute to Dulwich, see the `CONTRIBUTING <CONTRIBUTING.rst>`_
+file and `list of open issues <https://github.com/dulwich/dulwich/issues>`_.
+
+Supported versions of Python
+----------------------------
+
+At the moment, Dulwich supports (and is tested on) CPython 3.5 and later and
+Pypy.
+
+The latest release series to support Python 2.x was the 0.19 series. See
+the 0.19 branch in the Dulwich git repository.
+
+
blob - 23df2b69b8598fb4a22f6c2fa5a9ca1d699e0bf6 (mode 644)
blob + /dev/null
--- build.cmd
+++ /dev/null
@@ -1,21 +0,0 @@
-@echo off
-:: To build extensions for 64 bit Python 3, we need to configure environment
-:: variables to use the MSVC 2010 C++ compilers from GRMSDKX_EN_DVD.iso of:
-:: MS Windows SDK for Windows 7 and .NET Framework 4
-::
-:: More details at:
-:: https://github.com/cython/cython/wiki/CythonExtensionsOnWindows
-
-IF "%DISTUTILS_USE_SDK%"=="1" (
-    ECHO Configuring environment to build with MSVC on a 64bit architecture
-    ECHO Using Windows SDK 7.1
-    "C:\Program Files\Microsoft SDKs\Windows\v7.1\Setup\WindowsSdkVer.exe" -q -version:v7.1
-    CALL "C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\SetEnv.cmd" /x64 /release
-    SET MSSdk=1
-    REM Need the following to allow tox to see the SDK compiler
-    SET TOX_TESTENV_PASSENV=DISTUTILS_USE_SDK MSSdk INCLUDE LIB
-) ELSE (
-    ECHO Using default MSVC build environment
-)
-
-CALL %*
blob - 46661a032954a650806d5726c2539e2e0ee12cba
blob + 17705982feb817acf7ec992a221782feac8d18e6
--- dulwich/__init__.py
+++ dulwich/__init__.py
@@ -22,4 +22,4 @@
 
 """Python implementation of the Git file formats and protocols."""
 
-__version__ = (0, 20, 23)
+__version__ = (0, 20, 25)
blob - /dev/null
blob + 3453a7e6d4cae4b300f04411042416a7eb7f3963 (mode 644)
--- /dev/null
+++ dulwich/__main__.py
@@ -0,0 +1,4 @@
+from . import cli
+
+if __name__ == "__main__":
+    cli._main()
blob - bcdd094b0d0e89eb1bfa26849b831f4fa64a8d1f
blob + d0cedf7479da398329c5c53b34651510a932c13e
--- dulwich/cli.py
+++ dulwich/cli.py
@@ -731,7 +731,7 @@ commands = {
 
 def main(argv=None):
     if argv is None:
-        argv = sys.argv
+        argv = sys.argv[1:]
 
     if len(argv) < 1:
         print("Usage: dulwich <%s> [OPTIONS...]" % ("|".join(commands.keys())))
@@ -747,9 +747,13 @@ def main(argv=None):
     return cmd_kls().run(argv[1:])
 
 
-if __name__ == "__main__":
+def _main():
     if "DULWICH_PDB" in os.environ and getattr(signal, "SIGQUIT", None):
         signal.signal(signal.SIGQUIT, signal_quit)  # type: ignore
     signal.signal(signal.SIGINT, signal_int)
 
-    sys.exit(main(sys.argv[1:]))
+    sys.exit(main())
+
+
+if __name__ == "__main__":
+    _main()
blob - 8cc1f40c9d55657633f6e2f98cd540f28b8ce803
blob + 2527853c78580ce737d656858002f15513a3bc57
--- dulwich/client.py
+++ dulwich/client.py
@@ -130,6 +130,15 @@ class HTTPUnauthorized(Exception):
     def __init__(self, www_authenticate, url):
         Exception.__init__(self, "No valid credentials provided")
         self.www_authenticate = www_authenticate
+        self.url = url
+
+
+class HTTPProxyUnauthorized(Exception):
+    """Raised when proxy authentication fails."""
+
+    def __init__(self, proxy_authenticate, url):
+        Exception.__init__(self, "No valid proxy credentials provided")
+        self.proxy_authenticate = proxy_authenticate
         self.url = url
 
 
@@ -549,8 +558,8 @@ class GitClient(object):
         Args:
           path: Remote path to fetch from
           determine_wants: Function determine what refs
-        to fetch. Receives dictionary of name->sha, should return
-        list of shas to fetch.
+            to fetch. Receives dictionary of name->sha, should return
+            list of shas to fetch.
           graph_walker: Object with next() and ack().
           pack_data: Callback called for each bit of data in the pack
           progress: Callback for progress reports (strings)
@@ -901,10 +910,10 @@ class TraditionalGitClient(GitClient):
         Args:
           path: Repository path (as bytestring)
           update_refs: Function to determine changes to remote refs.
-        Receive dict with existing remote refs, returns dict with
-        changed refs (name -> sha, where sha=ZERO_SHA for deletions)
+            Receive dict with existing remote refs, returns dict with
+            changed refs (name -> sha, where sha=ZERO_SHA for deletions)
           generate_pack_data: Function that can return a tuple with
-        number of objects and pack data to upload.
+            number of objects and pack data to upload.
           progress: Optional callback called with progress updates
 
         Returns:
@@ -995,8 +1004,8 @@ class TraditionalGitClient(GitClient):
         Args:
           path: Remote path to fetch from
           determine_wants: Function determine what refs
-        to fetch. Receives dictionary of name->sha, should return
-        list of shas to fetch.
+            to fetch. Receives dictionary of name->sha, should return
+            list of shas to fetch.
           graph_walker: Object with next() and ack().
           pack_data: Callback called for each bit of data in the pack
           progress: Callback for progress reports (strings)
@@ -1292,9 +1301,9 @@ class LocalGitClient(GitClient):
         Args:
           path: Repository path (as bytestring)
           update_refs: Function to determine changes to remote refs.
-        Receive dict with existing remote refs, returns dict with
-        changed refs (name -> sha, where sha=ZERO_SHA for deletions)
-        with number of items and pack data to upload.
+            Receive dict with existing remote refs, returns dict with
+            changed refs (name -> sha, where sha=ZERO_SHA for deletions)
+            with number of items and pack data to upload.
           progress: Optional progress function
 
         Returns:
@@ -1353,8 +1362,8 @@ class LocalGitClient(GitClient):
           path: Path to fetch from (as bytestring)
           target: Target repository to fetch into
           determine_wants: Optional function determine what refs
-        to fetch. Receives dictionary of name->sha, should return
-        list of shas to fetch. Defaults to all shas.
+            to fetch. Receives dictionary of name->sha, should return
+            list of shas to fetch. Defaults to all shas.
           progress: Optional progress function
           depth: Shallow fetch depth
 
@@ -1385,8 +1394,8 @@ class LocalGitClient(GitClient):
         Args:
           path: Remote path to fetch from
           determine_wants: Function determine what refs
-        to fetch. Receives dictionary of name->sha, should return
-        list of shas to fetch.
+            to fetch. Receives dictionary of name->sha, should return
+            list of shas to fetch.
           graph_walker: Object with next() and ack().
           pack_data: Callback called for each bit of data in the pack
           progress: Callback for progress reports (strings)
@@ -1759,8 +1768,6 @@ def default_urllib3_manager(   # noqa: C901
     if proxy_server is not None:
         if proxy_manager_cls is None:
             proxy_manager_cls = urllib3.ProxyManager
-        # `urllib3` requires a `str` object in both Python 2 and 3, while
-        # `ConfigDict` coerces entries to `bytes` on Python 3. Compensate.
         if not isinstance(proxy_server, str):
             proxy_server = proxy_server.decode()
         manager = proxy_manager_cls(proxy_server, headers=headers, **kwargs)
@@ -1772,71 +1779,20 @@ def default_urllib3_manager(   # noqa: C901
     return manager
 
 
-class HttpGitClient(GitClient):
-    def __init__(
-        self,
-        base_url,
-        dumb=None,
-        pool_manager=None,
-        config=None,
-        username=None,
-        password=None,
-        **kwargs
-    ):
-        self._base_url = base_url.rstrip("/") + "/"
-        self._username = username
-        self._password = password
-        self.dumb = dumb
+class AbstractHttpGitClient(GitClient):
+    """Abstract base class for HTTP Git Clients.
 
-        if pool_manager is None:
-            self.pool_manager = default_urllib3_manager(config)
-        else:
-            self.pool_manager = pool_manager
+    This is agonistic of the actual HTTP implementation.
 
-        if username is not None:
-            # No escaping needed: ":" is not allowed in username:
-            # https://tools.ietf.org/html/rfc2617#section-2
-            credentials = "%s:%s" % (username, password)
-            import urllib3.util
-
-            basic_auth = urllib3.util.make_headers(basic_auth=credentials)
-            self.pool_manager.headers.update(basic_auth)
+    Subclasses should provide an implementation of the
+    _http_request method.
+    """
 
+    def __init__(self, base_url, dumb=False, **kwargs):
+        self._base_url = base_url.rstrip("/") + "/"
+        self.dumb = dumb
         GitClient.__init__(self, **kwargs)
 
-    def get_url(self, path):
-        return self._get_url(path).rstrip("/")
-
-    @classmethod
-    def from_parsedurl(cls, parsedurl, **kwargs):
-        password = parsedurl.password
-        if password is not None:
-            kwargs["password"] = urlunquote(password)
-        username = parsedurl.username
-        if username is not None:
-            kwargs["username"] = urlunquote(username)
-        netloc = parsedurl.hostname
-        if parsedurl.port:
-            netloc = "%s:%s" % (netloc, parsedurl.port)
-        if parsedurl.username:
-            netloc = "%s@%s" % (parsedurl.username, netloc)
-        parsedurl = parsedurl._replace(netloc=netloc)
-        return cls(urlunparse(parsedurl), **kwargs)
-
-    def __repr__(self):
-        return "%s(%r, dumb=%r)" % (
-            type(self).__name__,
-            self._base_url,
-            self.dumb,
-        )
-
-    def _get_url(self, path):
-        if not isinstance(path, str):
-            # urllib3.util.url._encode_invalid_chars() converts the path back
-            # to bytes using the utf-8 codec.
-            path = path.decode("utf-8")
-        return urljoin(self._base_url, path).rstrip("/") + "/"
-
     def _http_request(self, url, headers=None, data=None, allow_compression=False):
         """Perform HTTP request.
 
@@ -1853,48 +1809,8 @@ class HttpGitClient(GitClient):
           method for the response data.
 
         """
-        req_headers = self.pool_manager.headers.copy()
-        if headers is not None:
-            req_headers.update(headers)
-        req_headers["Pragma"] = "no-cache"
-        if allow_compression:
-            req_headers["Accept-Encoding"] = "gzip"
-        else:
-            req_headers["Accept-Encoding"] = "identity"
 
-        if data is None:
-            resp = self.pool_manager.request("GET", url, headers=req_headers)
-        else:
-            resp = self.pool_manager.request(
-                "POST", url, headers=req_headers, body=data
-            )
-
-        if resp.status == 404:
-            raise NotGitRepository()
-        if resp.status == 401:
-            raise HTTPUnauthorized(resp.getheader("WWW-Authenticate"), url)
-        if resp.status != 200:
-            raise GitProtocolError(
-                "unexpected http resp %d for %s" % (resp.status, url)
-            )
-
-        # TODO: Optimization available by adding `preload_content=False` to the
-        # request and just passing the `read` method on instead of going via
-        # `BytesIO`, if we can guarantee that the entire response is consumed
-        # before issuing the next to still allow for connection reuse from the
-        # pool.
-        read = BytesIO(resp.data).read
-
-        resp.content_type = resp.getheader("Content-Type")
-        # Check if geturl() is available (urllib3 version >= 1.23)
-        try:
-            resp_url = resp.geturl()
-        except AttributeError:
-            # get_redirect_location() is available for urllib3 >= 1.1
-            resp.redirect_location = resp.get_redirect_location()
-        else:
-            resp.redirect_location = resp_url if resp_url != url else ""
-        return resp, read
+        raise NotImplementedError(self._http_request)
 
     def _discover_references(self, service, base_url):
         assert base_url[-1] == "/"
@@ -1934,6 +1850,11 @@ class HttpGitClient(GitClient):
             resp.close()
 
     def _smart_request(self, service, url, data):
+        """Send a 'smart' HTTP request.
+
+        This is a simple wrapper around _http_request that sets
+        a couple of extra headers.
+        """
         assert url[-1] == "/"
         url = urljoin(url, service)
         result_content_type = "application/x-%s-result" % service
@@ -1955,10 +1876,10 @@ class HttpGitClient(GitClient):
         Args:
           path: Repository path (as bytestring)
           update_refs: Function to determine changes to remote refs.
-        Receives dict with existing remote refs, returns dict with
-        changed refs (name -> sha, where sha=ZERO_SHA for deletions)
+            Receives dict with existing remote refs, returns dict with
+            changed refs (name -> sha, where sha=ZERO_SHA for deletions)
           generate_pack_data: Function that can return a tuple
-        with number of elements and pack data to upload.
+            with number of elements and pack data to upload.
           progress: Optional progress function
 
         Returns:
@@ -2089,7 +2010,124 @@ class HttpGitClient(GitClient):
         refs, _, _ = self._discover_references(b"git-upload-pack", url)
         return refs
 
+    def get_url(self, path):
+        return self._get_url(path).rstrip("/")
 
+    def _get_url(self, path):
+        return urljoin(self._base_url, path).rstrip("/") + "/"
+
+    @classmethod
+    def from_parsedurl(cls, parsedurl, **kwargs):
+        password = parsedurl.password
+        if password is not None:
+            kwargs["password"] = urlunquote(password)
+        username = parsedurl.username
+        if username is not None:
+            kwargs["username"] = urlunquote(username)
+        netloc = parsedurl.hostname
+        if parsedurl.port:
+            netloc = "%s:%s" % (netloc, parsedurl.port)
+        if parsedurl.username:
+            netloc = "%s@%s" % (parsedurl.username, netloc)
+        parsedurl = parsedurl._replace(netloc=netloc)
+        return cls(urlunparse(parsedurl), **kwargs)
+
+    def __repr__(self):
+        return "%s(%r, dumb=%r)" % (
+            type(self).__name__,
+            self._base_url,
+            self.dumb,
+        )
+
+
+class Urllib3HttpGitClient(AbstractHttpGitClient):
+    def __init__(
+        self,
+        base_url,
+        dumb=None,
+        pool_manager=None,
+        config=None,
+        username=None,
+        password=None,
+        **kwargs
+    ):
+        self._username = username
+        self._password = password
+
+        if pool_manager is None:
+            self.pool_manager = default_urllib3_manager(config)
+        else:
+            self.pool_manager = pool_manager
+
+        if username is not None:
+            # No escaping needed: ":" is not allowed in username:
+            # https://tools.ietf.org/html/rfc2617#section-2
+            credentials = "%s:%s" % (username, password)
+            import urllib3.util
+
+            basic_auth = urllib3.util.make_headers(basic_auth=credentials)
+            self.pool_manager.headers.update(basic_auth)
+
+        super(Urllib3HttpGitClient, self).__init__(
+            base_url=base_url, dumb=dumb, **kwargs)
+
+    def _get_url(self, path):
+        if not isinstance(path, str):
+            # urllib3.util.url._encode_invalid_chars() converts the path back
+            # to bytes using the utf-8 codec.
+            path = path.decode("utf-8")
+        return urljoin(self._base_url, path).rstrip("/") + "/"
+
+    def _http_request(self, url, headers=None, data=None, allow_compression=False):
+        req_headers = self.pool_manager.headers.copy()
+        if headers is not None:
+            req_headers.update(headers)
+        req_headers["Pragma"] = "no-cache"
+        if allow_compression:
+            req_headers["Accept-Encoding"] = "gzip"
+        else:
+            req_headers["Accept-Encoding"] = "identity"
+
+        if data is None:
+            resp = self.pool_manager.request("GET", url, headers=req_headers)
+        else:
+            resp = self.pool_manager.request(
+                "POST", url, headers=req_headers, body=data
+            )
+
+        if resp.status == 404:
+            raise NotGitRepository()
+        if resp.status == 401:
+            raise HTTPUnauthorized(resp.getheader("WWW-Authenticate"), url)
+        if resp.status == 407:
+            raise HTTPProxyUnauthorized(resp.getheader("Proxy-Authenticate"), url)
+        if resp.status != 200:
+            raise GitProtocolError(
+                "unexpected http resp %d for %s" % (resp.status, url)
+            )
+
+        # TODO: Optimization available by adding `preload_content=False` to the
+        # request and just passing the `read` method on instead of going via
+        # `BytesIO`, if we can guarantee that the entire response is consumed
+        # before issuing the next to still allow for connection reuse from the
+        # pool.
+        read = BytesIO(resp.data).read
+
+        resp.content_type = resp.getheader("Content-Type")
+        # Check if geturl() is available (urllib3 version >= 1.23)
+        try:
+            resp_url = resp.geturl()
+        except AttributeError:
+            # get_redirect_location() is available for urllib3 >= 1.1
+            resp.redirect_location = resp.get_redirect_location()
+        else:
+            resp.redirect_location = resp_url if resp_url != url else ""
+        return resp, read
+
+
+HttpGitClient = Urllib3HttpGitClient
+
+
 def get_transport_and_path_from_url(url, config=None, **kwargs):
     """Obtain a git client from a URL.
 
blob - 7868faece3ef5c7566164681f7f2cfa2d7818291
blob + 65f7de9e6ed5c8aacbec0b0c84dff92f24c76224
--- dulwich/config.py
+++ dulwich/config.py
@@ -41,7 +41,7 @@ try:
         MutableMapping,
     )
 except ImportError:  # python < 3.7
-    from collections import (
+    from collections import (  # type: ignore
         Iterable,
         MutableMapping,
     )
@@ -387,14 +387,16 @@ class ConfigFile(ConfigDict):
         super(ConfigFile, self).__init__(values=values, encoding=encoding)
         self.path = None
 
-    @classmethod
-    def from_file(cls, f: BinaryIO) -> "ConfigFile":
+    @classmethod  # noqa: C901
+    def from_file(cls, f: BinaryIO) -> "ConfigFile":  # noqa: C901
         """Read configuration from a file-like object."""
         ret = cls()
         section = None  # type: Optional[Tuple[bytes, ...]]
         setting = None
         continuation = None
         for lineno, line in enumerate(f.readlines()):
+            if lineno == 0 and line.startswith(b'\xef\xbb\xbf'):
+                line = line[3:]
             line = line.lstrip()
             if setting is None:
                 # Parse section header ("[bla]")
blob - 6abdc27dce50a22d372feeb383b89b909485cc7a
blob + acdec8c74d0e42c3480a4ad10ba5bf517107beca
--- dulwich/file.py
+++ dulwich/file.py
@@ -66,7 +66,7 @@ def _fancy_rename(oldname, newname):
     os.remove(tmpfile)
 
 
-def GitFile(filename, mode="rb", bufsize=-1):
+def GitFile(filename, mode="rb", bufsize=-1, mask=0o644):
     """Create a file object that obeys the git file locking protocol.
 
     Returns: a builtin file object or a _GitFile object
@@ -77,6 +77,10 @@ def GitFile(filename, mode="rb", bufsize=-1):
     are not.  To read and write from the same file, you can take advantage of
     the fact that opening a file for write does not actually open the file you
     request.
+
+    The default file mask makes any created files user-writable and
+    world-readable.
+
     """
     if "a" in mode:
         raise IOError("append mode not supported for Git files")
@@ -85,7 +89,7 @@ def GitFile(filename, mode="rb", bufsize=-1):
     if "b" not in mode:
         raise IOError("text mode not supported for Git files")
     if "w" in mode:
-        return _GitFile(filename, mode, bufsize)
+        return _GitFile(filename, mode, bufsize, mask)
     else:
         return io.open(filename, mode, bufsize)
 
@@ -136,7 +140,7 @@ class _GitFile(object):
         "writelines",
     )
 
-    def __init__(self, filename, mode, bufsize):
+    def __init__(self, filename, mode, bufsize, mask):
         self._filename = filename
         if isinstance(self._filename, bytes):
             self._lockfilename = self._filename + b".lock"
@@ -146,6 +150,7 @@ class _GitFile(object):
             fd = os.open(
                 self._lockfilename,
                 os.O_RDWR | os.O_CREAT | os.O_EXCL | getattr(os, "O_BINARY", 0),
+                mask,
             )
         except FileExistsError:
             raise FileLocked(filename, self._lockfilename)
blob - cc457a22c467b77297ccf68cd3a37a57fc94e2c2
blob + b75560f35c84987f5fe9a4ab835faf773f428756
--- dulwich/ignore.py
+++ dulwich/ignore.py
@@ -123,7 +123,7 @@ def read_ignore_patterns(f: BinaryIO) -> Iterable[byte
         line = line.rstrip(b"\r\n")
 
         # Ignore blank lines, they're used for readability.
-        if not line:
+        if not line.strip():
             continue
 
         if line.startswith(b"#"):
blob - 99c816388356f2a3d2bc51a88eac1178be84de1e
blob + 551f9c1f100fe4abcfe4907bf58ca38e468bce2d
--- dulwich/object_store.py
+++ dulwich/object_store.py
@@ -70,7 +70,12 @@ from dulwich.refs import ANNOTATED_TAG_SUFFIX
 INFODIR = "info"
 PACKDIR = "pack"
 
+# use permissions consistent with Git; just readable by everyone
+# TODO: should packs also be non-writable on Windows? if so, that
+# would requite some rather significant adjustments to the test suite
+PACK_MODE = 0o444 if sys.platform != "win32" else 0o644
 
+
 class BaseObjectStore(object):
     """Object store interface."""
 
@@ -805,7 +810,7 @@ class DiskObjectStore(PackBasedObjectStore):
         os.rename(path, target_pack)
 
         # Write the index.
-        index_file = GitFile(pack_base_name + ".idx", "wb")
+        index_file = GitFile(pack_base_name + ".idx", "wb", mask=PACK_MODE)
         try:
             write_pack_index_v2(index_file, entries, pack_sha)
             index_file.close()
@@ -837,6 +842,7 @@ class DiskObjectStore(PackBasedObjectStore):
 
         fd, path = tempfile.mkstemp(dir=self.path, prefix="tmp_pack_")
         with os.fdopen(fd, "w+b") as f:
+            os.chmod(path, PACK_MODE)
             indexer = PackIndexer(f, resolve_ext_ref=self.get_raw)
             copier = PackStreamCopier(read_all, read_some, f, delta_iter=indexer)
             copier.verify()
@@ -856,7 +862,7 @@ class DiskObjectStore(PackBasedObjectStore):
             basename = self._get_pack_basepath(entries)
             index_name = basename + ".idx"
             if not os.path.exists(index_name):
-                with GitFile(index_name, "wb") as f:
+                with GitFile(index_name, "wb", mask=PACK_MODE) as f:
                     write_pack_index_v2(f, entries, p.get_stored_checksum())
         for pack in self.packs:
             if pack._basename == basename:
@@ -885,6 +891,7 @@ class DiskObjectStore(PackBasedObjectStore):
 
         fd, path = tempfile.mkstemp(dir=self.pack_dir, suffix=".pack")
         f = os.fdopen(fd, "wb")
+        os.chmod(path, PACK_MODE)
 
         def commit():
             f.flush()
@@ -916,7 +923,7 @@ class DiskObjectStore(PackBasedObjectStore):
             pass
         if os.path.exists(path):
             return  # Already there, no need to write again
-        with GitFile(path, "wb") as f:
+        with GitFile(path, "wb", mask=PACK_MODE) as f:
             f.write(
                 obj.as_legacy_object(compression_level=self.loose_compression_level)
             )
blob - ea1722ed440b2f655df90a7287cbdfa073d34cbb
blob + e3cf42621f37b8972edb77a7ab332d747c21ae62
--- dulwich/porcelain.py
+++ dulwich/porcelain.py
@@ -1808,15 +1808,24 @@ def stash_push(repo):
         stash.push()
 
 
-def stash_pop(repo):
-    """Pop a new stash from the stack."""
+def stash_pop(repo, index):
+    """Pop a stash from the stack."""
     with open_repo_closing(repo) as r:
         from dulwich.stash import Stash
 
         stash = Stash.from_repo(r)
-        stash.pop()
+        stash.pop(index)
 
 
+def stash_drop(repo, index):
+    """Drop a stash from the stack."""
+    with open_repo_closing(repo) as r:
+        from dulwich.stash import Stash
+
+        stash = Stash.from_repo(r)
+        stash.drop(index)
+
+
 def ls_files(repo):
     """List all files in an index."""
     with open_repo_closing(repo) as r:
blob - 6cbdbf8f0833d8b11ca159fa3460e298f4cb27c3
blob + 0d34658f1ee8cf20e73ecdb9bd36bad5a269f8ba
--- dulwich/repo.py
+++ dulwich/repo.py
@@ -1262,8 +1262,10 @@ class Repo(BaseRepo):
 
         root_path_bytes = os.fsencode(self.path)
 
-        if not isinstance(fs_paths, list):
+        if isinstance(fs_paths, str):
             fs_paths = [fs_paths]
+        fs_paths = list(fs_paths)
+
         from dulwich.index import (
             blob_from_path_and_stat,
             index_entry_from_stat,
blob - 72f9e5c2d9aabb93cd8065664e910b485e9ccea2
blob + 487c2aa87a71268507d4f499917e408a8868ef4e
--- dulwich/tests/test_config.py
+++ 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_utf8_bom(self):
+        text = "[core]\nfoo = b\u00e4r\n".encode("utf-8-sig")
+        cf = self.from_file(text)
+        self.assertEqual(b"b\xc3\xa4r", cf.get((b"core",), b"foo"))
+
     def test_from_file_section_case_insensitive_lower(self):
         cf = self.from_file(b"[cOre]\nfOo = bar\n")
         self.assertEqual(b"bar", cf.get((b"core",), b"foo"))
blob - 1e947beba3a3f8a5a19c8f9aee9538832dbe232c
blob + 6cb5c6611243da3c836351588f13e1cec027657a
--- dulwich/tests/test_ignore.py
+++ dulwich/tests/test_ignore.py
@@ -105,7 +105,7 @@ class ReadIgnorePatterns(TestCase):
         f = BytesIO(
             b"""
 # a comment
-
+\x20\x20
 # and an empty line:
 
 \\#not a comment
blob - 2e416cff8dfc51bbc9809e0e69387b8508eb2a15
blob + 68789aacb3abed59695a69a647b10b55b13e909d
--- dulwich/tests/test_object_store.py
+++ dulwich/tests/test_object_store.py
@@ -27,6 +27,7 @@ from unittest import skipUnless
 import os
 import shutil
 import stat
+import sys
 import tempfile
 
 from dulwich.index import (
@@ -437,6 +438,14 @@ class DiskObjectStoreTests(PackBasedObjectStoreTests, 
         store.add_alternate_path("# comment")
         for alt_path in store._read_alternate_paths():
             self.assertNotIn("#", alt_path)
+
+    def test_file_modes(self):
+        self.store.add_object(testobject)
+        path = self.store._get_shafile_path(testobject.id)
+        mode = os.stat(path).st_mode
+
+        packmode = "0o100444" if sys.platform != "win32" else "0o100666"
+        self.assertEqual(oct(mode), packmode)
 
     def test_corrupted_object_raise_exception(self):
         """Corrupted sha1 disk file should raise specific exception"""
@@ -448,8 +457,11 @@ class DiskObjectStoreTests(PackBasedObjectStoreTests, 
         self.assertIsNotNone(self.store._get_loose_object(testobject.id))
 
         path = self.store._get_shafile_path(testobject.id)
+        old_mode = os.stat(path).st_mode
+        os.chmod(path, 0o600)
         with open(path, "wb") as f:  # corrupt the file
             f.write(b"")
+        os.chmod(path, old_mode)
 
         expected_error_msg = "Corrupted empty file detected"
         try:
blob - ded79b24a6dc65b53153a374c50447a9fd2dc17e
blob + 278259c3b4eb3afb8a8d9170cdf94b9786105bff
--- dulwich/tests/test_pack.py
+++ dulwich/tests/test_pack.py
@@ -26,6 +26,7 @@ from io import BytesIO
 from hashlib import sha1
 import os
 import shutil
+import sys
 import tempfile
 import zlib
 
@@ -83,6 +84,7 @@ pack1_sha = b"bc63ddad95e7321ee734ea11a7a62d314e0d7481
 a_sha = b"6f670c0fb53f9463760b7295fbb814e965fb20c8"
 tree_sha = b"b2a2766a2879c209ab1176e7e778b81ae422eeaa"
 commit_sha = b"f18faa16531ac570a3fdc8c7ca16682548dafd12"
+indexmode = "0o100644" if sys.platform != "win32" else "0o100666"
 
 
 class PackTests(TestCase):
@@ -338,6 +340,7 @@ class TestPackData(PackTests):
             p.create_index_v1(filename)
             idx1 = load_pack_index(filename)
             idx2 = self.get_pack_index(pack1_sha)
+            self.assertEqual(oct(os.stat(filename).st_mode), indexmode)
             self.assertEqual(idx1, idx2)
 
     def test_create_index_v2(self):
@@ -346,6 +349,7 @@ class TestPackData(PackTests):
             p.create_index_v2(filename)
             idx1 = load_pack_index(filename)
             idx2 = self.get_pack_index(pack1_sha)
+            self.assertEqual(oct(os.stat(filename).st_mode), indexmode)
             self.assertEqual(idx1, idx2)
 
     def test_compute_file_sha(self):
blob - 0a0e1882e5bf32cd0f4640be692a5c03e272a5c8
blob + 7717701f4009b24b8b2fe7246c08490fa59c9b2c
--- dulwich/tests/test_porcelain.py
+++ dulwich/tests/test_porcelain.py
@@ -111,55 +111,55 @@ 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
-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
-=YPjc
+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
+brMb2upTHa+kjktjdO96/BgSnKYqmNmPB/qB
+=ivA/
 -----END PGP PRIVATE KEY BLOCK-----
     """
 
@@ -197,55 +197,55 @@ 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
-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
-=kOKh
+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
+=9zU5
 -----END PGP PRIVATE KEY BLOCK-----
 """
 
blob - 9495d2f111061be0689b18bbb8012ac26c3e6bbf
blob + 4b75e0431b816f9e47d5ac5f0fb206354d533542
--- dulwich/tests/test_repository.py
+++ dulwich/tests/test_repository.py
@@ -21,10 +21,11 @@
 
 """Tests for the repository."""
 
+import glob
 import locale
 import os
-import stat
 import shutil
+import stat
 import sys
 import tempfile
 import warnings
@@ -79,7 +80,22 @@ class CreateRepositoryTests(TestCase):
         with repo.get_named_file("config") as f:
             config_text = f.read()
             self.assertTrue(barestr in config_text, "%r" % config_text)
+
+        if isinstance(repo, Repo):
+            expected_mode = '0o100644' if expect_filemode else '0o100666'
+            expected = {
+                'HEAD': expected_mode,
+                'config': expected_mode,
+                'description': expected_mode,
+            }
+            actual = {
+                f[len(repo._controldir) + 1:]: oct(os.stat(f).st_mode)
+                for f in glob.glob(os.path.join(repo._controldir, '*'))
+                if os.path.isfile(f)
+            }
 
+            self.assertEqual(expected, actual)
+
     def test_create_memory(self):
         repo = MemoryRepo.init_bare([], {})
         self._check_repo_contents(repo, True)
blob - e54aeeed9312dfef00c8d803064cdd5db15255df
blob + f42454ceef107f4fb2a83f9dfedde4aad6197efb
--- dulwich.egg-info/PKG-INFO
+++ dulwich.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: dulwich
-Version: 0.20.23
+Version: 0.20.25
 Summary: Python Git Library
 Home-page: https://www.dulwich.io/
 Author: Jelmer Vernooij
@@ -9,102 +9,6 @@ License: Apachev2 or later or GPLv2
 Project-URL: Bug Tracker, https://github.com/dulwich/dulwich/issues
 Project-URL: Repository, https://www.dulwich.io/code/
 Project-URL: GitHub, https://github.com/dulwich/dulwich
-Description: Dulwich
-        =======
-        
-        This is the Dulwich project.
-        
-        It aims to provide an interface to git repos (both local and remote) that
-        doesn't call out to git directly but instead uses pure Python.
-        
-        **Main website**: <https://www.dulwich.io/>
-        
-        **License**: Apache License, version 2 or GNU General Public License, version 2 or later.
-        
-        The project is named after the part of London that Mr. and Mrs. Git live in
-        in the particular Monty Python sketch.
-        
-        Installation
-        ------------
-        
-        By default, Dulwich' setup.py will attempt to build and install the optional C
-        extensions. The reason for this is that they significantly improve the performance
-        since some low-level operations that are executed often are much slower in CPython.
-        
-        If you don't want to install the C bindings, specify the --pure argument to setup.py::
-        
-            $ python setup.py --pure install
-        
-        or if you are installing from pip::
-        
-            $ pip install dulwich --global-option="--pure"
-        
-        Note that you can also specify --global-option in a
-        `requirements.txt <https://pip.pypa.io/en/stable/reference/pip_install/#requirement-specifiers>`_
-        file, e.g. like this::
-        
-            dulwich --global-option=--pure
-        
-        Getting started
-        ---------------
-        
-        Dulwich comes with both a lower-level API and higher-level plumbing ("porcelain").
-        
-        For example, to use the lower level API to access the commit message of the
-        last commit::
-        
-            >>> from dulwich.repo import Repo
-            >>> r = Repo('.')
-            >>> r.head()
-            '57fbe010446356833a6ad1600059d80b1e731e15'
-            >>> c = r[r.head()]
-            >>> c
-            <Commit 015fc1267258458901a94d228e39f0a378370466>
-            >>> c.message
-            'Add note about encoding.\n'
-        
-        And to print it using porcelain::
-        
-            >>> from dulwich import porcelain
-            >>> porcelain.log('.', max_entries=1)
-            --------------------------------------------------
-            commit: 57fbe010446356833a6ad1600059d80b1e731e15
-            Author: Jelmer Vernooij <jelmer@jelmer.uk>
-            Date:   Sat Apr 29 2017 23:57:34 +0000
-        
-            Add note about encoding.
-        
-        Further documentation
-        ---------------------
-        
-        The dulwich documentation can be found in docs/ and built by running ``make
-        doc``. It can also be found `on the web <https://www.dulwich.io/docs/>`_.
-        
-        Help
-        ----
-        
-        There is a *#dulwich* IRC channel on the `OFTC <https://www.oftc.net/>`_, and
-        `dulwich-announce <https://groups.google.com/forum/#!forum/dulwich-announce>`_
-        and `dulwich-discuss <https://groups.google.com/forum/#!forum/dulwich-discuss>`_
-        mailing lists.
-        
-        Contributing
-        ------------
-        
-        For a full list of contributors, see the git logs or `AUTHORS <AUTHORS>`_.
-        
-        If you'd like to contribute to Dulwich, see the `CONTRIBUTING <CONTRIBUTING.rst>`_
-        file and `list of open issues <https://github.com/dulwich/dulwich/issues>`_.
-        
-        Supported versions of Python
-        ----------------------------
-        
-        At the moment, Dulwich supports (and is tested on) CPython 3.5 and later and
-        Pypy.
-        
-        The latest release series to support Python 2.x was the 0.19 series. See
-        the 0.19 branch in the Dulwich git repository.
-        
 Keywords: git vcs
 Platform: UNKNOWN
 Classifier: Development Status :: 4 - Beta
@@ -124,3 +28,103 @@ Provides-Extra: fastimport
 Provides-Extra: https
 Provides-Extra: pgp
 Provides-Extra: watch
+License-File: COPYING
+License-File: AUTHORS
+
+Dulwich
+=======
+
+This is the Dulwich project.
+
+It aims to provide an interface to git repos (both local and remote) that
+doesn't call out to git directly but instead uses pure Python.
+
+**Main website**: <https://www.dulwich.io/>
+
+**License**: Apache License, version 2 or GNU General Public License, version 2 or later.
+
+The project is named after the part of London that Mr. and Mrs. Git live in
+in the particular Monty Python sketch.
+
+Installation
+------------
+
+By default, Dulwich' setup.py will attempt to build and install the optional C
+extensions. The reason for this is that they significantly improve the performance
+since some low-level operations that are executed often are much slower in CPython.
+
+If you don't want to install the C bindings, specify the --pure argument to setup.py::
+
+    $ python setup.py --pure install
+
+or if you are installing from pip::
+
+    $ pip install dulwich --global-option="--pure"
+
+Note that you can also specify --global-option in a
+`requirements.txt <https://pip.pypa.io/en/stable/reference/pip_install/#requirement-specifiers>`_
+file, e.g. like this::
+
+    dulwich --global-option=--pure
+
+Getting started
+---------------
+
+Dulwich comes with both a lower-level API and higher-level plumbing ("porcelain").
+
+For example, to use the lower level API to access the commit message of the
+last commit::
+
+    >>> from dulwich.repo import Repo
+    >>> r = Repo('.')
+    >>> r.head()
+    '57fbe010446356833a6ad1600059d80b1e731e15'
+    >>> c = r[r.head()]
+    >>> c
+    <Commit 015fc1267258458901a94d228e39f0a378370466>
+    >>> c.message
+    'Add note about encoding.\n'
+
+And to print it using porcelain::
+
+    >>> from dulwich import porcelain
+    >>> porcelain.log('.', max_entries=1)
+    --------------------------------------------------
+    commit: 57fbe010446356833a6ad1600059d80b1e731e15
+    Author: Jelmer Vernooij <jelmer@jelmer.uk>
+    Date:   Sat Apr 29 2017 23:57:34 +0000
+
+    Add note about encoding.
+
+Further documentation
+---------------------
+
+The dulwich documentation can be found in docs/ and built by running ``make
+doc``. It can also be found `on the web <https://www.dulwich.io/docs/>`_.
+
+Help
+----
+
+There is a *#dulwich* IRC channel on the `OFTC <https://www.oftc.net/>`_, and
+`dulwich-announce <https://groups.google.com/forum/#!forum/dulwich-announce>`_
+and `dulwich-discuss <https://groups.google.com/forum/#!forum/dulwich-discuss>`_
+mailing lists.
+
+Contributing
+------------
+
+For a full list of contributors, see the git logs or `AUTHORS <AUTHORS>`_.
+
+If you'd like to contribute to Dulwich, see the `CONTRIBUTING <CONTRIBUTING.rst>`_
+file and `list of open issues <https://github.com/dulwich/dulwich/issues>`_.
+
+Supported versions of Python
+----------------------------
+
+At the moment, Dulwich supports (and is tested on) CPython 3.5 and later and
+Pypy.
+
+The latest release series to support Python 2.x was the 0.19 series. See
+the 0.19 branch in the Dulwich git repository.
+
+
blob - 7e9c7a83aa250024ec3157354277444b57b1af47
blob + 75bc62eb2b568f43d43518ae5b2c38c61eaa47be
--- dulwich.egg-info/SOURCES.txt
+++ dulwich.egg-info/SOURCES.txt
@@ -15,7 +15,6 @@ README.rst
 README.swift.rst
 SECURITY.md
 TODO
-build.cmd
 dulwich.cfg
 releaser.conf
 requirements.txt
@@ -51,6 +50,7 @@ docs/tutorial/remote.txt
 docs/tutorial/repo.txt
 docs/tutorial/tag.txt
 dulwich/__init__.py
+dulwich/__main__.py
 dulwich/_diff_tree.c
 dulwich/_objects.c
 dulwich/_pack.c
@@ -93,6 +93,7 @@ dulwich.egg-info/PKG-INFO
 dulwich.egg-info/SOURCES.txt
 dulwich.egg-info/dependency_links.txt
 dulwich.egg-info/entry_points.txt
+dulwich.egg-info/not-zip-safe
 dulwich.egg-info/requires.txt
 dulwich.egg-info/top_level.txt
 dulwich/cloud/__init__.py
blob - /dev/null
blob + 8b137891791fe96927ad78e64b0aad7bded08bdc (mode 644)
--- /dev/null
+++ dulwich.egg-info/not-zip-safe
@@ -0,0 +1 @@
+
blob - cf6b7a3527d1b3a5537644f176fbbab6df669daf
blob + bcad72df255a46d39eb9f694dbc8467fb8880de4
--- releaser.conf
+++ releaser.conf
@@ -1,3 +1,4 @@
+# See https://github.com/jelmer/releaser
 news_file: "NEWS"
 timeout_days: 5
 tag_name: "dulwich-$VERSION"
blob - c9da33d268834db81a4b27b70faab81bce4e93ef
blob + 8168ef69730e5fec94de224eeac1c970f189fe58
--- setup.py
+++ setup.py
@@ -23,7 +23,7 @@ if sys.version_info < (3, 5):
         'For 2.7 support, please install a version prior to 0.20')
 
 
-dulwich_version_string = '0.20.23'
+dulwich_version_string = '0.20.25'
 
 
 class DulwichDistribution(Distribution):
@@ -116,6 +116,7 @@ setup(name='dulwich',
       package_data={'': ['../docs/tutorial/*.txt', 'py.typed']},
       scripts=scripts,
       ext_modules=ext_modules,
+      zip_safe=False,
       distclass=DulwichDistribution,
       classifiers=[
           'Development Status :: 4 - Beta',