Răsfoiți Sursa

Import upstream version 0.20.24+git20210814.1.80bffac

Jelmer Vernooij 3 ani în urmă
părinte
comite
df7efb6cd5

+ 7 - 3
.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: |

+ 22 - 1
NEWS

@@ -1,3 +1,24 @@
+0.20.25	UNRELEASED
+
+ * 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)

+ 0 - 126
PKG-INFO

@@ -1,126 +0,0 @@
-Metadata-Version: 2.1
-Name: dulwich
-Version: 0.20.23
-Summary: Python Git Library
-Home-page: https://www.dulwich.io/
-Author: Jelmer Vernooij
-Author-email: jelmer@jelmer.uk
-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
-Classifier: License :: OSI Approved :: Apache Software License
-Classifier: Programming Language :: Python :: 3.5
-Classifier: Programming Language :: Python :: 3.6
-Classifier: Programming Language :: Python :: 3.7
-Classifier: Programming Language :: Python :: 3.8
-Classifier: Programming Language :: Python :: 3.9
-Classifier: Programming Language :: Python :: Implementation :: CPython
-Classifier: Programming Language :: Python :: Implementation :: PyPy
-Classifier: Operating System :: POSIX
-Classifier: Operating System :: Microsoft :: Windows
-Classifier: Topic :: Software Development :: Version Control
-Requires-Python: >=3.5
-Provides-Extra: fastimport
-Provides-Extra: https
-Provides-Extra: pgp
-Provides-Extra: watch

+ 0 - 21
build.cmd

@@ -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 %*

+ 0 - 126
dulwich.egg-info/PKG-INFO

@@ -1,126 +0,0 @@
-Metadata-Version: 2.1
-Name: dulwich
-Version: 0.20.23
-Summary: Python Git Library
-Home-page: https://www.dulwich.io/
-Author: Jelmer Vernooij
-Author-email: jelmer@jelmer.uk
-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
-Classifier: License :: OSI Approved :: Apache Software License
-Classifier: Programming Language :: Python :: 3.5
-Classifier: Programming Language :: Python :: 3.6
-Classifier: Programming Language :: Python :: 3.7
-Classifier: Programming Language :: Python :: 3.8
-Classifier: Programming Language :: Python :: 3.9
-Classifier: Programming Language :: Python :: Implementation :: CPython
-Classifier: Programming Language :: Python :: Implementation :: PyPy
-Classifier: Operating System :: POSIX
-Classifier: Operating System :: Microsoft :: Windows
-Classifier: Topic :: Software Development :: Version Control
-Requires-Python: >=3.5
-Provides-Extra: fastimport
-Provides-Extra: https
-Provides-Extra: pgp
-Provides-Extra: watch

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

@@ -1,242 +0,0 @@
-.coveragerc
-.deepsource.toml
-.flake8
-.gitignore
-.mailmap
-.testr.conf
-AUTHORS
-CODE_OF_CONDUCT.md
-CONTRIBUTING.rst
-COPYING
-MANIFEST.in
-Makefile
-NEWS
-README.rst
-README.swift.rst
-SECURITY.md
-TODO
-build.cmd
-dulwich.cfg
-releaser.conf
-requirements.txt
-setup.cfg
-setup.py
-status.yaml
-tox.ini
-.github/workflows/pythonpackage.yml
-.github/workflows/pythonpublish.yml
-bin/dul-receive-pack
-bin/dul-upload-pack
-bin/dulwich
-devscripts/PREAMBLE.c
-devscripts/PREAMBLE.py
-devscripts/replace-preamble.sh
-docs/Makefile
-docs/conf.py
-docs/index.txt
-docs/make.bat
-docs/performance.txt
-docs/protocol.txt
-docs/api/index.txt
-docs/tutorial/.gitignore
-docs/tutorial/Makefile
-docs/tutorial/conclusion.txt
-docs/tutorial/encoding.txt
-docs/tutorial/file-format.txt
-docs/tutorial/index.txt
-docs/tutorial/introduction.txt
-docs/tutorial/object-store.txt
-docs/tutorial/porcelain.txt
-docs/tutorial/remote.txt
-docs/tutorial/repo.txt
-docs/tutorial/tag.txt
-dulwich/__init__.py
-dulwich/_diff_tree.c
-dulwich/_objects.c
-dulwich/_pack.c
-dulwich/archive.py
-dulwich/bundle.py
-dulwich/cli.py
-dulwich/client.py
-dulwich/config.py
-dulwich/diff_tree.py
-dulwich/errors.py
-dulwich/fastexport.py
-dulwich/file.py
-dulwich/graph.py
-dulwich/greenthreads.py
-dulwich/hooks.py
-dulwich/ignore.py
-dulwich/index.py
-dulwich/lfs.py
-dulwich/line_ending.py
-dulwich/log_utils.py
-dulwich/lru_cache.py
-dulwich/mailmap.py
-dulwich/object_store.py
-dulwich/objects.py
-dulwich/objectspec.py
-dulwich/pack.py
-dulwich/patch.py
-dulwich/porcelain.py
-dulwich/protocol.py
-dulwich/py.typed
-dulwich/reflog.py
-dulwich/refs.py
-dulwich/repo.py
-dulwich/server.py
-dulwich/stash.py
-dulwich/stdint.h
-dulwich/walk.py
-dulwich/web.py
-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/requires.txt
-dulwich.egg-info/top_level.txt
-dulwich/cloud/__init__.py
-dulwich/cloud/gcs.py
-dulwich/contrib/README.md
-dulwich/contrib/__init__.py
-dulwich/contrib/diffstat.py
-dulwich/contrib/paramiko_vendor.py
-dulwich/contrib/release_robot.py
-dulwich/contrib/swift.py
-dulwich/contrib/test_paramiko_vendor.py
-dulwich/contrib/test_release_robot.py
-dulwich/contrib/test_swift.py
-dulwich/contrib/test_swift_smoke.py
-dulwich/tests/__init__.py
-dulwich/tests/test_archive.py
-dulwich/tests/test_blackbox.py
-dulwich/tests/test_bundle.py
-dulwich/tests/test_client.py
-dulwich/tests/test_config.py
-dulwich/tests/test_diff_tree.py
-dulwich/tests/test_fastexport.py
-dulwich/tests/test_file.py
-dulwich/tests/test_grafts.py
-dulwich/tests/test_graph.py
-dulwich/tests/test_greenthreads.py
-dulwich/tests/test_hooks.py
-dulwich/tests/test_ignore.py
-dulwich/tests/test_index.py
-dulwich/tests/test_lfs.py
-dulwich/tests/test_line_ending.py
-dulwich/tests/test_lru_cache.py
-dulwich/tests/test_mailmap.py
-dulwich/tests/test_missing_obj_finder.py
-dulwich/tests/test_object_store.py
-dulwich/tests/test_objects.py
-dulwich/tests/test_objectspec.py
-dulwich/tests/test_pack.py
-dulwich/tests/test_patch.py
-dulwich/tests/test_porcelain.py
-dulwich/tests/test_protocol.py
-dulwich/tests/test_reflog.py
-dulwich/tests/test_refs.py
-dulwich/tests/test_repository.py
-dulwich/tests/test_server.py
-dulwich/tests/test_stash.py
-dulwich/tests/test_utils.py
-dulwich/tests/test_walk.py
-dulwich/tests/test_web.py
-dulwich/tests/utils.py
-dulwich/tests/compat/__init__.py
-dulwich/tests/compat/server_utils.py
-dulwich/tests/compat/test_client.py
-dulwich/tests/compat/test_pack.py
-dulwich/tests/compat/test_patch.py
-dulwich/tests/compat/test_porcelain.py
-dulwich/tests/compat/test_repository.py
-dulwich/tests/compat/test_server.py
-dulwich/tests/compat/test_utils.py
-dulwich/tests/compat/test_web.py
-dulwich/tests/compat/utils.py
-dulwich/tests/data/blobs/11/11111111111111111111111111111111111111
-dulwich/tests/data/blobs/6f/670c0fb53f9463760b7295fbb814e965fb20c8
-dulwich/tests/data/blobs/95/4a536f7819d40e6f637f849ee187dd10066349
-dulwich/tests/data/blobs/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391
-dulwich/tests/data/commits/0d/89f20333fbb1d2f3a94da77f4981373d8f4310
-dulwich/tests/data/commits/5d/ac377bdded4c9aeb8dff595f0faeebcc8498cc
-dulwich/tests/data/commits/60/dacdc733de308bb77bb76ce0fb0f9b44c9769e
-dulwich/tests/data/indexes/index
-dulwich/tests/data/packs/pack-bc63ddad95e7321ee734ea11a7a62d314e0d7481.idx
-dulwich/tests/data/packs/pack-bc63ddad95e7321ee734ea11a7a62d314e0d7481.pack
-dulwich/tests/data/repos/.gitattributes
-dulwich/tests/data/repos/issue88_expect_ack_nak_client.export
-dulwich/tests/data/repos/issue88_expect_ack_nak_other.export
-dulwich/tests/data/repos/issue88_expect_ack_nak_server.export
-dulwich/tests/data/repos/server_new.export
-dulwich/tests/data/repos/server_old.export
-dulwich/tests/data/repos/a.git/HEAD
-dulwich/tests/data/repos/a.git/packed-refs
-dulwich/tests/data/repos/a.git/objects/28/237f4dc30d0d462658d6b937b08a0f0b6ef55a
-dulwich/tests/data/repos/a.git/objects/2a/72d929692c41d8554c07f6301757ba18a65d91
-dulwich/tests/data/repos/a.git/objects/4e/f30bbfe26431a69c3820d3a683df54d688f2ec
-dulwich/tests/data/repos/a.git/objects/4f/2e6529203aa6d44b5af6e3292c837ceda003f9
-dulwich/tests/data/repos/a.git/objects/7d/9a07d797595ef11344549b8d08198e48c15364
-dulwich/tests/data/repos/a.git/objects/a2/96d0bb611188cabb256919f36bc30117cca005
-dulwich/tests/data/repos/a.git/objects/a9/0fa2d900a17e99b433217e988c4eb4a2e9a097
-dulwich/tests/data/repos/a.git/objects/b0/931cadc54336e78a1d980420e3268903b57a50
-dulwich/tests/data/repos/a.git/objects/ff/d47d45845a8f6576491e1edb97e3fe6a850e7f
-dulwich/tests/data/repos/a.git/refs/heads/master
-dulwich/tests/data/repos/a.git/refs/tags/mytag
-dulwich/tests/data/repos/empty.git/HEAD
-dulwich/tests/data/repos/empty.git/config
-dulwich/tests/data/repos/empty.git/objects/info/.gitignore
-dulwich/tests/data/repos/empty.git/objects/pack/.gitignore
-dulwich/tests/data/repos/empty.git/refs/heads/.gitignore
-dulwich/tests/data/repos/empty.git/refs/tags/.gitignore
-dulwich/tests/data/repos/ooo_merge.git/HEAD
-dulwich/tests/data/repos/ooo_merge.git/objects/29/69be3e8ee1c0222396a5611407e4769f14e54b
-dulwich/tests/data/repos/ooo_merge.git/objects/38/74e9c60a6d149c44c928140f250d81e6381520
-dulwich/tests/data/repos/ooo_merge.git/objects/6f/670c0fb53f9463760b7295fbb814e965fb20c8
-dulwich/tests/data/repos/ooo_merge.git/objects/70/c190eb48fa8bbb50ddc692a17b44cb781af7f6
-dulwich/tests/data/repos/ooo_merge.git/objects/76/01d7f6231db6a57f7bbb79ee52e4d462fd44d1
-dulwich/tests/data/repos/ooo_merge.git/objects/90/182552c4a85a45ec2a835cadc3451bebdfe870
-dulwich/tests/data/repos/ooo_merge.git/objects/95/4a536f7819d40e6f637f849ee187dd10066349
-dulwich/tests/data/repos/ooo_merge.git/objects/b2/a2766a2879c209ab1176e7e778b81ae422eeaa
-dulwich/tests/data/repos/ooo_merge.git/objects/f5/07291b64138b875c28e03469025b1ea20bc614
-dulwich/tests/data/repos/ooo_merge.git/objects/f9/e39b120c68182a4ba35349f832d0e4e61f485c
-dulwich/tests/data/repos/ooo_merge.git/objects/fb/5b0425c7ce46959bec94d54b9a157645e114f5
-dulwich/tests/data/repos/ooo_merge.git/refs/heads/master
-dulwich/tests/data/repos/refs.git/HEAD
-dulwich/tests/data/repos/refs.git/packed-refs
-dulwich/tests/data/repos/refs.git/objects/3b/9e5457140e738c2dcd39bf6d7acf88379b90d1
-dulwich/tests/data/repos/refs.git/objects/3e/c9c43c84ff242e3ef4a9fc5bc111fd780a76a8
-dulwich/tests/data/repos/refs.git/objects/42/d06bd4b77fed026b154d16493e5deab78f02ec
-dulwich/tests/data/repos/refs.git/objects/a1/8114c31713746a33a2e70d9914d1ef3e781425
-dulwich/tests/data/repos/refs.git/objects/cd/a609072918d7b70057b6bef9f4c2537843fcfe
-dulwich/tests/data/repos/refs.git/objects/df/6800012397fb85c56e7418dd4eb9405dee075c
-dulwich/tests/data/repos/refs.git/refs/heads/40-char-ref-aaaaaaaaaaaaaaaaaa
-dulwich/tests/data/repos/refs.git/refs/heads/loop
-dulwich/tests/data/repos/refs.git/refs/heads/master
-dulwich/tests/data/repos/refs.git/refs/tags/refs-0.2
-dulwich/tests/data/repos/simple_merge.git/HEAD
-dulwich/tests/data/repos/simple_merge.git/objects/0d/89f20333fbb1d2f3a94da77f4981373d8f4310
-dulwich/tests/data/repos/simple_merge.git/objects/1b/6318f651a534b38f9c7aedeebbd56c1e896853
-dulwich/tests/data/repos/simple_merge.git/objects/29/69be3e8ee1c0222396a5611407e4769f14e54b
-dulwich/tests/data/repos/simple_merge.git/objects/4c/ffe90e0a41ad3f5190079d7c8f036bde29cbe6
-dulwich/tests/data/repos/simple_merge.git/objects/5d/ac377bdded4c9aeb8dff595f0faeebcc8498cc
-dulwich/tests/data/repos/simple_merge.git/objects/60/dacdc733de308bb77bb76ce0fb0f9b44c9769e
-dulwich/tests/data/repos/simple_merge.git/objects/6f/670c0fb53f9463760b7295fbb814e965fb20c8
-dulwich/tests/data/repos/simple_merge.git/objects/70/c190eb48fa8bbb50ddc692a17b44cb781af7f6
-dulwich/tests/data/repos/simple_merge.git/objects/90/182552c4a85a45ec2a835cadc3451bebdfe870
-dulwich/tests/data/repos/simple_merge.git/objects/95/4a536f7819d40e6f637f849ee187dd10066349
-dulwich/tests/data/repos/simple_merge.git/objects/ab/64bbdcc51b170d21588e5c5d391ee5c0c96dfd
-dulwich/tests/data/repos/simple_merge.git/objects/d4/bdad6549dfedf25d3b89d21f506aff575b28a7
-dulwich/tests/data/repos/simple_merge.git/objects/d8/0c186a03f423a81b39df39dc87fd269736ca86
-dulwich/tests/data/repos/simple_merge.git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391
-dulwich/tests/data/repos/simple_merge.git/refs/heads/master
-dulwich/tests/data/repos/submodule/dotgit
-dulwich/tests/data/tags/71/033db03a03c6a36721efcf1968dd8f8e0cf023
-dulwich/tests/data/trees/70/c190eb48fa8bbb50ddc692a17b44cb781af7f6
-examples/clone.py
-examples/config.py
-examples/diff.py
-examples/gcs.py
-examples/latest_change.py
-examples/memoryrepo.py
-examples/rename-branch.py

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

@@ -1 +0,0 @@
-

+ 0 - 3
dulwich.egg-info/entry_points.txt

@@ -1,3 +0,0 @@
-[console_scripts]
-dulwich = dulwich.cli:main
-

+ 0 - 14
dulwich.egg-info/requires.txt

@@ -1,14 +0,0 @@
-certifi
-urllib3>=1.24.1
-
-[fastimport]
-fastimport
-
-[https]
-urllib3[secure]>=1.24.1
-
-[pgp]
-gpg
-
-[watch]
-pyinotify

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

@@ -1 +0,0 @@
-dulwich

+ 1 - 1
dulwich/__init__.py

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

+ 4 - 0
dulwich/__main__.py

@@ -0,0 +1,4 @@
+from . import cli
+
+if __name__ == "__main__":
+    cli._main()

+ 7 - 3
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()

+ 158 - 120
dulwich/client.py

@@ -133,6 +133,15 @@ class HTTPUnauthorized(Exception):
         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
+
+
 def _fileno_can_read(fileno):
     """Check if a file descriptor is readable."""
     return len(select.select([fileno], [], [], 0)[0]) > 0
@@ -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
-
-        if pool_manager is None:
-            self.pool_manager = default_urllib3_manager(config)
-        else:
-            self.pool_manager = pool_manager
+class AbstractHttpGitClient(GitClient):
+    """Abstract base class for HTTP Git Clients.
 
-        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
+    This is agonistic of the actual HTTP implementation.
 
-            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,6 +2010,123 @@ 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.

+ 5 - 3
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]")

+ 8 - 3
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)

+ 1 - 1
dulwich/ignore.py

@@ -123,7 +123,7 @@ def read_ignore_patterns(f: BinaryIO) -> Iterable[bytes]:
         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"#"):

+ 10 - 3
dulwich/object_store.py

@@ -70,6 +70,11 @@ 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)
             )

+ 12 - 3
dulwich/porcelain.py

@@ -1808,13 +1808,22 @@ 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):

+ 3 - 1
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,

+ 5 - 0
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"))

+ 1 - 1
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

+ 12 - 0
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 (
@@ -438,6 +439,14 @@ class DiskObjectStoreTests(PackBasedObjectStoreTests, TestCase):
         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"""
         self.store.add_object(testobject)
@@ -448,8 +457,11 @@ class DiskObjectStoreTests(PackBasedObjectStoreTests, TestCase):
         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:

+ 4 - 0
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):

+ 98 - 98
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-----
 """
 

+ 17 - 1
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
@@ -80,6 +81,21 @@ class CreateRepositoryTests(TestCase):
             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)

+ 1 - 0
releaser.conf

@@ -1,3 +1,4 @@
+# See https://github.com/jelmer/releaser
 news_file: "NEWS"
 timeout_days: 5
 tag_name: "dulwich-$VERSION"

+ 0 - 5
setup.cfg

@@ -1,7 +1,2 @@
 [mypy]
 ignore_missing_imports = True
-
-[egg_info]
-tag_build = 
-tag_date = 0
-

+ 2 - 1
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.24'
 
 
 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',