Browse Source

Import upstream version 0.20.24+git20210814.1.80bffac

Jelmer Vernooij 3 years ago
parent
commit
df7efb6cd5

+ 7 - 3
.github/workflows/pythonpackage.yml

@@ -1,6 +1,10 @@
 name: Python package
 name: Python package
 
 
-on: [push, pull_request]
+on:
+  push:
+  pull_request:
+  schedule:
+    - cron: '0 6 * * *'  # Daily 6AM UTC build
 
 
 jobs:
 jobs:
   build:
   build:
@@ -9,7 +13,7 @@ jobs:
     strategy:
     strategy:
       matrix:
       matrix:
         os: [ubuntu-latest, macos-latest, windows-latest]
         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:
         exclude:
           # sqlite3 exit handling seems to get in the way
           # sqlite3 exit handling seems to get in the way
           - os: macos-latest
           - os: macos-latest
@@ -46,7 +50,7 @@ jobs:
       if: "matrix.os != 'windows-latest' && matrix.python-version != 'pypy3'"
       if: "matrix.os != 'windows-latest' && matrix.python-version != 'pypy3'"
     - name: Install mypy
     - name: Install mypy
       run: |
       run: |
-        pip install -U mypy
+        pip install -U mypy types-paramiko types-certifi
       if: "matrix.python-version != 'pypy3'"
       if: "matrix.python-version != 'pypy3'"
     - name: Style checks
     - name: Style checks
       run: |
       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
 0.20.23	2021-05-24
 
 
  * Fix installation of GPG during package publishing.
  * Fix installation of GPG during package publishing.
@@ -282,7 +303,7 @@
  BUG FIXES
  BUG FIXES
 
 
  * Avoid ``PermissionError``, since it is Python3-specific.
  * Avoid ``PermissionError``, since it is Python3-specific.
-  (Jelmer Vernooij)
+   (Jelmer Vernooij)
 
 
  * Fix regression that added a dependency on C git for the
  * Fix regression that added a dependency on C git for the
    test suite. (Jelmer Vernooij, #720)
    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."""
 """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):
 def main(argv=None):
     if argv is None:
     if argv is None:
-        argv = sys.argv
+        argv = sys.argv[1:]
 
 
     if len(argv) < 1:
     if len(argv) < 1:
         print("Usage: dulwich <%s> [OPTIONS...]" % ("|".join(commands.keys())))
         print("Usage: dulwich <%s> [OPTIONS...]" % ("|".join(commands.keys())))
@@ -747,9 +747,13 @@ def main(argv=None):
     return cmd_kls().run(argv[1:])
     return cmd_kls().run(argv[1:])
 
 
 
 
-if __name__ == "__main__":
+def _main():
     if "DULWICH_PDB" in os.environ and getattr(signal, "SIGQUIT", None):
     if "DULWICH_PDB" in os.environ and getattr(signal, "SIGQUIT", None):
         signal.signal(signal.SIGQUIT, signal_quit)  # type: ignore
         signal.signal(signal.SIGQUIT, signal_quit)  # type: ignore
     signal.signal(signal.SIGINT, signal_int)
     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
         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):
 def _fileno_can_read(fileno):
     """Check if a file descriptor is readable."""
     """Check if a file descriptor is readable."""
     return len(select.select([fileno], [], [], 0)[0]) > 0
     return len(select.select([fileno], [], [], 0)[0]) > 0
@@ -549,8 +558,8 @@ class GitClient(object):
         Args:
         Args:
           path: Remote path to fetch from
           path: Remote path to fetch from
           determine_wants: Function determine what refs
           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().
           graph_walker: Object with next() and ack().
           pack_data: Callback called for each bit of data in the pack
           pack_data: Callback called for each bit of data in the pack
           progress: Callback for progress reports (strings)
           progress: Callback for progress reports (strings)
@@ -901,10 +910,10 @@ class TraditionalGitClient(GitClient):
         Args:
         Args:
           path: Repository path (as bytestring)
           path: Repository path (as bytestring)
           update_refs: Function to determine changes to remote refs.
           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
           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
           progress: Optional callback called with progress updates
 
 
         Returns:
         Returns:
@@ -995,8 +1004,8 @@ class TraditionalGitClient(GitClient):
         Args:
         Args:
           path: Remote path to fetch from
           path: Remote path to fetch from
           determine_wants: Function determine what refs
           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().
           graph_walker: Object with next() and ack().
           pack_data: Callback called for each bit of data in the pack
           pack_data: Callback called for each bit of data in the pack
           progress: Callback for progress reports (strings)
           progress: Callback for progress reports (strings)
@@ -1292,9 +1301,9 @@ class LocalGitClient(GitClient):
         Args:
         Args:
           path: Repository path (as bytestring)
           path: Repository path (as bytestring)
           update_refs: Function to determine changes to remote refs.
           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
           progress: Optional progress function
 
 
         Returns:
         Returns:
@@ -1353,8 +1362,8 @@ class LocalGitClient(GitClient):
           path: Path to fetch from (as bytestring)
           path: Path to fetch from (as bytestring)
           target: Target repository to fetch into
           target: Target repository to fetch into
           determine_wants: Optional function determine what refs
           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
           progress: Optional progress function
           depth: Shallow fetch depth
           depth: Shallow fetch depth
 
 
@@ -1385,8 +1394,8 @@ class LocalGitClient(GitClient):
         Args:
         Args:
           path: Remote path to fetch from
           path: Remote path to fetch from
           determine_wants: Function determine what refs
           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().
           graph_walker: Object with next() and ack().
           pack_data: Callback called for each bit of data in the pack
           pack_data: Callback called for each bit of data in the pack
           progress: Callback for progress reports (strings)
           progress: Callback for progress reports (strings)
@@ -1759,8 +1768,6 @@ def default_urllib3_manager(   # noqa: C901
     if proxy_server is not None:
     if proxy_server is not None:
         if proxy_manager_cls is None:
         if proxy_manager_cls is None:
             proxy_manager_cls = urllib3.ProxyManager
             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):
         if not isinstance(proxy_server, str):
             proxy_server = proxy_server.decode()
             proxy_server = proxy_server.decode()
         manager = proxy_manager_cls(proxy_server, headers=headers, **kwargs)
         manager = proxy_manager_cls(proxy_server, headers=headers, **kwargs)
@@ -1772,71 +1779,20 @@ def default_urllib3_manager(   # noqa: C901
     return manager
     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)
         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):
     def _http_request(self, url, headers=None, data=None, allow_compression=False):
         """Perform HTTP request.
         """Perform HTTP request.
 
 
@@ -1853,48 +1809,8 @@ class HttpGitClient(GitClient):
           method for the response data.
           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):
     def _discover_references(self, service, base_url):
         assert base_url[-1] == "/"
         assert base_url[-1] == "/"
@@ -1934,6 +1850,11 @@ class HttpGitClient(GitClient):
             resp.close()
             resp.close()
 
 
     def _smart_request(self, service, url, data):
     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] == "/"
         assert url[-1] == "/"
         url = urljoin(url, service)
         url = urljoin(url, service)
         result_content_type = "application/x-%s-result" % service
         result_content_type = "application/x-%s-result" % service
@@ -1955,10 +1876,10 @@ class HttpGitClient(GitClient):
         Args:
         Args:
           path: Repository path (as bytestring)
           path: Repository path (as bytestring)
           update_refs: Function to determine changes to remote refs.
           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
           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
           progress: Optional progress function
 
 
         Returns:
         Returns:
@@ -2089,6 +2010,123 @@ class HttpGitClient(GitClient):
         refs, _, _ = self._discover_references(b"git-upload-pack", url)
         refs, _, _ = self._discover_references(b"git-upload-pack", url)
         return refs
         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):
 def get_transport_and_path_from_url(url, config=None, **kwargs):
     """Obtain a git client from a URL.
     """Obtain a git client from a URL.

+ 5 - 3
dulwich/config.py

@@ -41,7 +41,7 @@ try:
         MutableMapping,
         MutableMapping,
     )
     )
 except ImportError:  # python < 3.7
 except ImportError:  # python < 3.7
-    from collections import (
+    from collections import (  # type: ignore
         Iterable,
         Iterable,
         MutableMapping,
         MutableMapping,
     )
     )
@@ -387,14 +387,16 @@ class ConfigFile(ConfigDict):
         super(ConfigFile, self).__init__(values=values, encoding=encoding)
         super(ConfigFile, self).__init__(values=values, encoding=encoding)
         self.path = None
         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."""
         """Read configuration from a file-like object."""
         ret = cls()
         ret = cls()
         section = None  # type: Optional[Tuple[bytes, ...]]
         section = None  # type: Optional[Tuple[bytes, ...]]
         setting = None
         setting = None
         continuation = None
         continuation = None
         for lineno, line in enumerate(f.readlines()):
         for lineno, line in enumerate(f.readlines()):
+            if lineno == 0 and line.startswith(b'\xef\xbb\xbf'):
+                line = line[3:]
             line = line.lstrip()
             line = line.lstrip()
             if setting is None:
             if setting is None:
                 # Parse section header ("[bla]")
                 # Parse section header ("[bla]")

+ 8 - 3
dulwich/file.py

@@ -66,7 +66,7 @@ def _fancy_rename(oldname, newname):
     os.remove(tmpfile)
     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.
     """Create a file object that obeys the git file locking protocol.
 
 
     Returns: a builtin file object or a _GitFile object
     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
     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
     the fact that opening a file for write does not actually open the file you
     request.
     request.
+
+    The default file mask makes any created files user-writable and
+    world-readable.
+
     """
     """
     if "a" in mode:
     if "a" in mode:
         raise IOError("append mode not supported for Git files")
         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:
     if "b" not in mode:
         raise IOError("text mode not supported for Git files")
         raise IOError("text mode not supported for Git files")
     if "w" in mode:
     if "w" in mode:
-        return _GitFile(filename, mode, bufsize)
+        return _GitFile(filename, mode, bufsize, mask)
     else:
     else:
         return io.open(filename, mode, bufsize)
         return io.open(filename, mode, bufsize)
 
 
@@ -136,7 +140,7 @@ class _GitFile(object):
         "writelines",
         "writelines",
     )
     )
 
 
-    def __init__(self, filename, mode, bufsize):
+    def __init__(self, filename, mode, bufsize, mask):
         self._filename = filename
         self._filename = filename
         if isinstance(self._filename, bytes):
         if isinstance(self._filename, bytes):
             self._lockfilename = self._filename + b".lock"
             self._lockfilename = self._filename + b".lock"
@@ -146,6 +150,7 @@ class _GitFile(object):
             fd = os.open(
             fd = os.open(
                 self._lockfilename,
                 self._lockfilename,
                 os.O_RDWR | os.O_CREAT | os.O_EXCL | getattr(os, "O_BINARY", 0),
                 os.O_RDWR | os.O_CREAT | os.O_EXCL | getattr(os, "O_BINARY", 0),
+                mask,
             )
             )
         except FileExistsError:
         except FileExistsError:
             raise FileLocked(filename, self._lockfilename)
             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")
         line = line.rstrip(b"\r\n")
 
 
         # Ignore blank lines, they're used for readability.
         # Ignore blank lines, they're used for readability.
-        if not line:
+        if not line.strip():
             continue
             continue
 
 
         if line.startswith(b"#"):
         if line.startswith(b"#"):

+ 10 - 3
dulwich/object_store.py

@@ -70,6 +70,11 @@ from dulwich.refs import ANNOTATED_TAG_SUFFIX
 INFODIR = "info"
 INFODIR = "info"
 PACKDIR = "pack"
 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):
 class BaseObjectStore(object):
     """Object store interface."""
     """Object store interface."""
@@ -805,7 +810,7 @@ class DiskObjectStore(PackBasedObjectStore):
         os.rename(path, target_pack)
         os.rename(path, target_pack)
 
 
         # Write the index.
         # Write the index.
-        index_file = GitFile(pack_base_name + ".idx", "wb")
+        index_file = GitFile(pack_base_name + ".idx", "wb", mask=PACK_MODE)
         try:
         try:
             write_pack_index_v2(index_file, entries, pack_sha)
             write_pack_index_v2(index_file, entries, pack_sha)
             index_file.close()
             index_file.close()
@@ -837,6 +842,7 @@ class DiskObjectStore(PackBasedObjectStore):
 
 
         fd, path = tempfile.mkstemp(dir=self.path, prefix="tmp_pack_")
         fd, path = tempfile.mkstemp(dir=self.path, prefix="tmp_pack_")
         with os.fdopen(fd, "w+b") as f:
         with os.fdopen(fd, "w+b") as f:
+            os.chmod(path, PACK_MODE)
             indexer = PackIndexer(f, resolve_ext_ref=self.get_raw)
             indexer = PackIndexer(f, resolve_ext_ref=self.get_raw)
             copier = PackStreamCopier(read_all, read_some, f, delta_iter=indexer)
             copier = PackStreamCopier(read_all, read_some, f, delta_iter=indexer)
             copier.verify()
             copier.verify()
@@ -856,7 +862,7 @@ class DiskObjectStore(PackBasedObjectStore):
             basename = self._get_pack_basepath(entries)
             basename = self._get_pack_basepath(entries)
             index_name = basename + ".idx"
             index_name = basename + ".idx"
             if not os.path.exists(index_name):
             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())
                     write_pack_index_v2(f, entries, p.get_stored_checksum())
         for pack in self.packs:
         for pack in self.packs:
             if pack._basename == basename:
             if pack._basename == basename:
@@ -885,6 +891,7 @@ class DiskObjectStore(PackBasedObjectStore):
 
 
         fd, path = tempfile.mkstemp(dir=self.pack_dir, suffix=".pack")
         fd, path = tempfile.mkstemp(dir=self.pack_dir, suffix=".pack")
         f = os.fdopen(fd, "wb")
         f = os.fdopen(fd, "wb")
+        os.chmod(path, PACK_MODE)
 
 
         def commit():
         def commit():
             f.flush()
             f.flush()
@@ -916,7 +923,7 @@ class DiskObjectStore(PackBasedObjectStore):
             pass
             pass
         if os.path.exists(path):
         if os.path.exists(path):
             return  # Already there, no need to write again
             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(
             f.write(
                 obj.as_legacy_object(compression_level=self.loose_compression_level)
                 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()
         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:
     with open_repo_closing(repo) as r:
         from dulwich.stash import Stash
         from dulwich.stash import Stash
 
 
         stash = Stash.from_repo(r)
         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):
 def ls_files(repo):

+ 3 - 1
dulwich/repo.py

@@ -1262,8 +1262,10 @@ class Repo(BaseRepo):
 
 
         root_path_bytes = os.fsencode(self.path)
         root_path_bytes = os.fsencode(self.path)
 
 
-        if not isinstance(fs_paths, list):
+        if isinstance(fs_paths, str):
             fs_paths = [fs_paths]
             fs_paths = [fs_paths]
+        fs_paths = list(fs_paths)
+
         from dulwich.index import (
         from dulwich.index import (
             blob_from_path_and_stat,
             blob_from_path_and_stat,
             index_entry_from_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"))
         self.assertEqual(b"bar", cf.get((b"core", b"foo"), 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):
     def test_from_file_section_case_insensitive_lower(self):
         cf = self.from_file(b"[cOre]\nfOo = bar\n")
         cf = self.from_file(b"[cOre]\nfOo = bar\n")
         self.assertEqual(b"bar", cf.get((b"core",), b"foo"))
         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(
         f = BytesIO(
             b"""
             b"""
 # a comment
 # a comment
-
+\x20\x20
 # and an empty line:
 # and an empty line:
 
 
 \\#not a comment
 \\#not a comment

+ 12 - 0
dulwich/tests/test_object_store.py

@@ -27,6 +27,7 @@ from unittest import skipUnless
 import os
 import os
 import shutil
 import shutil
 import stat
 import stat
+import sys
 import tempfile
 import tempfile
 
 
 from dulwich.index import (
 from dulwich.index import (
@@ -438,6 +439,14 @@ class DiskObjectStoreTests(PackBasedObjectStoreTests, TestCase):
         for alt_path in store._read_alternate_paths():
         for alt_path in store._read_alternate_paths():
             self.assertNotIn("#", alt_path)
             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):
     def test_corrupted_object_raise_exception(self):
         """Corrupted sha1 disk file should raise specific exception"""
         """Corrupted sha1 disk file should raise specific exception"""
         self.store.add_object(testobject)
         self.store.add_object(testobject)
@@ -448,8 +457,11 @@ class DiskObjectStoreTests(PackBasedObjectStoreTests, TestCase):
         self.assertIsNotNone(self.store._get_loose_object(testobject.id))
         self.assertIsNotNone(self.store._get_loose_object(testobject.id))
 
 
         path = self.store._get_shafile_path(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
         with open(path, "wb") as f:  # corrupt the file
             f.write(b"")
             f.write(b"")
+        os.chmod(path, old_mode)
 
 
         expected_error_msg = "Corrupted empty file detected"
         expected_error_msg = "Corrupted empty file detected"
         try:
         try:

+ 4 - 0
dulwich/tests/test_pack.py

@@ -26,6 +26,7 @@ from io import BytesIO
 from hashlib import sha1
 from hashlib import sha1
 import os
 import os
 import shutil
 import shutil
+import sys
 import tempfile
 import tempfile
 import zlib
 import zlib
 
 
@@ -83,6 +84,7 @@ pack1_sha = b"bc63ddad95e7321ee734ea11a7a62d314e0d7481"
 a_sha = b"6f670c0fb53f9463760b7295fbb814e965fb20c8"
 a_sha = b"6f670c0fb53f9463760b7295fbb814e965fb20c8"
 tree_sha = b"b2a2766a2879c209ab1176e7e778b81ae422eeaa"
 tree_sha = b"b2a2766a2879c209ab1176e7e778b81ae422eeaa"
 commit_sha = b"f18faa16531ac570a3fdc8c7ca16682548dafd12"
 commit_sha = b"f18faa16531ac570a3fdc8c7ca16682548dafd12"
+indexmode = "0o100644" if sys.platform != "win32" else "0o100666"
 
 
 
 
 class PackTests(TestCase):
 class PackTests(TestCase):
@@ -338,6 +340,7 @@ class TestPackData(PackTests):
             p.create_index_v1(filename)
             p.create_index_v1(filename)
             idx1 = load_pack_index(filename)
             idx1 = load_pack_index(filename)
             idx2 = self.get_pack_index(pack1_sha)
             idx2 = self.get_pack_index(pack1_sha)
+            self.assertEqual(oct(os.stat(filename).st_mode), indexmode)
             self.assertEqual(idx1, idx2)
             self.assertEqual(idx1, idx2)
 
 
     def test_create_index_v2(self):
     def test_create_index_v2(self):
@@ -346,6 +349,7 @@ class TestPackData(PackTests):
             p.create_index_v2(filename)
             p.create_index_v2(filename)
             idx1 = load_pack_index(filename)
             idx1 = load_pack_index(filename)
             idx2 = self.get_pack_index(pack1_sha)
             idx2 = self.get_pack_index(pack1_sha)
+            self.assertEqual(oct(os.stat(filename).st_mode), indexmode)
             self.assertEqual(idx1, idx2)
             self.assertEqual(idx1, idx2)
 
 
     def test_compute_file_sha(self):
     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-----
 -----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-----
 -----END PGP PRIVATE KEY BLOCK-----
 """
 """
 
 

+ 17 - 1
dulwich/tests/test_repository.py

@@ -21,10 +21,11 @@
 
 
 """Tests for the repository."""
 """Tests for the repository."""
 
 
+import glob
 import locale
 import locale
 import os
 import os
-import stat
 import shutil
 import shutil
+import stat
 import sys
 import sys
 import tempfile
 import tempfile
 import warnings
 import warnings
@@ -80,6 +81,21 @@ class CreateRepositoryTests(TestCase):
             config_text = f.read()
             config_text = f.read()
             self.assertTrue(barestr in config_text, "%r" % config_text)
             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):
     def test_create_memory(self):
         repo = MemoryRepo.init_bare([], {})
         repo = MemoryRepo.init_bare([], {})
         self._check_repo_contents(repo, True)
         self._check_repo_contents(repo, True)

+ 1 - 0
releaser.conf

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

+ 0 - 5
setup.cfg

@@ -1,7 +1,2 @@
 [mypy]
 [mypy]
 ignore_missing_imports = True
 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')
         '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):
 class DulwichDistribution(Distribution):
@@ -116,6 +116,7 @@ setup(name='dulwich',
       package_data={'': ['../docs/tutorial/*.txt', 'py.typed']},
       package_data={'': ['../docs/tutorial/*.txt', 'py.typed']},
       scripts=scripts,
       scripts=scripts,
       ext_modules=ext_modules,
       ext_modules=ext_modules,
+      zip_safe=False,
       distclass=DulwichDistribution,
       distclass=DulwichDistribution,
       classifiers=[
       classifiers=[
           'Development Status :: 4 - Beta',
           'Development Status :: 4 - Beta',