Parcourir la source

Import upstream version 0.20.46

Jelmer Vernooij il y a 2 ans
Parent
commit
4d8b946632
100 fichiers modifiés avec 1126 ajouts et 588 suppressions
  1. 1 1
      .flake8
  2. 38 41
      .github/workflows/pythonpackage.yml
  3. 79 75
      .github/workflows/pythonwheels.yml
  4. 17 16
      CODE_OF_CONDUCT.md
  5. 1 1
      Makefile
  6. 65 4
      NEWS
  7. 2 1
      PKG-INFO
  8. 2 2
      README.swift.rst
  9. 2 1
      SECURITY.md
  10. 9 3
      docs/conf.py
  11. 1 1
      docs/tutorial/encoding.txt
  12. 7 0
      docs/tutorial/porcelain.txt
  13. 2 1
      dulwich.egg-info/PKG-INFO
  14. 81 80
      dulwich.egg-info/SOURCES.txt
  15. 2 3
      dulwich.egg-info/requires.txt
  16. 1 1
      dulwich/__init__.py
  17. 1 1
      dulwich/archive.py
  18. 16 2
      dulwich/cli.py
  19. 59 86
      dulwich/client.py
  20. 80 52
      dulwich/config.py
  21. 2 2
      dulwich/contrib/diffstat.py
  22. 1 1
      dulwich/contrib/release_robot.py
  23. 2 2
      dulwich/contrib/swift.py
  24. 1 1
      dulwich/contrib/test_paramiko_vendor.py
  25. 1 1
      dulwich/contrib/test_swift_smoke.py
  26. 1 0
      dulwich/file.py
  27. 1 1
      dulwich/graph.py
  28. 3 1
      dulwich/hooks.py
  29. 2 2
      dulwich/index.py
  30. 0 6
      dulwich/object_store.py
  31. 56 8
      dulwich/objects.py
  32. 1 1
      dulwich/patch.py
  33. 150 25
      dulwich/porcelain.py
  34. 2 0
      dulwich/protocol.py
  35. 24 22
      dulwich/refs.py
  36. 26 11
      dulwich/repo.py
  37. 1 1
      dulwich/server.py
  38. 40 0
      dulwich/submodule.py
  39. 1 1
      dulwich/tests/__init__.py
  40. 7 2
      dulwich/tests/compat/test_client.py
  41. 1 1
      dulwich/tests/compat/test_porcelain.py
  42. 5 2
      dulwich/tests/compat/utils.py
  43. 0 29
      dulwich/tests/test_client.py
  44. 35 2
      dulwich/tests/test_config.py
  45. 1 1
      dulwich/tests/test_greenthreads.py
  46. 1 1
      dulwich/tests/test_index.py
  47. 2 14
      dulwich/tests/test_objects.py
  48. 1 1
      dulwich/tests/test_pack.py
  49. 232 4
      dulwich/tests/test_porcelain.py
  50. 5 4
      dulwich/tests/test_refs.py
  51. 1 1
      dulwich/tests/test_repository.py
  52. 1 1
      dulwich/tests/utils.py
  53. 1 1
      requirements.txt
  54. 24 38
      setup.py
  55. 28 28
      status.yaml
  56. 0 0
      testdata/blobs/11/11111111111111111111111111111111111111
  57. 0 0
      testdata/blobs/6f/670c0fb53f9463760b7295fbb814e965fb20c8
  58. 0 0
      testdata/blobs/95/4a536f7819d40e6f637f849ee187dd10066349
  59. 0 0
      testdata/blobs/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391
  60. 0 0
      testdata/commits/0d/89f20333fbb1d2f3a94da77f4981373d8f4310
  61. 0 0
      testdata/commits/5d/ac377bdded4c9aeb8dff595f0faeebcc8498cc
  62. 0 0
      testdata/commits/60/dacdc733de308bb77bb76ce0fb0f9b44c9769e
  63. 0 0
      testdata/indexes/index
  64. 0 0
      testdata/packs/pack-bc63ddad95e7321ee734ea11a7a62d314e0d7481.idx
  65. 0 0
      testdata/packs/pack-bc63ddad95e7321ee734ea11a7a62d314e0d7481.pack
  66. 0 0
      testdata/repos/.gitattributes
  67. 0 0
      testdata/repos/a.git/HEAD
  68. 0 0
      testdata/repos/a.git/objects/28/237f4dc30d0d462658d6b937b08a0f0b6ef55a
  69. 0 0
      testdata/repos/a.git/objects/2a/72d929692c41d8554c07f6301757ba18a65d91
  70. 0 0
      testdata/repos/a.git/objects/4e/f30bbfe26431a69c3820d3a683df54d688f2ec
  71. 0 0
      testdata/repos/a.git/objects/4f/2e6529203aa6d44b5af6e3292c837ceda003f9
  72. 0 0
      testdata/repos/a.git/objects/7d/9a07d797595ef11344549b8d08198e48c15364
  73. 0 0
      testdata/repos/a.git/objects/a2/96d0bb611188cabb256919f36bc30117cca005
  74. 0 0
      testdata/repos/a.git/objects/a9/0fa2d900a17e99b433217e988c4eb4a2e9a097
  75. 0 0
      testdata/repos/a.git/objects/b0/931cadc54336e78a1d980420e3268903b57a50
  76. 0 0
      testdata/repos/a.git/objects/ff/d47d45845a8f6576491e1edb97e3fe6a850e7f
  77. 0 0
      testdata/repos/a.git/packed-refs
  78. 0 0
      testdata/repos/a.git/refs/heads/master
  79. 0 0
      testdata/repos/a.git/refs/tags/mytag
  80. 0 0
      testdata/repos/empty.git/HEAD
  81. 0 0
      testdata/repos/empty.git/config
  82. 0 0
      testdata/repos/empty.git/objects/info/.gitignore
  83. 0 0
      testdata/repos/empty.git/objects/pack/.gitignore
  84. 0 0
      testdata/repos/empty.git/refs/heads/.gitignore
  85. 0 0
      testdata/repos/empty.git/refs/tags/.gitignore
  86. 0 0
      testdata/repos/issue88_expect_ack_nak_client.export
  87. 0 0
      testdata/repos/issue88_expect_ack_nak_other.export
  88. 0 0
      testdata/repos/issue88_expect_ack_nak_server.export
  89. 0 0
      testdata/repos/ooo_merge.git/HEAD
  90. 0 0
      testdata/repos/ooo_merge.git/objects/29/69be3e8ee1c0222396a5611407e4769f14e54b
  91. 0 0
      testdata/repos/ooo_merge.git/objects/38/74e9c60a6d149c44c928140f250d81e6381520
  92. 0 0
      testdata/repos/ooo_merge.git/objects/6f/670c0fb53f9463760b7295fbb814e965fb20c8
  93. 0 0
      testdata/repos/ooo_merge.git/objects/70/c190eb48fa8bbb50ddc692a17b44cb781af7f6
  94. 0 0
      testdata/repos/ooo_merge.git/objects/76/01d7f6231db6a57f7bbb79ee52e4d462fd44d1
  95. 0 0
      testdata/repos/ooo_merge.git/objects/90/182552c4a85a45ec2a835cadc3451bebdfe870
  96. 0 0
      testdata/repos/ooo_merge.git/objects/95/4a536f7819d40e6f637f849ee187dd10066349
  97. 0 0
      testdata/repos/ooo_merge.git/objects/b2/a2766a2879c209ab1176e7e778b81ae422eeaa
  98. 0 0
      testdata/repos/ooo_merge.git/objects/f5/07291b64138b875c28e03469025b1ea20bc614
  99. 0 0
      testdata/repos/ooo_merge.git/objects/f9/e39b120c68182a4ba35349f832d0e4e61f485c
  100. 0 0
      testdata/repos/ooo_merge.git/objects/fb/5b0425c7ce46959bec94d54b9a157645e114f5

+ 1 - 1
.flake8

@@ -1,5 +1,5 @@
 [flake8]
-extend-ignore = E203, E266, E501, W293, W291
+extend-ignore = E203, E266, E501, W293, W291, W503
 max-line-length = 88
 max-complexity = 18
 select = B,C,E,F,W,T4,B9

+ 38 - 41
.github/workflows/pythonpackage.yml

@@ -4,16 +4,16 @@ on:
   push:
   pull_request:
   schedule:
-    - cron: '0 6 * * *'  # Daily 6AM UTC build
+    - cron: "0 6 * * *" # Daily 6AM UTC build
 
 jobs:
   build:
-
     runs-on: ${{ matrix.os }}
     strategy:
       matrix:
         os: [ubuntu-latest, macos-latest, windows-latest]
-        python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", pypy3]
+        python-version:
+          ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11.0-rc - 3.11", pypy3]
         exclude:
           # sqlite3 exit handling seems to get in the way
           - os: macos-latest
@@ -24,41 +24,38 @@ jobs:
       fail-fast: false
 
     steps:
-    - uses: actions/checkout@v2
-    - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v2
-      with:
-        python-version: ${{ matrix.python-version }}
-    - name: Install native dependencies (Ubuntu)
-      run: sudo apt-get update && sudo apt-get install -y libgpgme-dev libgpg-error-dev
-      if: "matrix.os == 'ubuntu-latest'"
-    - name: Install native dependencies (MacOS)
-      run: brew install swig gpgme
-      if: "matrix.os == 'macos-latest'"
-    - name: Install dependencies
-      run: |
-        python -m pip install --upgrade pip
-        pip install -U pip coverage codecov flake8 fastimport paramiko
-    - name: Install gpg on supported platforms
-      run: pip install -U gpg
-      if: "matrix.os != 'windows-latest' && matrix.python-version != 'pypy3'"
-    - name: Install mypy
-      run: |
-        pip install -U mypy types-paramiko types-certifi types-requests
-      if: "matrix.python-version != 'pypy3'"
-    - name: Style checks
-      run: |
-        python -m flake8
-    - name: Typing checks
-      run: |
-        python -m mypy dulwich
-      if: "matrix.python-version != 'pypy3'"
-    - name: Build
-      run: |
-        python setup.py build_ext -i
-    - name: Coverage test suite run
-      run: |
-        python -m coverage run -p -m unittest dulwich.tests.test_suite
-    - name: Upload coverage details
-      run: |
-        codecov
+      - uses: actions/checkout@v2
+      - name: Set up Python ${{ matrix.python-version }}
+        uses: actions/setup-python@v2
+        with:
+          python-version: ${{ matrix.python-version }}
+      - name: Install native dependencies (Ubuntu)
+        run: sudo apt-get update && sudo apt-get install -y libgpgme-dev libgpg-error-dev
+        if: "matrix.os == 'ubuntu-latest'"
+      - name: Install native dependencies (MacOS)
+        run: brew install swig gpgme
+        if: "matrix.os == 'macos-latest'"
+      - name: Install dependencies
+        run: |
+          python -m pip install --upgrade pip
+          pip install -U pip coverage flake8 fastimport paramiko urllib3
+      - name: Install gpg on supported platforms
+        run: pip install -U gpg
+        if: "matrix.os != 'windows-latest' && matrix.python-version != 'pypy3'"
+      - name: Install mypy
+        run: |
+          pip install -U mypy types-paramiko types-requests
+        if: "matrix.python-version != 'pypy3'"
+      - name: Style checks
+        run: |
+          python -m flake8
+      - name: Typing checks
+        run: |
+          python -m mypy dulwich
+        if: "matrix.python-version != 'pypy3'"
+      - name: Build
+        run: |
+          python setup.py build_ext -i
+      - name: Coverage test suite run
+        run: |
+          python -m coverage run -p -m unittest dulwich.tests.test_suite

+ 79 - 75
.github/workflows/pythonwheels.yml

@@ -4,79 +4,83 @@ on:
   push:
   pull_request:
   schedule:
-    - cron: '0 6 * * *'  # Daily 6AM UTC build
+    - cron: "0 6 * * *" # Daily 6AM UTC build
 
 jobs:
   build:
-
     runs-on: ${{ matrix.os }}
     strategy:
       matrix:
         os: [macos-latest, windows-latest]
-        python-version: ['3.6', '3.7', '3.8', '3.9', '3.10']
+        python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11.0-rc - 3.11"]
+        architecture: ["x64", "x86"]
         include:
           - os: ubuntu-latest
-            python-version: '3.x'
+            python-version: "3.x"
           # path encoding
+        exclude:
+          - os: macos-latest
+            architecture: "x86"
       fail-fast: true
 
     steps:
-    - uses: actions/checkout@v2
-    - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v2
-      with:
-        python-version: ${{ matrix.python-version }}
-    - name: Install native dependencies (Ubuntu)
-      run: sudo apt-get update && sudo apt-get install -y libgpgme-dev libgpg-error-dev
-      if: "matrix.os == 'ubuntu-latest'"
-    - name: Install native dependencies (MacOS)
-      run: brew install swig gpgme
-      if: "matrix.os == 'macos-latest'"
-    - name: Install dependencies
-      run: |
-        python -m pip install --upgrade pip
-        pip install setuptools wheel fastimport paramiko urllib3
-    - name: Install gpg on supported platforms
-      run: pip install -U gpg
-      if: "matrix.os != 'windows-latest' && matrix.python-version != 'pypy3'"
-    - name: Run test suite
-      run: |
-        python -m unittest dulwich.tests.test_suite
-    - name: Build
-      run: |
-        python setup.py sdist bdist_wheel
-      if: "matrix.os != 'ubuntu-latest'"
-    - uses: docker/setup-qemu-action@v1
-      name: Set up QEMU
-      if: "matrix.os == 'ubuntu-latest'"
-    - name: Build (Linux aarch64)
-      uses: RalfG/python-wheels-manylinux-build@v0.3.3-manylinux2014_aarch64
-      with:
-        python-versions: 'cp36-cp36m cp37-cp37m cp38-cp38 cp39-cp39 cp310-cp310'
-      if: "matrix.os == 'ubuntu-latest'"
-    - name: Build (Linux)
-      uses: RalfG/python-wheels-manylinux-build@v0.3.1
-      with:
-        python-versions: 'cp36-cp36m cp37-cp37m cp38-cp38 cp39-cp39 cp310-cp310'
-      env:
-        # Temporary fix for LD_LIBRARY_PATH issue. See
-        # https://github.com/RalfG/python-wheels-manylinux-build/issues/26
-        LD_LIBRARY_PATH: /usr/local/lib:${{ env.LD_LIBRARY_PATH }}
-      if: "matrix.os == 'ubuntu-latest'"
-    - name: Upload wheels (Linux)
-      uses: actions/upload-artifact@v2
-      # Only include *manylinux* wheels; the other wheels files are built but
-      # rejected by pip.
-      if: "matrix.os == 'ubuntu-latest'"
-      with:
-        name: dist
-        path: dist/*manylinux*.whl
-    - name: Upload wheels (non-Linux)
-      uses: actions/upload-artifact@v2
-      with:
-        name: dist
-        path: dist/*.whl
-      if: "matrix.os != 'ubuntu-latest'"
+      - uses: actions/checkout@v2
+      - name: Set up Python ${{ matrix.python-version }}
+        uses: actions/setup-python@v2
+        with:
+          python-version: ${{ matrix.python-version }}
+          architecture: ${{ matrix.architecture }}
+      - name: Install native dependencies (Ubuntu)
+        run: sudo apt-get update && sudo apt-get install -y libgpgme-dev libgpg-error-dev
+        if: "matrix.os == 'ubuntu-latest'"
+      - name: Install native dependencies (MacOS)
+        run: brew install swig gpgme
+        if: "matrix.os == 'macos-latest'"
+      - name: Install dependencies
+        run: |
+          python -m pip install --upgrade pip
+          pip install setuptools wheel fastimport paramiko urllib3
+      - name: Install gpg on supported platforms
+        run: pip install -U gpg
+        if: "matrix.os != 'windows-latest' && matrix.python-version != 'pypy3'"
+      - name: Run test suite
+        run: |
+          python -m unittest dulwich.tests.test_suite
+      - name: Build
+        run: |
+          python setup.py sdist bdist_wheel
+        if: "matrix.os != 'ubuntu-latest'"
+      - uses: docker/setup-qemu-action@v1
+        name: Set up QEMU
+        if: "matrix.os == 'ubuntu-latest'"
+      - name: Build (Linux aarch64)
+        uses: RalfG/python-wheels-manylinux-build@v0.5.0-manylinux2014_aarch64
+        with:
+          python-versions: "cp36-cp36m cp37-cp37m cp38-cp38 cp39-cp39 cp310-cp310 cp311-cp311"
+        if: "matrix.os == 'ubuntu-latest'"
+      - name: Build (Linux)
+        uses: RalfG/python-wheels-manylinux-build@v0.5.0
+        with:
+          python-versions: "cp36-cp36m cp37-cp37m cp38-cp38 cp39-cp39 cp310-cp310 cp311-cp311"
+        env:
+          # Temporary fix for LD_LIBRARY_PATH issue. See
+          # https://github.com/RalfG/python-wheels-manylinux-build/issues/26
+          LD_LIBRARY_PATH: /usr/local/lib:${{ env.LD_LIBRARY_PATH }}
+        if: "matrix.os == 'ubuntu-latest'"
+      - name: Upload wheels (Linux)
+        uses: actions/upload-artifact@v2
+        # Only include *manylinux* wheels; the other wheels files are built but
+        # rejected by pip.
+        if: "matrix.os == 'ubuntu-latest'"
+        with:
+          name: dist
+          path: dist/*manylinux*.whl
+      - name: Upload wheels (non-Linux)
+        uses: actions/upload-artifact@v2
+        with:
+          name: dist
+          path: dist/*.whl
+        if: "matrix.os != 'ubuntu-latest'"
 
   publish:
     runs-on: ubuntu-latest
@@ -84,18 +88,18 @@ jobs:
     needs: build
     if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/dulwich-')
     steps:
-    - name: Set up Python
-      uses: actions/setup-python@v2
-      with:
-        python-version: "3.x"
-    - name: Install twine
-      run: |
-        python -m pip install --upgrade pip
-        pip install twine
-    - name: Download wheels
-      uses: actions/download-artifact@v2
-    - name: Publish wheels
-      env:
-        TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
-        TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
-      run: twine upload dist/*.whl
+      - name: Set up Python
+        uses: actions/setup-python@v2
+        with:
+          python-version: "3.x"
+      - name: Install twine
+        run: |
+          python -m pip install --upgrade pip
+          pip install twine
+      - name: Download wheels
+        uses: actions/download-artifact@v2
+      - name: Publish wheels
+        env:
+          TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
+          TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
+        run: twine upload dist/*.whl

+ 17 - 16
CODE_OF_CONDUCT.md

@@ -14,22 +14,22 @@ appearance, race, religion, or sexual identity and orientation.
 Examples of behavior that contributes to creating a positive environment
 include:
 
-* Using welcoming and inclusive language
-* Being respectful of differing viewpoints and experiences
-* Gracefully accepting constructive criticism
-* Focusing on what is best for the community
-* Showing empathy towards other community members
+- Using welcoming and inclusive language
+- Being respectful of differing viewpoints and experiences
+- Gracefully accepting constructive criticism
+- Focusing on what is best for the community
+- Showing empathy towards other community members
 
 Examples of unacceptable behavior by participants include:
 
-* The use of sexualized language or imagery and unwelcome sexual attention or
- advances
-* Trolling, insulting/derogatory comments, and personal or political attacks
-* Public or private harassment
-* Publishing others' private information, such as a physical or electronic
- address, without explicit permission
-* Other conduct which could reasonably be considered inappropriate in a
- professional setting
+- The use of sexualized language or imagery and unwelcome sexual attention or
+  advances
+- Trolling, insulting/derogatory comments, and personal or political attacks
+- Public or private harassment
+- Publishing others' private information, such as a physical or electronic
+  address, without explicit permission
+- Other conduct which could reasonably be considered inappropriate in a
+  professional setting
 
 ## Our Responsibilities
 
@@ -67,10 +67,11 @@ members of the project's leadership.
 
 ## Attribution
 
-This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
-available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 1.4, available at
+<https://www.contributor-covenant.org/version/1/4/code-of-conduct.html>
 
 [homepage]: https://www.contributor-covenant.org
 
 For answers to common questions about this code of conduct, see
-https://www.contributor-covenant.org/faq
+<https://www.contributor-covenant.org/faq>

+ 1 - 1
Makefile

@@ -74,4 +74,4 @@ coverage-html: coverage
 .PHONY: apidocs
 
 apidocs:
-	pydoctor --docformat=google dulwich --project-url=https://www.dulwich.io/
+	pydoctor --intersphinx http://urllib3.readthedocs.org/en/latest/objects.inv --intersphinx http://docs.python.org/3/objects.inv --docformat=google dulwich --project-url=https://www.dulwich.io/

+ 65 - 4
NEWS

@@ -1,3 +1,64 @@
+0.20.46	2022-09-06
+
+ * Apply insteadOf to rsync-style location strings
+   (previously it was just applied to URLs).
+   (Jelmer Vernooij, python-poetry/poetry#6329)
+
+ * Drop use of certifi, instead relying on urllib3's default
+   code to find system CAs. (Jelmer Vernooij, #1025)
+
+ * Implement timezone parsing in porcelain.
+   (springheeledjack0, #1026)
+
+ * Drop support for running without setuptools.
+   (Jelmer Vernooij)
+
+ * Ensure configuration is loaded when
+   running "dulwich clone".
+   (Jelmer Vernooij)
+
+ * Build 32 bit wheels for Windows.
+   (Benjamin Parzella)
+
+ * tests: Ignore errors when deleting GNUPG 
+   home directory. Fixes spurious errors racing
+   gnupg-agent. Thanks, Matěj Cepl. Fixes #1000
+
+ * config: Support closing brackets in quotes in section
+   names. (Jelmer Vernooij, #10124)
+
+ * Various and formatting fixes. (Kian-Meng Ang)
+
+ * Document basic authentication in dulwich.porcelain.clone.
+   (TuringTux)
+
+ * Flush before calling fsync, ensuring buffers
+   are filled. (wernha)
+
+ * Support GPG commit signing. (springheeledjack0)
+
+ * Add python 3.11 support. (Saugat Pachhai)
+
+ * Allow missing GPG during tests. (Jakub Kulík)
+
+ * status: return posix-style untracked paths instead of nt-style paths on
+   win32 (Daniele Trifirò)
+
+ * Honour PATH environment when running C Git for testing.
+   (Stefan Sperling)
+
+ * Split out exception for symbolic reference loops.
+   (Jelmer Vernooij)
+
+ * Move various long-deprecated methods.
+   (Jelmer Vernooij)
+
+
+0.20.45	2022-07-15
+
+ * Add basic ``dulwich.porcelain.submodule_list`` and ``dulwich.porcelain.submodule_add``
+  (Jelmer Vernooij)
+
 0.20.44	2022-06-30
 
  * Fix reading of chunks in server. (Jelmer Vernooij, #977)
@@ -980,7 +1041,7 @@
     probing the filesystem for trustable permissions.
     (Koen Martens)
 
-  * Fix ``porcelain.reset`` to respect the comittish argument.
+  * Fix ``porcelain.reset`` to respect the committish argument.
     (Koen Martens)
 
   * Fix dulwich.porcelain.ls_remote() on Python 3.
@@ -2017,7 +2078,7 @@ FEATURES
 
   * Provide strnlen() on mingw32 which doesn't have it. (Hans Kolek)
 
-  * Set bare=true in the configuratin for bare repositories. (Dirk Neumann)
+  * Set bare=true in the configuration for bare repositories. (Dirk Neumann)
 
  FEATURES
 
@@ -2101,7 +2162,7 @@ FEATURES
 
  FEATURES
 
-  * Move named file initilization to BaseRepo. (Dave Borowitz)
+  * Move named file initialization to BaseRepo. (Dave Borowitz)
 
   * Add logging utilities and git/HTTP server logging. (Dave Borowitz)
 
@@ -2182,7 +2243,7 @@ note: This list is most likely incomplete for 0.6.0.
   * Fix RefsContainer.add_if_new to support dangling symrefs.
     (Dave Borowitz)
 
-  * Non-existant index files in non-bare repositories are now treated as 
+  * Non-existent index files in non-bare repositories are now treated as 
     empty. (Dave Borowitz)
 
   * Always update ShaFile.id when the contents of the object get changed. 

+ 2 - 1
PKG-INFO

@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: dulwich
-Version: 0.20.44
+Version: 0.20.46
 Summary: Python Git Library
 Home-page: https://www.dulwich.io/
 Author: Jelmer Vernooij
@@ -18,6 +18,7 @@ Classifier: Programming Language :: Python :: 3.7
 Classifier: Programming Language :: Python :: 3.8
 Classifier: Programming Language :: Python :: 3.9
 Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
 Classifier: Programming Language :: Python :: Implementation :: CPython
 Classifier: Programming Language :: Python :: Implementation :: PyPy
 Classifier: Operating System :: POSIX

+ 2 - 2
README.swift.rst

@@ -46,7 +46,7 @@ be used as template::
 
 
 Note that for now we use the same tenant to perform the requests
-against Swift. Therefor there is only one Swift account used
+against Swift. Therefore there is only one Swift account used
 for storing repositories. Each repository will be contained in
 a Swift container.
 
@@ -118,7 +118,7 @@ The other Git commands can be used the way you do usually against
 a regular repository.
 
 Note the daemon subcommands starts a Git server listening for the
-Git protocol. Therefor there is no authentication or encryption
+Git protocol. Therefore there is no authentication or encryption
 at all between the cGIT client and the GIT server (Dulwich).
 
 Note on the .info file for pack object

+ 2 - 1
SECURITY.md

@@ -9,4 +9,5 @@
 
 ## Reporting a Vulnerability
 
-Please report security issues by e-mail to jelmer@jelmer.uk, ideally PGP encrypted to the key at https://jelmer.uk/D729A457.asc
+Please report security issues by e-mail to jelmer@jelmer.uk, ideally PGP
+encrypted to the key at <https://jelmer.uk/D729A457.asc>

+ 9 - 3
docs/conf.py

@@ -31,7 +31,7 @@ extensions = [
     'sphinx.ext.ifconfig',
     'sphinx.ext.intersphinx',
     'sphinx.ext.napoleon',
-    ]
+]
 
 autoclass_content = "both"
 
@@ -186,8 +186,8 @@ htmlhelp_basename = 'dulwichdoc'
 # (source start file, target name, title, author, documentclass
 # [howto/manual]).
 latex_documents = [
-  ('index', 'dulwich.tex', u'dulwich Documentation',
-   u'Jelmer Vernooij', 'manual'),
+    ('index', 'dulwich.tex', u'dulwich Documentation',
+     'Jelmer Vernooij', 'manual'),
 ]
 
 # The name of an image file (relative to this directory) to place at the top of
@@ -206,3 +206,9 @@ latex_documents = [
 
 # If false, no module index is generated.
 # latex_use_modindex = True
+
+# Add mappings
+intersphinx_mapping = {
+    'urllib3': ('http://urllib3.readthedocs.org/en/latest', None),
+    'python': ('http://docs.python.org/3', None),
+}

+ 1 - 1
docs/tutorial/encoding.txt

@@ -12,7 +12,7 @@ and commit messages.
 .. _C git: https://github.com/git/git/blob/master/Documentation/i18n.txt
 
 The library should be able to read *all* existing git repositories,
-irregardless of what encoding they use. This is the main reason why Dulwich
+regardless of what encoding they use. This is the main reason why Dulwich
 does not convert paths to unicode strings.
 
 A further consideration is that converting back and forth to unicode

+ 7 - 0
docs/tutorial/porcelain.txt

@@ -24,6 +24,13 @@ Clone a repository
 ------------------
 
   >>> porcelain.clone("git://github.com/jelmer/dulwich", "dulwich-clone")
+  
+Basic authentication works using the ``username`` and ``password`` parameters:
+
+  >>> porcelain.clone(
+      "https://example.com/a-private-repo.git",
+      "a-private-repo-clone",
+      username="user", password="password")
 
 Commit changes
 --------------

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

@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: dulwich
-Version: 0.20.44
+Version: 0.20.46
 Summary: Python Git Library
 Home-page: https://www.dulwich.io/
 Author: Jelmer Vernooij
@@ -18,6 +18,7 @@ Classifier: Programming Language :: Python :: 3.7
 Classifier: Programming Language :: Python :: 3.8
 Classifier: Programming Language :: Python :: 3.9
 Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
 Classifier: Programming Language :: Python :: Implementation :: CPython
 Classifier: Programming Language :: Python :: Implementation :: PyPy
 Classifier: Operating System :: POSIX

+ 81 - 80
dulwich.egg-info/SOURCES.txt

@@ -89,6 +89,7 @@ dulwich/repo.py
 dulwich/server.py
 dulwich/stash.py
 dulwich/stdint.h
+dulwich/submodule.py
 dulwich/walk.py
 dulwich/web.py
 dulwich.egg-info/PKG-INFO
@@ -168,89 +169,89 @@ 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
+examples/rename-branch.py
+testdata/blobs/11/11111111111111111111111111111111111111
+testdata/blobs/6f/670c0fb53f9463760b7295fbb814e965fb20c8
+testdata/blobs/95/4a536f7819d40e6f637f849ee187dd10066349
+testdata/blobs/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391
+testdata/commits/0d/89f20333fbb1d2f3a94da77f4981373d8f4310
+testdata/commits/5d/ac377bdded4c9aeb8dff595f0faeebcc8498cc
+testdata/commits/60/dacdc733de308bb77bb76ce0fb0f9b44c9769e
+testdata/indexes/index
+testdata/packs/pack-bc63ddad95e7321ee734ea11a7a62d314e0d7481.idx
+testdata/packs/pack-bc63ddad95e7321ee734ea11a7a62d314e0d7481.pack
+testdata/repos/.gitattributes
+testdata/repos/issue88_expect_ack_nak_client.export
+testdata/repos/issue88_expect_ack_nak_other.export
+testdata/repos/issue88_expect_ack_nak_server.export
+testdata/repos/server_new.export
+testdata/repos/server_old.export
+testdata/repos/a.git/HEAD
+testdata/repos/a.git/packed-refs
+testdata/repos/a.git/objects/28/237f4dc30d0d462658d6b937b08a0f0b6ef55a
+testdata/repos/a.git/objects/2a/72d929692c41d8554c07f6301757ba18a65d91
+testdata/repos/a.git/objects/4e/f30bbfe26431a69c3820d3a683df54d688f2ec
+testdata/repos/a.git/objects/4f/2e6529203aa6d44b5af6e3292c837ceda003f9
+testdata/repos/a.git/objects/7d/9a07d797595ef11344549b8d08198e48c15364
+testdata/repos/a.git/objects/a2/96d0bb611188cabb256919f36bc30117cca005
+testdata/repos/a.git/objects/a9/0fa2d900a17e99b433217e988c4eb4a2e9a097
+testdata/repos/a.git/objects/b0/931cadc54336e78a1d980420e3268903b57a50
+testdata/repos/a.git/objects/ff/d47d45845a8f6576491e1edb97e3fe6a850e7f
+testdata/repos/a.git/refs/heads/master
+testdata/repos/a.git/refs/tags/mytag
+testdata/repos/empty.git/HEAD
+testdata/repos/empty.git/config
+testdata/repos/empty.git/objects/info/.gitignore
+testdata/repos/empty.git/objects/pack/.gitignore
+testdata/repos/empty.git/refs/heads/.gitignore
+testdata/repos/empty.git/refs/tags/.gitignore
+testdata/repos/ooo_merge.git/HEAD
+testdata/repos/ooo_merge.git/objects/29/69be3e8ee1c0222396a5611407e4769f14e54b
+testdata/repos/ooo_merge.git/objects/38/74e9c60a6d149c44c928140f250d81e6381520
+testdata/repos/ooo_merge.git/objects/6f/670c0fb53f9463760b7295fbb814e965fb20c8
+testdata/repos/ooo_merge.git/objects/70/c190eb48fa8bbb50ddc692a17b44cb781af7f6
+testdata/repos/ooo_merge.git/objects/76/01d7f6231db6a57f7bbb79ee52e4d462fd44d1
+testdata/repos/ooo_merge.git/objects/90/182552c4a85a45ec2a835cadc3451bebdfe870
+testdata/repos/ooo_merge.git/objects/95/4a536f7819d40e6f637f849ee187dd10066349
+testdata/repos/ooo_merge.git/objects/b2/a2766a2879c209ab1176e7e778b81ae422eeaa
+testdata/repos/ooo_merge.git/objects/f5/07291b64138b875c28e03469025b1ea20bc614
+testdata/repos/ooo_merge.git/objects/f9/e39b120c68182a4ba35349f832d0e4e61f485c
+testdata/repos/ooo_merge.git/objects/fb/5b0425c7ce46959bec94d54b9a157645e114f5
+testdata/repos/ooo_merge.git/refs/heads/master
+testdata/repos/refs.git/HEAD
+testdata/repos/refs.git/packed-refs
+testdata/repos/refs.git/objects/3b/9e5457140e738c2dcd39bf6d7acf88379b90d1
+testdata/repos/refs.git/objects/3e/c9c43c84ff242e3ef4a9fc5bc111fd780a76a8
+testdata/repos/refs.git/objects/42/d06bd4b77fed026b154d16493e5deab78f02ec
+testdata/repos/refs.git/objects/a1/8114c31713746a33a2e70d9914d1ef3e781425
+testdata/repos/refs.git/objects/cd/a609072918d7b70057b6bef9f4c2537843fcfe
+testdata/repos/refs.git/objects/df/6800012397fb85c56e7418dd4eb9405dee075c
+testdata/repos/refs.git/refs/heads/40-char-ref-aaaaaaaaaaaaaaaaaa
+testdata/repos/refs.git/refs/heads/loop
+testdata/repos/refs.git/refs/heads/master
+testdata/repos/refs.git/refs/tags/refs-0.2
+testdata/repos/simple_merge.git/HEAD
+testdata/repos/simple_merge.git/objects/0d/89f20333fbb1d2f3a94da77f4981373d8f4310
+testdata/repos/simple_merge.git/objects/1b/6318f651a534b38f9c7aedeebbd56c1e896853
+testdata/repos/simple_merge.git/objects/29/69be3e8ee1c0222396a5611407e4769f14e54b
+testdata/repos/simple_merge.git/objects/4c/ffe90e0a41ad3f5190079d7c8f036bde29cbe6
+testdata/repos/simple_merge.git/objects/5d/ac377bdded4c9aeb8dff595f0faeebcc8498cc
+testdata/repos/simple_merge.git/objects/60/dacdc733de308bb77bb76ce0fb0f9b44c9769e
+testdata/repos/simple_merge.git/objects/6f/670c0fb53f9463760b7295fbb814e965fb20c8
+testdata/repos/simple_merge.git/objects/70/c190eb48fa8bbb50ddc692a17b44cb781af7f6
+testdata/repos/simple_merge.git/objects/90/182552c4a85a45ec2a835cadc3451bebdfe870
+testdata/repos/simple_merge.git/objects/95/4a536f7819d40e6f637f849ee187dd10066349
+testdata/repos/simple_merge.git/objects/ab/64bbdcc51b170d21588e5c5d391ee5c0c96dfd
+testdata/repos/simple_merge.git/objects/d4/bdad6549dfedf25d3b89d21f506aff575b28a7
+testdata/repos/simple_merge.git/objects/d8/0c186a03f423a81b39df39dc87fd269736ca86
+testdata/repos/simple_merge.git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391
+testdata/repos/simple_merge.git/refs/heads/master
+testdata/repos/submodule/dotgit
+testdata/tags/71/033db03a03c6a36721efcf1968dd8f8e0cf023
+testdata/trees/70/c190eb48fa8bbb50ddc692a17b44cb781af7f6

+ 2 - 3
dulwich.egg-info/requires.txt

@@ -1,11 +1,10 @@
-certifi
-urllib3>=1.24.1
+urllib3>=1.25
 
 [fastimport]
 fastimport
 
 [https]
-urllib3[secure]>=1.24.1
+urllib3>=1.24.1
 
 [paramiko]
 paramiko

+ 1 - 1
dulwich/__init__.py

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

+ 1 - 1
dulwich/archive.py

@@ -90,7 +90,7 @@ def tar_stream(store, tree, mtime, prefix=b"", format=""):
         if format == "gz":
             # Manually correct the gzip header file modification time so that
             # archives created from the same Git tree are always identical.
-            # The gzip header file modification time is not currenctly
+            # The gzip header file modification time is not currently
             # accessible from the tarfile API, see:
             # https://bugs.python.org/issue31526
             buf.seek(0)

+ 16 - 2
dulwich/cli.py

@@ -321,6 +321,14 @@ class cmd_rev_list(Command):
         porcelain.rev_list(".", args)
 
 
+class cmd_submodule(Command):
+    def run(self, args):
+        parser = optparse.OptionParser()
+        options, args = parser.parse_args(args)
+        for path, sha in porcelain.submodule_list("."):
+            sys.stdout.write(' %s %s\n' % (sha, path))
+
+
 class cmd_tag(Command):
     def run(self, args):
         parser = optparse.OptionParser()
@@ -499,7 +507,7 @@ class cmd_ls_tree(Command):
             "-r",
             "--recursive",
             action="store_true",
-            help="Recusively list tree contents.",
+            help="Recursively list tree contents.",
         )
         parser.add_option("--name-only", action="store_true", help="Only display name.")
         options, args = parser.parse_args(args)
@@ -553,10 +561,15 @@ class cmd_push(Command):
 
     def run(self, argv):
         parser = argparse.ArgumentParser()
+        parser.add_argument('-f', '--force', action='store_true', help='Force')
         parser.add_argument('to_location', type=str)
         parser.add_argument('refspec', type=str, nargs='*')
         args = parser.parse_args(argv)
-        porcelain.push('.', args.to_location, args.refspec or None)
+        try:
+            porcelain.push('.', args.to_location, args.refspec or None, force=args.force)
+        except porcelain.DivergedBranches:
+            sys.stderr.write('Diverged branches; specify --force to override')
+            return 1
 
 
 class cmd_remote_add(Command):
@@ -721,6 +734,7 @@ commands = {
     "stash": cmd_stash,
     "status": cmd_status,
     "symbolic-ref": cmd_symbolic_ref,
+    "submodule": cmd_submodule,
     "tag": cmd_tag,
     "update-server-info": cmd_update_server_info,
     "upload-pack": cmd_upload_pack,

+ 59 - 86
dulwich/client.py

@@ -46,7 +46,18 @@ import select
 import socket
 import subprocess
 import sys
-from typing import Any, Callable, Dict, List, Optional, Set, Tuple, IO, Iterable
+from typing import (
+    Any,
+    Callable,
+    Dict,
+    List,
+    Optional,
+    Set,
+    Tuple,
+    IO,
+    Union,
+    TYPE_CHECKING,
+)
 
 from urllib.parse import (
     quote as urlquote,
@@ -57,9 +68,12 @@ from urllib.parse import (
     urlunparse,
 )
 
+if TYPE_CHECKING:
+    import urllib3
+
 
 import dulwich
-from dulwich.config import get_xdg_config_home_path, Config
+from dulwich.config import get_xdg_config_home_path, Config, apply_instead_of
 from dulwich.errors import (
     GitProtocolError,
     NotGitRepository,
@@ -437,7 +451,6 @@ class _v1ReceivePackHeader(object):
         """Handle the head of a 'git-receive-pack' request.
 
         Args:
-          proto: Protocol object to read from
           capabilities: List of negotiated capabilities
           old_refs: Old refs, as received from the server
           new_refs: Refs to change
@@ -950,6 +963,21 @@ class GitClient(object):
         negotiated_capabilities = self._fetch_capabilities & server_capabilities
         return (negotiated_capabilities, symrefs, agent)
 
+    def archive(
+        self,
+        path,
+        committish,
+        write_data,
+        progress=None,
+        write_error=None,
+        format=None,
+        subdirs=None,
+        prefix=None,
+    ):
+        """Retrieve an archive of the specified tree.
+        """
+        raise NotImplementedError(self.archive)
+
 
 def check_wants(wants, refs):
     """Check that a set of wants is valid.
@@ -1533,31 +1561,6 @@ default_local_git_client_cls = LocalGitClient
 class SSHVendor(object):
     """A client side SSH implementation."""
 
-    def connect_ssh(
-        self,
-        host,
-        command,
-        username=None,
-        port=None,
-        password=None,
-        key_filename=None,
-    ):
-        # This function was deprecated in 0.9.1
-        import warnings
-
-        warnings.warn(
-            "SSHVendor.connect_ssh has been renamed to SSHVendor.run_command",
-            DeprecationWarning,
-        )
-        return self.run_command(
-            host,
-            command,
-            username=username,
-            port=port,
-            password=password,
-            key_filename=key_filename,
-        )
-
     def run_command(
         self,
         host,
@@ -1616,7 +1619,8 @@ class SubprocessSSHVendor(SSHVendor):
 
         if ssh_command:
             import shlex
-            args = shlex.split(ssh_command) + ["-x"]
+            args = shlex.split(
+                ssh_command, posix=(sys.platform != 'win32')) + ["-x"]
         else:
             args = ["ssh", "-x"]
 
@@ -1658,7 +1662,8 @@ class PLinkSSHVendor(SSHVendor):
 
         if ssh_command:
             import shlex
-            args = shlex.split(ssh_command) + ["-ssh"]
+            args = shlex.split(
+                ssh_command, posix=(sys.platform != 'win32')) + ["-ssh"]
         elif sys.platform == "win32":
             args = ["plink.exe", "-ssh"]
         else:
@@ -1781,7 +1786,7 @@ class SSHGitClient(TraditionalGitClient):
             kwargs["password"] = self.password
         if self.key_filename is not None:
             kwargs["key_filename"] = self.key_filename
-        # GIT_SSH_COMMAND takes precendence over GIT_SSH
+        # GIT_SSH_COMMAND takes precedence over GIT_SSH
         if self.ssh_command is not None:
             kwargs["ssh_command"] = self.ssh_command
         con = self.ssh_vendor.run_command(
@@ -1807,8 +1812,8 @@ def default_user_agent_string():
 
 def default_urllib3_manager(   # noqa: C901
     config, pool_manager_cls=None, proxy_manager_cls=None, **override_kwargs
-):
-    """Return `urllib3` connection pool manager.
+) -> Union["urllib3.ProxyManager", "urllib3.PoolManager"]:
+    """Return urllib3 connection pool manager.
 
     Honour detected proxy configurations.
 
@@ -1817,9 +1822,9 @@ def default_urllib3_manager(   # noqa: C901
       override_kwargs: Additional arguments for `urllib3.ProxyManager`
 
     Returns:
-      `pool_manager_cls` (defaults to `urllib3.ProxyManager`) instance for
-      proxy configurations, `proxy_manager_cls` (defaults to
-      `urllib3.PoolManager`) instance otherwise.
+      Either pool_manager_cls (defaults to `urllib3.ProxyManager`) instance for
+      proxy configurations, proxy_manager_cls
+      (defaults to `urllib3.PoolManager`) instance otherwise
 
     """
     proxy_server = user_agent = None
@@ -1858,7 +1863,9 @@ def default_urllib3_manager(   # noqa: C901
 
     headers = {"User-agent": user_agent}
 
-    kwargs = {}
+    kwargs = {
+        "ca_certs" : ca_certs,
+    }
     if ssl_verify is True:
         kwargs["cert_reqs"] = "CERT_REQUIRED"
     elif ssl_verify is False:
@@ -1867,19 +1874,8 @@ def default_urllib3_manager(   # noqa: C901
         # Default to SSL verification
         kwargs["cert_reqs"] = "CERT_REQUIRED"
 
-    if ca_certs is not None:
-        kwargs["ca_certs"] = ca_certs
     kwargs.update(override_kwargs)
 
-    # Try really hard to find a SSL certificate path
-    if "ca_certs" not in kwargs and kwargs.get("cert_reqs") != "CERT_NONE":
-        try:
-            import certifi
-        except ImportError:
-            pass
-        else:
-            kwargs["ca_certs"] = certifi.where()
-
     import urllib3
 
     if proxy_server is not None:
@@ -1919,9 +1915,9 @@ class AbstractHttpGitClient(GitClient):
           data: Request data.
 
         Returns:
-          Tuple (`response`, `read`), where response is an `urllib3`
-          response object with additional `content_type` and
-          `redirect_location` properties, and `read` is a consumable read
+          Tuple (response, read), where response is an urllib3
+          response object with additional content_type and
+          redirect_location properties, and read is a consumable read
           method for the response data.
 
         """
@@ -2268,40 +2264,6 @@ def _win32_url_to_path(parsed) -> str:
     return url2pathname(netloc + path)  # type: ignore
 
 
-def iter_instead_of(config: Config, push: bool = False) -> Iterable[Tuple[str, str]]:
-    """Iterate over insteadOf / pushInsteadOf values.
-    """
-    for section in config.sections():
-        if section[0] != b'url':
-            continue
-        replacement = section[1]
-        try:
-            needles = list(config.get_multivar(section, "insteadOf"))
-        except KeyError:
-            needles = []
-        if push:
-            try:
-                needles += list(config.get_multivar(section, "pushInsteadOf"))
-            except KeyError:
-                pass
-        for needle in needles:
-            yield needle.decode('utf-8'), replacement.decode('utf-8')
-
-
-def apply_instead_of(config: Config, orig_url: str, push: bool = False) -> str:
-    """Apply insteadOf / pushInsteadOf to a URL.
-    """
-    longest_needle = ""
-    updated_url = orig_url
-    for needle, replacement in iter_instead_of(config, push):
-        if not orig_url.startswith(needle):
-            continue
-        if len(longest_needle) < len(needle):
-            longest_needle = needle
-            updated_url = replacement + orig_url[len(needle):]
-    return updated_url
-
-
 def get_transport_and_path_from_url(
         url: str, config: Optional[Config] = None,
         operation: Optional[str] = None, **kwargs) -> Tuple[GitClient, str]:
@@ -2321,6 +2283,12 @@ def get_transport_and_path_from_url(
     """
     if config is not None:
         url = apply_instead_of(config, url, push=(operation == "push"))
+
+    return _get_transport_and_path_from_url(
+        url, config=config, operation=operation, **kwargs)
+
+
+def _get_transport_and_path_from_url(url, config, operation, **kwargs):
     parsed = urlparse(url)
     if parsed.scheme == "git":
         return (TCPGitClient.from_parsedurl(parsed, **kwargs), parsed.path)
@@ -2363,6 +2331,7 @@ def parse_rsync_url(location: str) -> Tuple[Optional[str], str, str]:
 
 def get_transport_and_path(
     location: str,
+    config: Optional[Config] = None,
     operation: Optional[str] = None,
     **kwargs: Any
 ) -> Tuple[GitClient, str]:
@@ -2380,9 +2349,13 @@ def get_transport_and_path(
       Tuple with client instance and relative path.
 
     """
+    if config is not None:
+        location = apply_instead_of(config, location, push=(operation == "push"))
+
     # First, try to parse it as a URL
     try:
-        return get_transport_and_path_from_url(location, operation=operation, **kwargs)
+        return _get_transport_and_path_from_url(
+            location, config=config, operation=operation, **kwargs)
     except ValueError:
         pass
 

+ 80 - 52
dulwich/config.py

@@ -28,7 +28,6 @@ TODO:
 
 import os
 import sys
-import warnings
 from typing import (
     BinaryIO,
     Iterable,
@@ -45,7 +44,7 @@ from typing import (
 from dulwich.file import GitFile
 
 
-SENTINAL = object()
+SENTINEL = object()
 
 
 def lower_key(key):
@@ -112,13 +111,13 @@ class CaseInsensitiveOrderedMultiDict(MutableMapping):
     def __getitem__(self, item):
         return self._keyed[lower_key(item)]
 
-    def get(self, key, default=SENTINAL):
+    def get(self, key, default=SENTINEL):
         try:
             return self[key]
         except KeyError:
             pass
 
-        if default is SENTINAL:
+        if default is SENTINEL:
             return type(self)()
 
         return default
@@ -129,7 +128,7 @@ class CaseInsensitiveOrderedMultiDict(MutableMapping):
             if lower_key(actual) == key:
                 yield value
 
-    def setdefault(self, key, default=SENTINAL):
+    def setdefault(self, key, default=SENTINEL):
         try:
             return self[key]
         except KeyError:
@@ -231,29 +230,6 @@ class Config(object):
         """
         raise NotImplementedError(self.items)
 
-    def iteritems(self, section: SectionLike) -> Iterator[Tuple[Name, Value]]:
-        """Iterate over the configuration pairs for a specific section.
-
-        Args:
-          section: Tuple with section name and optional subsection namee
-        Returns:
-          Iterator over (name, value) pairs
-        """
-        warnings.warn(
-            "Use %s.items instead." % type(self).__name__,
-            DeprecationWarning,
-            stacklevel=3,
-        )
-        return self.items(section)
-
-    def itersections(self) -> Iterator[Section]:
-        warnings.warn(
-            "Use %s.items instead." % type(self).__name__,
-            DeprecationWarning,
-            stacklevel=3,
-        )
-        return self.sections()
-
     def sections(self) -> Iterator[Section]:
         """Iterate over the sections.
 
@@ -507,6 +483,46 @@ def _strip_comments(line: bytes) -> bytes:
     return line
 
 
+def _parse_section_header_line(line: bytes) -> Tuple[Section, bytes]:
+    # Parse section header ("[bla]")
+    line = _strip_comments(line).rstrip()
+    in_quotes = False
+    escaped = False
+    for i, c in enumerate(line):
+        if escaped:
+            escaped = False
+            continue
+        if c == ord(b'"'):
+            in_quotes = not in_quotes
+        if c == ord(b'\\'):
+            escaped = True
+        if c == ord(b']') and not in_quotes:
+            last = i
+            break
+    else:
+        raise ValueError("expected trailing ]")
+    pts = line[1:last].split(b" ", 1)
+    line = line[last + 1:]
+    section: Section
+    if len(pts) == 2:
+        if pts[1][:1] != b'"' or pts[1][-1:] != b'"':
+            raise ValueError("Invalid subsection %r" % pts[1])
+        else:
+            pts[1] = pts[1][1:-1]
+        if not _check_section_name(pts[0]):
+            raise ValueError("invalid section name %r" % pts[0])
+        section = (pts[0], pts[1])
+    else:
+        if not _check_section_name(pts[0]):
+            raise ValueError("invalid section name %r" % pts[0])
+        pts = pts[0].split(b".", 1)
+        if len(pts) == 2:
+            section = (pts[0], pts[1])
+        else:
+            section = (pts[0],)
+    return section, line
+
+
 class ConfigFile(ConfigDict):
     """A Git configuration file, like .git/config or ~/.gitconfig."""
 
@@ -532,31 +548,8 @@ class ConfigFile(ConfigDict):
                 line = line[3:]
             line = line.lstrip()
             if setting is None:
-                # Parse section header ("[bla]")
                 if len(line) > 0 and line[:1] == b"[":
-                    line = _strip_comments(line).rstrip()
-                    try:
-                        last = line.index(b"]")
-                    except ValueError:
-                        raise ValueError("expected trailing ]")
-                    pts = line[1:last].split(b" ", 1)
-                    line = line[last + 1 :]
-                    if len(pts) == 2:
-                        if pts[1][:1] != b'"' or pts[1][-1:] != b'"':
-                            raise ValueError("Invalid subsection %r" % pts[1])
-                        else:
-                            pts[1] = pts[1][1:-1]
-                        if not _check_section_name(pts[0]):
-                            raise ValueError("invalid section name %r" % pts[0])
-                        section = (pts[0], pts[1])
-                    else:
-                        if not _check_section_name(pts[0]):
-                            raise ValueError("invalid section name %r" % pts[0])
-                        pts = pts[0].split(b".", 1)
-                        if len(pts) == 2:
-                            section = (pts[0], pts[1])
-                        else:
-                            section = (pts[0],)
+                    section, line = _parse_section_header_line(line)
                     ret._values.setdefault(section)
                 if _strip_comments(line).strip() == b"":
                     continue
@@ -772,3 +765,38 @@ def parse_submodules(config: ConfigFile) -> Iterator[Tuple[bytes, bytes, bytes]]
             sm_path = config.get(section, b"path")
             sm_url = config.get(section, b"url")
             yield (sm_path, sm_url, section_name)
+
+
+def iter_instead_of(config: Config, push: bool = False) -> Iterable[Tuple[str, str]]:
+    """Iterate over insteadOf / pushInsteadOf values.
+    """
+    for section in config.sections():
+        if section[0] != b'url':
+            continue
+        replacement = section[1]
+        try:
+            needles = list(config.get_multivar(section, "insteadOf"))
+        except KeyError:
+            needles = []
+        if push:
+            try:
+                needles += list(config.get_multivar(section, "pushInsteadOf"))
+            except KeyError:
+                pass
+        for needle in needles:
+            assert isinstance(needle, bytes)
+            yield needle.decode('utf-8'), replacement.decode('utf-8')
+
+
+def apply_instead_of(config: Config, orig_url: str, push: bool = False) -> str:
+    """Apply insteadOf / pushInsteadOf to a URL.
+    """
+    longest_needle = ""
+    updated_url = orig_url
+    for needle, replacement in iter_instead_of(config, push):
+        if not orig_url.startswith(needle):
+            continue
+        if len(longest_needle) < len(needle):
+            longest_needle = needle
+            updated_url = replacement + orig_url[len(needle):]
+    return updated_url

+ 2 - 2
dulwich/contrib/diffstat.py

@@ -184,7 +184,7 @@ def diffstat(lines, max_width=80):
 
 def main():
     argv = sys.argv
-    # allow diffstat.py to also be used from the comand line
+    # allow diffstat.py to also be used from the command line
     if len(sys.argv) > 1:
         diffpath = argv[1]
         data = b""
@@ -197,7 +197,7 @@ def main():
 
     # if no path argument to a diff file is passed in, run
     # a self test. The test case includes tricky things like
-    # a diff of diff, binary files, renames with futher changes
+    # a diff of diff, binary files, renames with further changes
     # added files and removed files.
     # All extracted from Sigil-Ebook/Sigil's github repo with
     # full permission to use under this license.

+ 1 - 1
dulwich/contrib/release_robot.py

@@ -29,7 +29,7 @@ Copy the following into the package ``__init__.py`` module::
     __version__ = get_current_version()
 
 This example assumes the tags have a leading "v" like "v0.3", and that the
-``.git`` folder is in a project folder that containts the package folder.
+``.git`` folder is in a project folder that contains the package folder.
 
 EG::
 

+ 2 - 2
dulwich/contrib/swift.py

@@ -504,9 +504,9 @@ class SwiftPackReader(object):
     """A SwiftPackReader that mimic read and sync method
 
     The reader allows to read a specified amount of bytes from
-    a given offset of a Swift object. A read offset is kept internaly.
+    a given offset of a Swift object. A read offset is kept internally.
     The reader will read from Swift a specified amount of data to complete
-    its internal buffer. chunk_length specifiy the amount of data
+    its internal buffer. chunk_length specify the amount of data
     to read from Swift.
     """
 

+ 1 - 1
dulwich/contrib/test_paramiko_vendor.py

@@ -131,7 +131,7 @@ class ParamikoSSHVendorTests(TestCase):
     def setUp(self):
         import paramiko.transport
 
-        # reenable server functionality for tests
+        # re-enable server functionality for tests
         if hasattr(paramiko.transport, "SERVER_DISABLED_BY_GENTOO"):
             paramiko.transport.SERVER_DISABLED_BY_GENTOO = False
 

+ 1 - 1
dulwich/contrib/test_swift_smoke.py

@@ -123,7 +123,7 @@ class SwiftRepoSmokeTest(unittest.TestCase):
         swift.SwiftRepo.init_bare(self.scon, self.conf)
         tcp_client = client.TCPGitClient(self.server_address, port=self.port)
         remote_refs = tcp_client.fetch(self.fakerepo, local_repo)
-        # The remote repo is empty (no refs retreived)
+        # The remote repo is empty (no refs retrieved)
         self.assertEqual(remote_refs, None)
 
     def test_push_commit(self):

+ 1 - 0
dulwich/file.py

@@ -189,6 +189,7 @@ class _GitFile(object):
         """
         if self._closed:
             return
+        self._file.flush()
         os.fsync(self._file.fileno())
         self._file.close()
         try:

+ 1 - 1
dulwich/graph.py

@@ -74,7 +74,7 @@ def _find_lcas(lookup_parents, c1, c2s):
                     cstates[pcmt] = flags
                 wlst.append(pcmt)
 
-    # walk final candidates removing any superceded by _DNC by later lower LCAs
+    # walk final candidates removing any superseded by _DNC by later lower LCAs
     results = []
     for cmt in cands:
         if not (cstates[cmt] & _DNC):

+ 3 - 1
dulwich/hooks.py

@@ -100,7 +100,9 @@ class ShellHook(Hook):
             args = self.pre_exec_callback(*args)
 
         try:
-            ret = subprocess.call([self.filepath] + list(args), cwd=self.cwd)
+            ret = subprocess.call(
+                [os.path.relpath(self.filepath, self.cwd)] + list(args),
+                cwd=self.cwd)
             if ret != 0:
                 if self.post_exec_callback is not None:
                     self.post_exec_callback(0, *args)

+ 2 - 2
dulwich/index.py

@@ -886,7 +886,7 @@ def index_entry_from_path(path, object_store=None):
 
     This returns an index value for files, symlinks
     and tree references. for directories and
-    non-existant files it returns None
+    non-existent files it returns None
 
     Args:
       path: Path to create an index entry for
@@ -929,7 +929,7 @@ def iter_fresh_entries(
 
 
 def iter_fresh_objects(paths, root_path, include_deleted=False, object_store=None):
-    """Iterate over versions of objecs on disk referenced by index.
+    """Iterate over versions of objects on disk referenced by index.
 
     Args:
       root_path: Root path to access from

+ 0 - 6
dulwich/object_store.py

@@ -1152,12 +1152,6 @@ class ObjectStoreIterator(ObjectIterator):
         """Return the number of objects."""
         return len(list(self.itershas()))
 
-    def empty(self):
-        import warnings
-
-        warnings.warn("Use bool() instead.", DeprecationWarning)
-        return self._empty()
-
     def _empty(self):
         it = self.itershas()
         try:

+ 56 - 8
dulwich/objects.py

@@ -34,7 +34,6 @@ from typing import (
     Union,
     Type,
 )
-import warnings
 import zlib
 from hashlib import sha1
 
@@ -1091,13 +1090,6 @@ class Tree(ShaFile):
           name: The name of the entry, as a string.
           hexsha: The hex SHA of the entry as a string.
         """
-        if isinstance(name, int) and isinstance(mode, bytes):
-            (name, mode) = (mode, name)
-            warnings.warn(
-                "Please use Tree.add(name, mode, hexsha)",
-                category=DeprecationWarning,
-                stacklevel=2,
-            )
         self._entries[name] = mode, hexsha
         self._needs_serialization = True
 
@@ -1428,6 +1420,62 @@ class Commit(ShaFile):
 
         # TODO: optionally check for duplicate parents
 
+    def sign(self, keyid: Optional[str] = None):
+        import gpg
+        with gpg.Context(armor=True) as c:
+            if keyid is not None:
+                key = c.get_key(keyid)
+                with gpg.Context(armor=True, signers=[key]) as ctx:
+                    self.gpgsig, unused_result = ctx.sign(
+                        self.as_raw_string(),
+                        mode=gpg.constants.sig.mode.DETACH,
+                    )
+            else:
+                self.gpgsig, unused_result = c.sign(
+                    self.as_raw_string(), mode=gpg.constants.sig.mode.DETACH
+                )
+
+    def verify(self, keyids: Optional[Iterable[str]] = None):
+        """Verify GPG signature for this commit (if it is signed).
+
+        Args:
+          keyids: Optional iterable of trusted keyids for this commit.
+            If this commit is not signed by any key in keyids verification will
+            fail. If not specified, this function only verifies that the commit
+            has a valid signature.
+
+        Raises:
+          gpg.errors.BadSignatures: if GPG signature verification fails
+          gpg.errors.MissingSignatures: if commit was not signed by a key
+            specified in keyids
+        """
+        if self._gpgsig is None:
+            return
+
+        import gpg
+
+        with gpg.Context() as ctx:
+            self_without_gpgsig = self.copy()
+            self_without_gpgsig._gpgsig = None
+            self_without_gpgsig.gpgsig = None
+            data, result = ctx.verify(
+                self_without_gpgsig.as_raw_string(),
+                signature=self._gpgsig,
+            )
+            if keyids:
+                keys = [
+                    ctx.get_key(key)
+                    for key in keyids
+                ]
+                for key in keys:
+                    for subkey in keys:
+                        for sig in result.signatures:
+                            if subkey.can_sign and subkey.fpr == sig.fpr:
+                                return
+                raise gpg.errors.MissingSignatures(
+                    result, keys, results=(data, result)
+                )
+
     def _serialize(self):
         chunks = []
         tree_bytes = self._tree.id if isinstance(self._tree, Tree) else self._tree

+ 1 - 1
dulwich/patch.py

@@ -202,7 +202,7 @@ def write_object_diff(f, store, old_file, new_file, diff_binary=False):
       diff_binary: Whether to diff files even if they
         are considered binary files by is_binary().
 
-    Note: the tuple elements should be None for nonexistant files
+    Note: the tuple elements should be None for nonexistent files
     """
     (old_path, old_mode, old_id) = old_file
     (new_path, new_mode, new_id) = new_file

+ 150 - 25
dulwich/porcelain.py

@@ -43,6 +43,7 @@ Currently implemented:
  * remote{_add}
  * receive-pack
  * reset
+ * submodule_list
  * rev-list
  * tag{_create,_delete,_list}
  * upload-pack
@@ -86,6 +87,7 @@ from dulwich.client import (
     get_transport_and_path,
 )
 from dulwich.config import (
+    ConfigFile,
     StackedConfig,
 )
 from dulwich.diff_tree import (
@@ -189,6 +191,78 @@ class RemoteExists(Error):
     """Raised when the remote already exists."""
 
 
+class TimezoneFormatError(Error):
+    """Raised when the timezone cannot be determined from a given string."""
+
+
+def parse_timezone_format(tz_str):
+    """Parse given string and attempt to return a timezone offset.
+    Different formats are considered in the following order:
+        - Git internal format: <unix timestamp> <timezone offset>
+        - RFC 2822: e.g. Mon, 20 Nov 1995 19:12:08 -0500
+        - ISO 8601: e.g. 1995-11-20T19:12:08-0500
+    Args:
+      tz_str: datetime string
+    Returns: Timezone offset as integer
+    Raises:
+      TimezoneFormatError: if timezone information cannot be extracted
+   """
+    import re
+
+    # Git internal format
+    internal_format_pattern = re.compile("^[0-9]+ [+-][0-9]{,4}$")
+    if re.match(internal_format_pattern, tz_str):
+        try:
+            tz_internal = parse_timezone(tz_str.split(" ")[1].encode(DEFAULT_ENCODING))
+            return tz_internal[0]
+        except ValueError:
+            pass
+
+    # RFC 2822
+    import email.utils
+    rfc_2822 = email.utils.parsedate_tz(tz_str)
+    if rfc_2822:
+        return rfc_2822[9]
+
+    # ISO 8601
+
+    # Supported offsets:
+    # sHHMM, sHH:MM, sHH
+    iso_8601_pattern = re.compile("[0-9] ?([+-])([0-9]{2})(?::(?=[0-9]{2}))?([0-9]{2})?$")
+    match = re.search(iso_8601_pattern, tz_str)
+    total_secs = 0
+    if match:
+        sign, hours, minutes = match.groups()
+        total_secs += int(hours) * 3600
+        if minutes:
+            total_secs += int(minutes) * 60
+        total_secs = -total_secs if sign == "-" else total_secs
+        return total_secs
+
+    # YYYY.MM.DD, MM/DD/YYYY, DD.MM.YYYY contain no timezone information
+
+    raise TimezoneFormatError(tz_str)
+
+
+def get_user_timezones():
+    """Retrieve local timezone as described in
+    https://raw.githubusercontent.com/git/git/v2.3.0/Documentation/date-formats.txt
+    Returns: A tuple containing author timezone, committer timezone
+    """
+    local_timezone = time.localtime().tm_gmtoff
+
+    if os.environ.get("GIT_AUTHOR_DATE"):
+        author_timezone = parse_timezone_format(os.environ["GIT_AUTHOR_DATE"])
+    else:
+        author_timezone = local_timezone
+    if os.environ.get("GIT_COMMITTER_DATE"):
+        commit_timezone = parse_timezone_format(os.environ["GIT_COMMITTER_DATE"])
+    else:
+        commit_timezone = local_timezone
+
+    return author_timezone, commit_timezone
+
+
 def open_repo(path_or_repo):
     """Open an argument that can be a repository or a path for a repository."""
     if isinstance(path_or_repo, BaseRepo):
@@ -327,9 +401,12 @@ def commit(
     repo=".",
     message=None,
     author=None,
+    author_timezone=None,
     committer=None,
+    commit_timezone=None,
     encoding=None,
     no_verify=False,
+    signoff=False,
 ):
     """Create a new commit.
 
@@ -337,25 +414,37 @@ def commit(
       repo: Path to repository
       message: Optional commit message
       author: Optional author name and email
+      author_timezone: Author timestamp timezone
       committer: Optional committer name and email
+      commit_timezone: Commit timestamp timezone
       no_verify: Skip pre-commit and commit-msg hooks
+      signoff: GPG Sign the commit (bool, defaults to False,
+        pass True to use default GPG key,
+        pass a str containing Key ID to use a specific GPG key)
     Returns: SHA1 of the new commit
     """
     # FIXME: Support --all argument
-    # FIXME: Support --signoff argument
     if getattr(message, "encode", None):
         message = message.encode(encoding or DEFAULT_ENCODING)
     if getattr(author, "encode", None):
         author = author.encode(encoding or DEFAULT_ENCODING)
     if getattr(committer, "encode", None):
         committer = committer.encode(encoding or DEFAULT_ENCODING)
+    local_timezone = get_user_timezones()
+    if author_timezone is None:
+        author_timezone = local_timezone[0]
+    if commit_timezone is None:
+        commit_timezone = local_timezone[1]
     with open_repo_closing(repo) as r:
         return r.do_commit(
             message=message,
             author=author,
+            author_timezone=author_timezone,
             committer=committer,
+            commit_timezone=commit_timezone,
             encoding=encoding,
             no_verify=no_verify,
+            sign=signoff if isinstance(signoff, (str, bool)) else None,
         )
 
 
@@ -401,6 +490,7 @@ def clone(
     origin="origin",
     depth=None,
     branch=None,
+    config=None,
     **kwargs
 ):
     """Clone a local or remote git repository.
@@ -416,6 +506,7 @@ def clone(
       depth: Depth to fetch at
       branch: Optional branch or tag to be used as HEAD in the new repository
         instead of the cloned repository's HEAD.
+      config: Configuration to use
     Returns: The new repository
     """
     if outstream is not None:
@@ -428,6 +519,9 @@ def clone(
         )
         # TODO(jelmer): Capture logging output and stream to errstream
 
+    if config is None:
+        config = StackedConfig.default()
+
     if checkout is None:
         checkout = not bare
     if checkout and bare:
@@ -438,7 +532,8 @@ def clone(
 
     mkdir = not os.path.exists(target)
 
-    (client, path) = get_transport_and_path(source, **kwargs)
+    (client, path) = get_transport_and_path(
+        source, config=config, **kwargs)
 
     return client.clone(
         path,
@@ -858,13 +953,49 @@ def rev_list(repo, commits, outstream=sys.stdout):
             outstream.write(entry.commit.id + b"\n")
 
 
-def tag(*args, **kwargs):
-    import warnings
+def _canonical_part(url: str) -> str:
+    name = url.rsplit('/', 1)[-1]
+    if name.endswith('.git'):
+        name = name[:-4]
+    return name
 
-    warnings.warn(
-        "tag has been deprecated in favour of tag_create.", DeprecationWarning
-    )
-    return tag_create(*args, **kwargs)
+
+def submodule_add(repo, url, path=None, name=None):
+    """Add a new submodule.
+
+    Args:
+      repo: Path to repository
+      url: URL of repository to add as submodule
+      path: Path where submodule should live
+    """
+    with open_repo_closing(repo) as r:
+        if path is None:
+            path = os.path.relpath(_canonical_part(url), r.path)
+        if name is None:
+            name = path
+
+        # TODO(jelmer): Move this logic to dulwich.submodule
+        gitmodules_path = os.path.join(r.path, ".gitmodules")
+        try:
+            config = ConfigFile.from_path(gitmodules_path)
+        except FileNotFoundError:
+            config = ConfigFile()
+            config.path = gitmodules_path
+        config.set(("submodule", name), "url", url)
+        config.set(("submodule", name), "path", path)
+        config.write_to_path()
+
+
+def submodule_list(repo):
+    """List submodules.
+
+    Args:
+      repo: Path to repository
+    """
+    from .submodule import iter_cached_submodules
+    with open_repo_closing(repo) as r:
+        for path, sha in iter_cached_submodules(r.object_store, r[r.head()].tree):
+            yield path.decode(DEFAULT_ENCODING), sha.decode(DEFAULT_ENCODING)
 
 
 def tag_create(
@@ -911,8 +1042,7 @@ def tag_create(
                 tag_time = int(time.time())
             tag_obj.tag_time = tag_time
             if tag_timezone is None:
-                # TODO(jelmer) Use current user timezone rather than UTC
-                tag_timezone = 0
+                tag_timezone = get_user_timezones()[1]
             elif isinstance(tag_timezone, str):
                 tag_timezone = parse_timezone(tag_timezone)
             tag_obj.tag_timezone = tag_timezone
@@ -927,16 +1057,6 @@ def tag_create(
         r.refs[_make_tag_ref(tag)] = tag_id
 
 
-def list_tags(*args, **kwargs):
-    import warnings
-
-    warnings.warn(
-        "list_tags has been deprecated in favour of tag_list.",
-        DeprecationWarning,
-    )
-    return tag_list(*args, **kwargs)
-
-
 def tag_list(repo, outstream=sys.stdout):
     """List all tags.
 
@@ -1164,10 +1284,10 @@ def status(repo=".", ignored=False, untracked_files="all"):
       untracked_files: How to handle untracked files, defaults to "all":
           "no": do not return untracked files
           "all": include all files in untracked directories
-        Using `untracked_files="no"` can be faster than "all" when the worktreee
+        Using untracked_files="no" can be faster than "all" when the worktreee
           contains many untracked files/directories.
 
-    Note: `untracked_files="normal" (`git`'s default) is not implemented.
+    Note: untracked_files="normal" (git's default) is not implemented.
 
     Returns: GitStatus tuple,
         staged -  dict with lists of staged paths (diff index/HEAD)
@@ -1190,7 +1310,12 @@ def status(repo=".", ignored=False, untracked_files="all"):
             exclude_ignored=not ignored,
             untracked_files=untracked_files,
         )
-        untracked_changes = list(untracked_paths)
+        if sys.platform == "win32":
+            untracked_changes = [
+                path.replace(os.path.sep, "/") for path in untracked_paths
+            ]
+        else:
+            untracked_changes = list(untracked_paths)
 
         return GitStatus(tracked_changes, unstaged_changes, untracked_changes)
 
@@ -1456,7 +1581,7 @@ def branch_create(repo, name, objectish=None, force=False):
             objectish = "HEAD"
         object = parse_object(r, objectish)
         refname = _make_branch_ref(name)
-        ref_message = b"branch: Created from " + objectish.encode("utf-8")
+        ref_message = b"branch: Created from " + objectish.encode(DEFAULT_ENCODING)
         if force:
             r.refs.set_if_equals(refname, None, object.id, message=ref_message)
         else:
@@ -1541,7 +1666,7 @@ def fetch(
     with open_repo_closing(repo) as r:
         (remote_name, remote_location) = get_remote_repo(r, remote_location)
         if message is None:
-            message = b"fetch: from " + remote_location.encode("utf-8")
+            message = b"fetch: from " + remote_location.encode(DEFAULT_ENCODING)
         client, path = get_transport_and_path(
             remote_location, config=r.get_config_stack(), **kwargs
         )

+ 2 - 0
dulwich/protocol.py

@@ -238,6 +238,8 @@ class Protocol(object):
             if self.report_activity:
                 self.report_activity(size, "read")
             pkt_contents = read(size - 4)
+        except ConnectionResetError:
+            raise HangupException()
         except socket.error as e:
             raise GitProtocolError(e)
         else:

+ 24 - 22
dulwich/refs.py

@@ -49,6 +49,14 @@ BAD_REF_CHARS = set(b"\177 ~^:?*[")
 ANNOTATED_TAG_SUFFIX = b"^{}"
 
 
+class SymrefLoop(Exception):
+    """There is a loop between one or more symrefs."""
+
+    def __init__(self, ref, depth):
+        self.ref = ref
+        self.depth = depth
+
+
 def parse_symref_value(contents):
     """Parse a symref value.
 
@@ -231,7 +239,7 @@ class RefsContainer(object):
         for key in keys:
             try:
                 ret[key] = self[(base + b"/" + key).strip(b"/")]
-            except KeyError:
+            except (SymrefLoop, KeyError):
                 continue  # Unable to resolve
 
         return ret
@@ -294,21 +302,9 @@ class RefsContainer(object):
                 break
             depth += 1
             if depth > 5:
-                raise KeyError(name)
+                raise SymrefLoop(name, depth)
         return refnames, contents
 
-    def _follow(self, name):
-        import warnings
-
-        warnings.warn(
-            "RefsContainer._follow is deprecated. Use RefsContainer.follow " "instead.",
-            DeprecationWarning,
-        )
-        refnames, contents = self.follow(name)
-        if not refnames:
-            return (None, contents)
-        return (refnames[-1], contents)
-
     def __contains__(self, refname):
         if self.read_ref(refname):
             return True
@@ -511,12 +507,12 @@ class DictRefsContainer(RefsContainer):
 
     def add_if_new(
         self,
-        name,
-        ref,
+        name: bytes,
+        ref: bytes,
         committer=None,
         timestamp=None,
         timezone=None,
-        message=None,
+        message: Optional[bytes] = None,
     ):
         if name in self._refs:
             return False
@@ -883,12 +879,12 @@ class DiskRefsContainer(RefsContainer):
 
     def add_if_new(
         self,
-        name,
-        ref,
+        name: bytes,
+        ref: bytes,
         committer=None,
         timestamp=None,
         timezone=None,
-        message=None,
+        message: Optional[bytes] = None,
     ):
         """Add a new reference only if it does not already exist.
 
@@ -1137,7 +1133,11 @@ def _set_origin_head(refs, origin, origin_head):
             refs.set_symbolic_ref(origin_ref, target_ref)
 
 
-def _set_default_branch(refs, origin, origin_head, branch, ref_message):
+def _set_default_branch(
+        refs: RefsContainer, origin: bytes, origin_head: bytes, branch: bytes,
+        ref_message: Optional[bytes]) -> bytes:
+    """Set the default branch.
+    """
     origin_base = b"refs/remotes/" + origin + b"/"
     if branch:
         origin_ref = origin_base + branch
@@ -1151,7 +1151,7 @@ def _set_default_branch(refs, origin, origin_head, branch, ref_message):
             head_ref = LOCAL_TAG_PREFIX + branch
         else:
             raise ValueError(
-                "%s is not a valid branch or tag" % os.fsencode(branch)
+                "%r is not a valid branch or tag" % os.fsencode(branch)
             )
     elif origin_head:
         head_ref = origin_head
@@ -1165,6 +1165,8 @@ def _set_default_branch(refs, origin, origin_head, branch, ref_message):
             )
         except KeyError:
             pass
+    else:
+        raise ValueError('neither origin_head nor branch are provided')
     return head_ref
 
 

+ 26 - 11
dulwich/repo.py

@@ -303,7 +303,7 @@ def _set_filesystem_hidden(path):
         if not SetFileAttributesW(path, FILE_ATTRIBUTE_HIDDEN):
             pass  # Could raise or log `ctypes.WinError()` here
 
-    # Could implement other platform specific filesytem hiding here
+    # Could implement other platform specific filesystem hiding here
 
 
 class ParentsProvider(object):
@@ -399,7 +399,7 @@ class BaseRepo(object):
         raise NotImplementedError(self._put_named_file)
 
     def _del_named_file(self, path):
-        """Delete a file in the contrl directory with the given name."""
+        """Delete a file in the control directory with the given name."""
         raise NotImplementedError(self._del_named_file)
 
     def open_index(self):
@@ -876,6 +876,7 @@ class BaseRepo(object):
         ref=b"HEAD",
         merge_heads=None,
         no_verify=False,
+        sign=False,
     ):
         """Create a new commit.
 
@@ -899,6 +900,9 @@ class BaseRepo(object):
           ref: Optional ref to commit to (defaults to current branch)
           merge_heads: Merge heads (defaults to .git/MERGE_HEAD)
           no_verify: Skip pre-commit and commit-msg hooks
+          sign: GPG Sign the commit (bool, defaults to False,
+            pass True to use default GPG key,
+            pass a str containing Key ID to use a specific GPG key)
 
         Returns:
           New commit SHA1
@@ -970,14 +974,20 @@ class BaseRepo(object):
         except KeyError:  # no hook defined, message not modified
             c.message = message
 
+        keyid = sign if isinstance(sign, str) else None
+
         if ref is None:
             # Create a dangling commit
             c.parents = merge_heads
+            if sign:
+                c.sign(keyid)
             self.object_store.add_object(c)
         else:
             try:
                 old_head = self.refs[ref]
                 c.parents = [old_head] + merge_heads
+                if sign:
+                    c.sign(keyid)
                 self.object_store.add_object(c)
                 ok = self.refs.set_if_equals(
                     ref,
@@ -990,6 +1000,8 @@ class BaseRepo(object):
                 )
             except KeyError:
                 c.parents = merge_heads
+                if sign:
+                    c.sign(keyid)
                 self.object_store.add_object(c)
                 ok = self.refs.add_if_new(
                     ref,
@@ -1041,7 +1053,7 @@ class UnsupportedVersion(Exception):
 class Repo(BaseRepo):
     """A git repository backed by local disk.
 
-    To open an existing repository, call the contructor with
+    To open an existing repository, call the constructor with
     the path of the repository.
 
     To create a new repository, use the Repo.init class method.
@@ -1052,11 +1064,14 @@ class Repo(BaseRepo):
 
     Attributes:
 
-      path (str): Path to the working copy (if it exists) or repository control
+      path: Path to the working copy (if it exists) or repository control
         directory (if the repository is bare)
-      bare (bool): Whether this is a bare repository
+      bare: Whether this is a bare repository
     """
 
+    path: str
+    bare: bool
+
     def __init__(
         self,
         root: str,
@@ -1065,11 +1080,11 @@ class Repo(BaseRepo):
     ) -> None:
         hidden_path = os.path.join(root, CONTROLDIR)
         if bare is None:
-            if (os.path.isfile(hidden_path) or
-                    os.path.isdir(os.path.join(hidden_path, OBJECTDIR))):
+            if (os.path.isfile(hidden_path)
+                    or os.path.isdir(os.path.join(hidden_path, OBJECTDIR))):
                 bare = False
-            elif (os.path.isdir(os.path.join(root, OBJECTDIR)) and
-                    os.path.isdir(os.path.join(root, REFSDIR))):
+            elif (os.path.isdir(os.path.join(root, OBJECTDIR))
+                    and os.path.isdir(os.path.join(root, REFSDIR))):
                 bare = True
             else:
                 raise NotGitRepository(
@@ -1356,7 +1371,7 @@ class Repo(BaseRepo):
         from dulwich.index import (
             IndexEntry,
             _fs_to_tree_path,
-            )
+        )
 
         index = self.open_index()
         try:
@@ -1375,7 +1390,7 @@ class Repo(BaseRepo):
                 tree_entry = self.object_store[tree_id].lookup_path(
                     self.object_store.__getitem__, tree_path)
             except KeyError:
-                # if tree_entry didnt exist, this file was being added, so
+                # if tree_entry didn't exist, this file was being added, so
                 # remove index entry
                 try:
                     del index[tree_path]

+ 1 - 1
dulwich/server.py

@@ -1068,7 +1068,7 @@ class ReceivePackHandler(PackHandler):
         client_refs = []
         ref = self.proto.read_pkt_line()
 
-        # if ref is none then client doesnt want to send us anything..
+        # if ref is none then client doesn't want to send us anything..
         if ref is None:
             return
 

+ 40 - 0
dulwich/submodule.py

@@ -0,0 +1,40 @@
+# config.py - Reading and writing Git config files
+# Copyright (C) 2011-2013 Jelmer Vernooij <jelmer@jelmer.uk>
+#
+# Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
+# General Public License as public by the Free Software Foundation; version 2.0
+# or (at your option) any later version. You can redistribute it and/or
+# modify it under the terms of either of these two licenses.
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# You should have received a copy of the licenses; if not, see
+# <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
+# and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
+# License, Version 2.0.
+#
+
+"""Working with Git submodules.
+"""
+
+from typing import Iterator, Tuple
+from .objects import S_ISGITLINK
+
+
+def iter_cached_submodules(store, root_tree_id: bytes) -> Iterator[Tuple[str, bytes]]:
+    """iterate over cached submodules.
+
+    Args:
+      store: Object store to iterate
+      root_tree_id: SHA of root tree
+
+    Returns:
+      Iterator over over (path, sha) tuples
+    """
+    for entry in store.iter_tree_contents(root_tree_id):
+        if S_ISGITLINK(entry.mode):
+            yield entry.path, entry.sha

+ 1 - 1
dulwich/tests/__init__.py

@@ -50,7 +50,7 @@ class TestCase(_TestCase):
     def setUp(self):
         super(TestCase, self).setUp()
         self._old_home = os.environ.get("HOME")
-        os.environ["HOME"] = "/nonexistant"
+        os.environ["HOME"] = "/nonexistent"
         os.environ["GIT_CONFIG_NOSYSTEM"] = "1"
 
     def tearDown(self):

+ 7 - 2
dulwich/tests/compat/test_client.py

@@ -1,4 +1,4 @@
-# test_client.py -- Compatibilty tests for git client.
+# test_client.py -- Compatibility tests for git client.
 # Copyright (C) 2010 Google, Inc.
 #
 # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
@@ -18,7 +18,7 @@
 # License, Version 2.0.
 #
 
-"""Compatibilty tests between the Dulwich client and the cgit server."""
+"""Compatibility tests between the Dulwich client and the cgit server."""
 
 import copy
 from io import BytesIO
@@ -429,6 +429,11 @@ class DulwichTCPClientTest(CompatTestCase, DulwichClientTestBase):
         def test_fetch_pack_no_side_band_64k(self):
             DulwichClientTestBase.test_fetch_pack_no_side_band_64k(self)
 
+    def test_send_remove_branch(self):
+        # This test fails intermittently on my machine, probably due to some sort
+        # of race condition. Probably also related to #1015
+        self.skipTest('skip flaky test; see #1015')
+
 
 class TestSSHVendor(object):
     @staticmethod

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

@@ -94,7 +94,7 @@ class TagCreateSignTestCase(PorcelainGpgTestCase, CompatTestCase):
                 'GNUPGHOME': os.environ['GNUPGHOME'],
                 'GIT_COMMITTER_NAME': 'Joe Example',
                 'GIT_COMMITTER_EMAIL': 'joe@example.com',
-                },
+            },
         )
         tag = self.repo[b"refs/tags/verifyme"]
         self.assertNotEqual(tag.signature, None)

+ 5 - 2
dulwich/tests/compat/utils.py

@@ -43,7 +43,9 @@ from dulwich.tests import (
 _DEFAULT_GIT = "git"
 _VERSION_LEN = 4
 _REPOS_DATA_DIR = os.path.abspath(
-    os.path.join(os.path.dirname(__file__), os.pardir, "data", "repos")
+    os.path.join(
+        os.path.dirname(__file__), os.pardir, os.pardir, os.pardir,
+        "testdata", "repos")
 )
 
 
@@ -82,7 +84,7 @@ def require_git_version(required_version, git_path=_DEFAULT_GIT):
 
     Args:
       required_version: A tuple of ints of the form (major, minor, point,
-        sub-point); ommitted components default to 0.
+        sub-point); omitted components default to 0.
       git_path: Path to the git executable; defaults to the version in
         the system path.
     Raises:
@@ -137,6 +139,7 @@ def run_git(
 
     env = popen_kwargs.pop("env", {})
     env["LC_ALL"] = env["LANG"] = "C"
+    env["PATH"] = os.getenv("PATH")
 
     args = [git_path] + args
     popen_kwargs["stdin"] = subprocess.PIPE

+ 0 - 29
dulwich/tests/test_client.py

@@ -52,7 +52,6 @@ from dulwich.client import (
     PLinkSSHVendor,
     HangupException,
     GitProtocolError,
-    apply_instead_of,
     check_wants,
     default_urllib3_manager,
     get_credentials_from_store,
@@ -1616,31 +1615,3 @@ And this line is just random noise, too.
                 ]
             ),
         )
-
-
-class ApplyInsteadOfTests(TestCase):
-    def test_none(self):
-        config = ConfigDict()
-        self.assertEqual(
-            'https://example.com/', apply_instead_of(config, 'https://example.com/'))
-
-    def test_apply(self):
-        config = ConfigDict()
-        config.set(
-            ('url', 'https://samba.org/'), 'insteadOf', 'https://example.com/')
-        self.assertEqual(
-            'https://samba.org/',
-            apply_instead_of(config, 'https://example.com/'))
-
-    def test_apply_multiple(self):
-        config = ConfigDict()
-        config.set(
-            ('url', 'https://samba.org/'), 'insteadOf', 'https://blah.com/')
-        config.set(
-            ('url', 'https://samba.org/'), 'insteadOf', 'https://example.com/')
-        self.assertEqual(
-            [b'https://blah.com/', b'https://example.com/'],
-            list(config.get_multivar(('url', 'https://samba.org/'), 'insteadOf')))
-        self.assertEqual(
-            'https://samba.org/',
-            apply_instead_of(config, 'https://example.com/'))

+ 35 - 2
dulwich/tests/test_config.py

@@ -36,6 +36,7 @@ from dulwich.config import (
     _escape_value,
     _parse_string,
     parse_submodules,
+    apply_instead_of,
 )
 from dulwich.tests import (
     TestCase,
@@ -103,6 +104,10 @@ class ConfigFileTests(TestCase):
         cf = self.from_file(b'[branch "foo#bar"] # a comment\nbar= foo\n')
         self.assertEqual(ConfigFile({(b"branch", b"foo#bar"): {b"bar": b"foo"}}), cf)
 
+    def test_closing_bracket_within_section_string(self):
+        cf = self.from_file(b'[branch "foo]bar"] # a comment\nbar= foo\n')
+        self.assertEqual(ConfigFile({(b"branch", b"foo]bar"): {b"bar": b"foo"}}), cf)
+
     def test_from_file_section(self):
         cf = self.from_file(b"[core]\nfoo = bar\n")
         self.assertEqual(b"bar", cf.get((b"core",), b"foo"))
@@ -300,7 +305,7 @@ class StackedConfigTests(TestCase):
     def test_default_backends(self):
         StackedConfig.default_backends()
 
-    @skipIf(sys.platform != "win32", "Windows specfic config location.")
+    @skipIf(sys.platform != "win32", "Windows specific config location.")
     def test_windows_config_from_path(self):
         from dulwich.config import get_win_system_paths
 
@@ -316,7 +321,7 @@ class StackedConfigTests(TestCase):
             paths,
         )
 
-    @skipIf(sys.platform != "win32", "Windows specfic config location.")
+    @skipIf(sys.platform != "win32", "Windows specific config location.")
     def test_windows_config_from_reg(self):
         import winreg
 
@@ -428,3 +433,31 @@ class SubmodulesTests(TestCase):
             ],
             got,
         )
+
+
+class ApplyInsteadOfTests(TestCase):
+    def test_none(self):
+        config = ConfigDict()
+        self.assertEqual(
+            'https://example.com/', apply_instead_of(config, 'https://example.com/'))
+
+    def test_apply(self):
+        config = ConfigDict()
+        config.set(
+            ('url', 'https://samba.org/'), 'insteadOf', 'https://example.com/')
+        self.assertEqual(
+            'https://samba.org/',
+            apply_instead_of(config, 'https://example.com/'))
+
+    def test_apply_multiple(self):
+        config = ConfigDict()
+        config.set(
+            ('url', 'https://samba.org/'), 'insteadOf', 'https://blah.com/')
+        config.set(
+            ('url', 'https://samba.org/'), 'insteadOf', 'https://example.com/')
+        self.assertEqual(
+            [b'https://blah.com/', b'https://example.com/'],
+            list(config.get_multivar(('url', 'https://samba.org/'), 'insteadOf')))
+        self.assertEqual(
+            'https://samba.org/',
+            apply_instead_of(config, 'https://example.com/'))

+ 1 - 1
dulwich/tests/test_greenthreads.py

@@ -130,6 +130,6 @@ class TestGreenThreadsMissingObjectFinder(TestCase):
         finder = GreenThreadsMissingObjectFinder(
             self.store, wants[0 : int(self.cmt_amount / 2)], wants
         )
-        # sha_done will contains commit id and sha of blob refered in tree
+        # sha_done will contains commit id and sha of blob referred in tree
         self.assertEqual(len(finder.sha_done), (self.cmt_amount / 2) * 2)
         self.assertEqual(len(finder.objects_to_send), self.cmt_amount / 2)

+ 1 - 1
dulwich/tests/test_index.py

@@ -86,7 +86,7 @@ def can_symlink():
 
 class IndexTestCase(TestCase):
 
-    datadir = os.path.join(os.path.dirname(__file__), "data/indexes")
+    datadir = os.path.join(os.path.dirname(__file__), "../../testdata/indexes")
 
     def get_simple_index(self, name):
         return Index(os.path.join(self.datadir, name))

+ 2 - 14
dulwich/tests/test_objects.py

@@ -30,7 +30,6 @@ from itertools import (
 )
 import os
 import stat
-import warnings
 from contextlib import contextmanager
 
 from dulwich.errors import (
@@ -87,7 +86,7 @@ class BlobReadTests(TestCase):
     """Test decompression of blobs"""
 
     def get_sha_file(self, cls, base, sha):
-        dir = os.path.join(os.path.dirname(__file__), "data", base)
+        dir = os.path.join(os.path.dirname(__file__), "..", "..", "testdata", base)
         return cls.from_path(hex_to_filename(dir, sha))
 
     def get_blob(self, sha):
@@ -840,17 +839,6 @@ class TreeTests(ShaFileCheckTests):
         self.assertEqual(x[b"myname"], (0o100755, myhexsha))
         self.assertEqual(b"100755 myname\0" + hex_to_sha(myhexsha), x.as_raw_string())
 
-    def test_add_old_order(self):
-        myhexsha = b"d80c186a03f423a81b39df39dc87fd269736ca86"
-        x = Tree()
-        warnings.simplefilter("ignore", DeprecationWarning)
-        try:
-            x.add(0o100755, b"myname", myhexsha)
-        finally:
-            warnings.resetwarnings()
-        self.assertEqual(x[b"myname"], (0o100755, myhexsha))
-        self.assertEqual(b"100755 myname\0" + hex_to_sha(myhexsha), x.as_raw_string())
-
     def test_simple(self):
         myhexsha = b"d80c186a03f423a81b39df39dc87fd269736ca86"
         x = Tree()
@@ -878,7 +866,7 @@ class TreeTests(ShaFileCheckTests):
         self.assertEqual(_SORTED_TREE_ITEMS, x.items())
 
     def _do_test_parse_tree(self, parse_tree):
-        dir = os.path.join(os.path.dirname(__file__), "data", "trees")
+        dir = os.path.join(os.path.dirname(__file__), "..", "..", "testdata", "trees")
         o = Tree.from_path(hex_to_filename(dir, tree_sha))
         self.assertEqual(
             [(b"a", 0o100644, a_sha), (b"b", 0o100644, b_sha)],

+ 1 - 1
dulwich/tests/test_pack.py

@@ -95,7 +95,7 @@ class PackTests(TestCase):
         self.tempdir = tempfile.mkdtemp()
         self.addCleanup(shutil.rmtree, self.tempdir)
 
-    datadir = os.path.abspath(os.path.join(os.path.dirname(__file__), "data/packs"))
+    datadir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../testdata/packs"))
 
     def get_pack_index(self, sha):
         """Returns a PackIndex from the datadir with the given sha"""

+ 232 - 4
dulwich/tests/test_porcelain.py

@@ -66,6 +66,11 @@ from dulwich.web import (
     make_wsgi_chain,
 )
 
+try:
+    import gpg
+except ImportError:
+    gpg = None
+
 
 def flat_walk_dir(dir_to_walk):
     for dirpath, _, filenames in os.walk(dir_to_walk):
@@ -94,6 +99,7 @@ class PorcelainTestCase(TestCase):
         self.assertLess(time.time() - ts, 50)
 
 
+@skipIf(gpg is None, "gpg is not available")
 class PorcelainGpgTestCase(PorcelainTestCase):
     DEFAULT_KEY = """
 -----BEGIN PGP PRIVATE KEY BLOCK-----
@@ -271,7 +277,10 @@ ya6JVZCRbMXfdCy8lVPgtNQ6VlHaj8Wvnn2FLbWWO2n2r3s=
         super(PorcelainGpgTestCase, self).setUp()
         self.gpg_dir = os.path.join(self.test_dir, "gpg")
         os.mkdir(self.gpg_dir, mode=0o700)
-        self.addCleanup(shutil.rmtree, self.gpg_dir)
+        # Ignore errors when deleting GNUPGHOME, because of race conditions
+        # (e.g. the gpg-agent socket having been deleted). See
+        # https://github.com/jelmer/dulwich/issues/1000
+        self.addCleanup(shutil.rmtree, self.gpg_dir, ignore_errors=True)
         self._old_gnupghome = os.environ.get("GNUPGHOME")
         os.environ["GNUPGHOME"] = self.gpg_dir
         if self._old_gnupghome is None:
@@ -412,6 +421,195 @@ class CommitTests(PorcelainTestCase):
         self.assertIsInstance(sha, bytes)
         self.assertEqual(len(sha), 40)
 
+    def test_timezone(self):
+        c1, c2, c3 = build_commit_graph(
+            self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
+        )
+        self.repo.refs[b"refs/heads/foo"] = c3.id
+        sha = porcelain.commit(
+            self.repo.path,
+            message="Some message",
+            author="Joe <joe@example.com>",
+            author_timezone=18000,
+            committer="Bob <bob@example.com>",
+            commit_timezone=18000,
+        )
+        self.assertIsInstance(sha, bytes)
+        self.assertEqual(len(sha), 40)
+
+        commit = self.repo.get_object(sha)
+        self.assertEqual(commit._author_timezone, 18000)
+        self.assertEqual(commit._commit_timezone, 18000)
+
+        os.environ["GIT_AUTHOR_DATE"] = os.environ["GIT_COMMITTER_DATE"] = "1995-11-20T19:12:08-0501"
+
+        sha = porcelain.commit(
+            self.repo.path,
+            message="Some message",
+            author="Joe <joe@example.com>",
+            committer="Bob <bob@example.com>",
+        )
+        self.assertIsInstance(sha, bytes)
+        self.assertEqual(len(sha), 40)
+
+        commit = self.repo.get_object(sha)
+        self.assertEqual(commit._author_timezone, -18060)
+        self.assertEqual(commit._commit_timezone, -18060)
+
+        del os.environ["GIT_AUTHOR_DATE"]
+        del os.environ["GIT_COMMITTER_DATE"]
+        local_timezone = time.localtime().tm_gmtoff
+
+        sha = porcelain.commit(
+            self.repo.path,
+            message="Some message",
+            author="Joe <joe@example.com>",
+            committer="Bob <bob@example.com>",
+        )
+        self.assertIsInstance(sha, bytes)
+        self.assertEqual(len(sha), 40)
+
+        commit = self.repo.get_object(sha)
+        self.assertEqual(commit._author_timezone, local_timezone)
+        self.assertEqual(commit._commit_timezone, local_timezone)
+
+
+@skipIf(platform.python_implementation() == "PyPy" or sys.platform == "win32", "gpgme not easily available or supported on Windows and PyPy")
+class CommitSignTests(PorcelainGpgTestCase):
+
+    def test_default_key(self):
+        c1, c2, c3 = build_commit_graph(
+            self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
+        )
+        self.repo.refs[b"HEAD"] = c3.id
+        cfg = self.repo.get_config()
+        cfg.set(("user",), "signingKey", PorcelainGpgTestCase.DEFAULT_KEY_ID)
+        self.import_default_key()
+
+        sha = porcelain.commit(
+            self.repo.path,
+            message="Some message",
+            author="Joe <joe@example.com>",
+            committer="Bob <bob@example.com>",
+            signoff=True,
+        )
+        self.assertIsInstance(sha, bytes)
+        self.assertEqual(len(sha), 40)
+
+        commit = self.repo.get_object(sha)
+        # GPG Signatures aren't deterministic, so we can't do a static assertion.
+        commit.verify()
+        commit.verify(keyids=[PorcelainGpgTestCase.DEFAULT_KEY_ID])
+
+        self.import_non_default_key()
+        self.assertRaises(
+            gpg.errors.MissingSignatures,
+            commit.verify,
+            keyids=[PorcelainGpgTestCase.NON_DEFAULT_KEY_ID],
+        )
+
+        commit.committer = b"Alice <alice@example.com>"
+        self.assertRaises(
+            gpg.errors.BadSignatures,
+            commit.verify,
+        )
+
+    def test_non_default_key(self):
+        c1, c2, c3 = build_commit_graph(
+            self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
+        )
+        self.repo.refs[b"HEAD"] = c3.id
+        cfg = self.repo.get_config()
+        cfg.set(("user",), "signingKey", PorcelainGpgTestCase.DEFAULT_KEY_ID)
+        self.import_non_default_key()
+
+        sha = porcelain.commit(
+            self.repo.path,
+            message="Some message",
+            author="Joe <joe@example.com>",
+            committer="Bob <bob@example.com>",
+            signoff=PorcelainGpgTestCase.NON_DEFAULT_KEY_ID,
+        )
+        self.assertIsInstance(sha, bytes)
+        self.assertEqual(len(sha), 40)
+
+        commit = self.repo.get_object(sha)
+        # GPG Signatures aren't deterministic, so we can't do a static assertion.
+        commit.verify()
+
+
+class TimezoneTests(PorcelainTestCase):
+
+    def put_envs(self, value):
+        os.environ["GIT_AUTHOR_DATE"] = os.environ["GIT_COMMITTER_DATE"] = value
+
+    def fallback(self, value):
+        self.put_envs(value)
+        self.assertRaises(porcelain.TimezoneFormatError, porcelain.get_user_timezones)
+
+    def test_internal_format(self):
+        self.put_envs("0 +0500")
+        self.assertTupleEqual((18000, 18000), porcelain.get_user_timezones())
+
+    def test_rfc_2822(self):
+        self.put_envs("Mon, 20 Nov 1995 19:12:08 -0500")
+        self.assertTupleEqual((-18000, -18000), porcelain.get_user_timezones())
+
+        self.put_envs("Mon, 20 Nov 1995 19:12:08")
+        self.assertTupleEqual((0, 0), porcelain.get_user_timezones())
+
+    def test_iso8601(self):
+        self.put_envs("1995-11-20T19:12:08-0501")
+        self.assertTupleEqual((-18060, -18060), porcelain.get_user_timezones())
+
+        self.put_envs("1995-11-20T19:12:08+0501")
+        self.assertTupleEqual((18060, 18060), porcelain.get_user_timezones())
+
+        self.put_envs("1995-11-20T19:12:08-05:01")
+        self.assertTupleEqual((-18060, -18060), porcelain.get_user_timezones())
+
+        self.put_envs("1995-11-20 19:12:08-05")
+        self.assertTupleEqual((-18000, -18000), porcelain.get_user_timezones())
+
+        # https://github.com/git/git/blob/96b2d4fa927c5055adc5b1d08f10a5d7352e2989/t/t6300-for-each-ref.sh#L128
+        self.put_envs("2006-07-03 17:18:44 +0200")
+        self.assertTupleEqual((7200, 7200), porcelain.get_user_timezones())
+
+    def test_missing_or_malformed(self):
+        # TODO: add more here
+        self.fallback("0 + 0500")
+        self.fallback("a +0500")
+
+        self.fallback("1995-11-20T19:12:08")
+        self.fallback("1995-11-20T19:12:08-05:")
+
+        self.fallback("1995.11.20")
+        self.fallback("11/20/1995")
+        self.fallback("20.11.1995")
+
+    def test_different_envs(self):
+        os.environ["GIT_AUTHOR_DATE"] = "0 +0500"
+        os.environ["GIT_COMMITTER_DATE"] = "0 +0501"
+        self.assertTupleEqual((18000, 18060), porcelain.get_user_timezones())
+
+    def test_no_envs(self):
+        local_timezone = time.localtime().tm_gmtoff
+
+        self.put_envs("0 +0500")
+        self.assertTupleEqual((18000, 18000), porcelain.get_user_timezones())
+
+        del os.environ["GIT_COMMITTER_DATE"]
+        self.assertTupleEqual((18000, local_timezone), porcelain.get_user_timezones())
+
+        self.put_envs("0 +0500")
+        del os.environ["GIT_AUTHOR_DATE"]
+        self.assertTupleEqual((local_timezone, 18000), porcelain.get_user_timezones())
+
+        self.put_envs("0 +0500")
+        del os.environ["GIT_AUTHOR_DATE"]
+        del os.environ["GIT_COMMITTER_DATE"]
+        self.assertTupleEqual((local_timezone, local_timezone), porcelain.get_user_timezones())
+
 
 class CleanTests(PorcelainTestCase):
     def put_files(self, tracked, ignored, untracked, empty_dirs):
@@ -647,7 +845,7 @@ class CloneTests(PorcelainTestCase):
         with tempfile.TemporaryDirectory() as parent:
             target_path = os.path.join(parent, "target")
             self.assertRaises(
-                Exception, porcelain.clone, "/nonexistant/repo", target_path
+                Exception, porcelain.clone, "/nonexistent/repo", target_path
             )
             self.assertFalse(os.path.exists(target_path))
 
@@ -1128,8 +1326,6 @@ class RevListTests(PorcelainTestCase):
 class TagCreateSignTests(PorcelainGpgTestCase):
 
     def test_default_key(self):
-        import gpg
-
         c1, c2, c3 = build_commit_graph(
             self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
         )
@@ -1408,6 +1604,28 @@ class ResetFileTests(PorcelainTestCase):
             self.assertEqual('hello', f.read())
 
 
+class SubmoduleTests(PorcelainTestCase):
+
+    def test_empty(self):
+        porcelain.commit(
+            repo=self.repo.path,
+            message=b"init",
+            author=b"author <email>",
+            committer=b"committer <email>",
+        )
+
+        self.assertEqual([], list(porcelain.submodule_list(self.repo)))
+
+    def test_add(self):
+        porcelain.submodule_add(self.repo, "../bar.git", "bar")
+        with open('%s/.gitmodules' % self.repo.path, 'r') as f:
+            self.assertEqual("""\
+[submodule "bar"]
+\turl = ../bar.git
+\tpath = bar
+""", f.read())
+
+
 class PushTests(PorcelainTestCase):
     def test_simple(self):
         """
@@ -1897,6 +2115,16 @@ class StatusTests(PorcelainTestCase):
         with self.assertRaises(ValueError):
             porcelain.status(self.repo.path, untracked_files="antani")
 
+    def test_status_untracked_path(self):
+        untracked_dir = os.path.join(self.repo_path, "untracked_dir")
+        os.mkdir(untracked_dir)
+        untracked_file = os.path.join(untracked_dir, "untracked_file")
+        with open(untracked_file, "w") as fh:
+            fh.write("untracked")
+
+        _, _, untracked = porcelain.status(self.repo.path, untracked_files="all")
+        self.assertEqual(untracked, ["untracked_dir/untracked_file"])
+
     def test_status_crlf_mismatch(self):
         # First make a commit as if the file has been added on a Linux system
         # or with core.autocrlf=True

+ 5 - 4
dulwich/tests/test_refs.py

@@ -34,6 +34,7 @@ from dulwich.objects import ZERO_SHA
 from dulwich.refs import (
     DictRefsContainer,
     InfoRefsContainer,
+    SymrefLoop,
     check_ref_format,
     _split_ref_line,
     parse_symref_value,
@@ -246,9 +247,9 @@ class RefsContainerTests(object):
         self.assertEqual(nines, self._refs[b"refs/heads/master"])
 
         self.assertTrue(
-            self._refs.set_if_equals(b"refs/heads/nonexistant", ZERO_SHA, nines)
+            self._refs.set_if_equals(b"refs/heads/nonexistent", ZERO_SHA, nines)
         )
-        self.assertEqual(nines, self._refs[b"refs/heads/nonexistant"])
+        self.assertEqual(nines, self._refs[b"refs/heads/nonexistent"])
 
     def test_add_if_new(self):
         nines = b"9" * 40
@@ -518,7 +519,7 @@ class DiskRefsContainerTests(RefsContainerTests, TestCase):
             ),
             self._refs.follow(b"refs/heads/master"),
         )
-        self.assertRaises(KeyError, self._refs.follow, b"refs/heads/loop")
+        self.assertRaises(SymrefLoop, self._refs.follow, b"refs/heads/loop")
 
     def test_delitem(self):
         RefsContainerTests.test_delitem(self)
@@ -622,7 +623,7 @@ class DiskRefsContainerTests(RefsContainerTests, TestCase):
             b"42d06bd4b77fed026b154d16493e5deab78f02ec",
             self._refs.read_ref(b"refs/heads/packed"),
         )
-        self.assertEqual(None, self._refs.read_ref(b"nonexistant"))
+        self.assertEqual(None, self._refs.read_ref(b"nonexistent"))
 
     def test_read_loose_ref(self):
         self._refs[b"refs/heads/foo"] = b"df6800012397fb85c56e7418dd4eb9405dee075c"

+ 1 - 1
dulwich/tests/test_repository.py

@@ -409,7 +409,7 @@ class RepositoryRootTests(TestCase):
     def test_clone_no_head(self):
         temp_dir = self.mkdtemp()
         self.addCleanup(shutil.rmtree, temp_dir)
-        repo_dir = os.path.join(os.path.dirname(__file__), "data", "repos")
+        repo_dir = os.path.join(os.path.dirname(__file__), "..", "..", "testdata", "repos")
         dest_dir = os.path.join(temp_dir, "a.git")
         shutil.copytree(os.path.join(repo_dir, "a.git"), dest_dir, symlinks=True)
         r = Repo(dest_dir)

+ 1 - 1
dulwich/tests/utils.py

@@ -76,7 +76,7 @@ def open_repo(name, temp_dir=None):
     """
     if temp_dir is None:
         temp_dir = tempfile.mkdtemp()
-    repo_dir = os.path.join(os.path.dirname(__file__), "data", "repos", name)
+    repo_dir = os.path.join(os.path.dirname(__file__), "..", "..", "testdata", "repos", name)
     temp_repo_dir = os.path.join(temp_dir, name)
     shutil.copytree(repo_dir, temp_repo_dir, symlinks=True)
     return Repo(temp_repo_dir)

+ 1 - 1
requirements.txt

@@ -1 +1 @@
-urllib3[secure]>=1.23
+urllib3>=1.23

+ 24 - 38
setup.py

@@ -1,20 +1,12 @@
 #!/usr/bin/python3
 # encoding: utf-8
 # Setup file for dulwich
-# Copyright (C) 2008-2016 Jelmer Vernooij <jelmer@jelmer.uk>
-
-try:
-    from setuptools import setup, Extension
-except ImportError:
-    from distutils.core import setup, Extension
-    has_setuptools = False
-else:
-    has_setuptools = True
-from distutils.core import Distribution
+# Copyright (C) 2008-2022 Jelmer Vernooij <jelmer@jelmer.uk>
+
+from setuptools import setup, Extension, Distribution
 import io
 import os
 import sys
-from typing import Dict, Any
 
 
 if sys.version_info < (3, 6):
@@ -23,7 +15,7 @@ if sys.version_info < (3, 6):
         'For 2.7 support, please install a version prior to 0.20')
 
 
-dulwich_version_string = '0.20.44'
+dulwich_version_string = '0.20.46'
 
 
 class DulwichDistribution(Distribution):
@@ -53,8 +45,8 @@ if sys.platform == 'darwin' and os.path.exists('/usr/bin/xcodebuild'):
     for line in out.splitlines():
         line = line.decode("utf8")
         # Also parse only first digit, because 3.2.1 can't be parsed nicely
-        if (line.startswith('Xcode') and
-                int(line.split()[1].split('.')[0]) >= 4):
+        if (line.startswith('Xcode')
+                and int(line.split()[1].split('.')[0]) >= 4):
             os.environ['ARCHFLAGS'] = ''
 
 tests_require = ['fastimport']
@@ -71,26 +63,7 @@ ext_modules = [
     Extension('dulwich._diff_tree', ['dulwich/_diff_tree.c']),
 ]
 
-setup_kwargs = {}  # type: Dict[str, Any]
 scripts = ['bin/dul-receive-pack', 'bin/dul-upload-pack']
-if has_setuptools:
-    setup_kwargs['extras_require'] = {
-        'fastimport': ['fastimport'],
-        'https': ['urllib3[secure]>=1.24.1'],
-        'pgp': ['gpg'],
-        'paramiko': ['paramiko'],
-        }
-    setup_kwargs['install_requires'] = ['urllib3>=1.24.1', 'certifi']
-    setup_kwargs['include_package_data'] = True
-    setup_kwargs['test_suite'] = 'dulwich.tests.test_suite'
-    setup_kwargs['tests_require'] = tests_require
-    setup_kwargs['entry_points'] = {
-        "console_scripts": [
-            "dulwich=dulwich.cli:main",
-        ]}
-    setup_kwargs['python_requires'] = '>=3.6'
-else:
-    scripts.append('bin/dulwich')
 
 
 with io.open(os.path.join(os.path.dirname(__file__), "README.rst"),
@@ -111,13 +84,21 @@ setup(name='dulwich',
           "GitHub": "https://github.com/dulwich/dulwich",
       },
       keywords="git vcs",
-      packages=['dulwich', 'dulwich.tests', 'dulwich.tests.compat',
-                'dulwich.contrib'],
+      packages=['dulwich', 'dulwich.cloud', 'dulwich.tests',
+                'dulwich.tests.compat', 'dulwich.contrib'],
       package_data={'': ['../docs/tutorial/*.txt', 'py.typed']},
       scripts=scripts,
       ext_modules=ext_modules,
       zip_safe=False,
-      distclass=DulwichDistribution,
+      distclass=DulwichDistribution,  # type: ignore
+      install_requires=['urllib3>=1.25'],
+      include_package_data=True,
+      test_suite='dulwich.tests.test_suite',
+      tests_require=tests_require,
+      entry_points={
+          "console_scripts": ["dulwich=dulwich.cli:main"]
+      },
+      python_requires='>=3.6',
       classifiers=[
           'Development Status :: 4 - Beta',
           'License :: OSI Approved :: Apache Software License',
@@ -126,11 +107,16 @@ setup(name='dulwich',
           'Programming Language :: Python :: 3.8',
           'Programming Language :: Python :: 3.9',
           'Programming Language :: Python :: 3.10',
+          'Programming Language :: Python :: 3.11',
           'Programming Language :: Python :: Implementation :: CPython',
           'Programming Language :: Python :: Implementation :: PyPy',
           'Operating System :: POSIX',
           'Operating System :: Microsoft :: Windows',
           'Topic :: Software Development :: Version Control',
       ],
-      **setup_kwargs
-      )
+      extras_require={
+          'fastimport': ['fastimport'],
+          'https': ['urllib3>=1.24.1'],
+          'pgp': ['gpg'],
+          'paramiko': ['paramiko'],
+      })

+ 28 - 28
status.yaml

@@ -1,30 +1,30 @@
 ---
 configuration:
- - key: core.compression
-   status: supported
- - key: core.looseCompression
-   status: supported
- - key: core.packCompression
-   status: supported
- - key: core.filemode
-   status: supported
- - key: http.proxy
-   status: supported
- - key: http.useragent
-   status: supported
- - key: http.sslVerify
-   status: supported
- - key: http.sslCAInfo
-   status: supported
- - key: i18n.commitEncoding
-   status: supported
- - key: core.excludsFile
-   status: supported
- - key: user.name
-   status: supported
- - key: user.email
-   status: supported
- - key: core.protectNTFS
-   status: supported
- - key: core.ignorecase
-   status: supported
+  - key: core.compression
+    status: supported
+  - key: core.looseCompression
+    status: supported
+  - key: core.packCompression
+    status: supported
+  - key: core.filemode
+    status: supported
+  - key: http.proxy
+    status: supported
+  - key: http.useragent
+    status: supported
+  - key: http.sslVerify
+    status: supported
+  - key: http.sslCAInfo
+    status: supported
+  - key: i18n.commitEncoding
+    status: supported
+  - key: core.excludsFile
+    status: supported
+  - key: user.name
+    status: supported
+  - key: user.email
+    status: supported
+  - key: core.protectNTFS
+    status: supported
+  - key: core.ignorecase
+    status: supported

+ 0 - 0
dulwich/tests/data/blobs/11/11111111111111111111111111111111111111 → testdata/blobs/11/11111111111111111111111111111111111111


+ 0 - 0
dulwich/tests/data/blobs/6f/670c0fb53f9463760b7295fbb814e965fb20c8 → testdata/blobs/6f/670c0fb53f9463760b7295fbb814e965fb20c8


+ 0 - 0
dulwich/tests/data/blobs/95/4a536f7819d40e6f637f849ee187dd10066349 → testdata/blobs/95/4a536f7819d40e6f637f849ee187dd10066349


+ 0 - 0
dulwich/tests/data/blobs/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 → testdata/blobs/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391


+ 0 - 0
dulwich/tests/data/commits/0d/89f20333fbb1d2f3a94da77f4981373d8f4310 → testdata/commits/0d/89f20333fbb1d2f3a94da77f4981373d8f4310


+ 0 - 0
dulwich/tests/data/commits/5d/ac377bdded4c9aeb8dff595f0faeebcc8498cc → testdata/commits/5d/ac377bdded4c9aeb8dff595f0faeebcc8498cc


+ 0 - 0
dulwich/tests/data/commits/60/dacdc733de308bb77bb76ce0fb0f9b44c9769e → testdata/commits/60/dacdc733de308bb77bb76ce0fb0f9b44c9769e


+ 0 - 0
dulwich/tests/data/indexes/index → testdata/indexes/index


+ 0 - 0
dulwich/tests/data/packs/pack-bc63ddad95e7321ee734ea11a7a62d314e0d7481.idx → testdata/packs/pack-bc63ddad95e7321ee734ea11a7a62d314e0d7481.idx


+ 0 - 0
dulwich/tests/data/packs/pack-bc63ddad95e7321ee734ea11a7a62d314e0d7481.pack → testdata/packs/pack-bc63ddad95e7321ee734ea11a7a62d314e0d7481.pack


+ 0 - 0
dulwich/tests/data/repos/.gitattributes → testdata/repos/.gitattributes


+ 0 - 0
dulwich/tests/data/repos/a.git/HEAD → testdata/repos/a.git/HEAD


+ 0 - 0
dulwich/tests/data/repos/a.git/objects/28/237f4dc30d0d462658d6b937b08a0f0b6ef55a → testdata/repos/a.git/objects/28/237f4dc30d0d462658d6b937b08a0f0b6ef55a


+ 0 - 0
dulwich/tests/data/repos/a.git/objects/2a/72d929692c41d8554c07f6301757ba18a65d91 → testdata/repos/a.git/objects/2a/72d929692c41d8554c07f6301757ba18a65d91


+ 0 - 0
dulwich/tests/data/repos/a.git/objects/4e/f30bbfe26431a69c3820d3a683df54d688f2ec → testdata/repos/a.git/objects/4e/f30bbfe26431a69c3820d3a683df54d688f2ec


+ 0 - 0
dulwich/tests/data/repos/a.git/objects/4f/2e6529203aa6d44b5af6e3292c837ceda003f9 → testdata/repos/a.git/objects/4f/2e6529203aa6d44b5af6e3292c837ceda003f9


+ 0 - 0
dulwich/tests/data/repos/a.git/objects/7d/9a07d797595ef11344549b8d08198e48c15364 → testdata/repos/a.git/objects/7d/9a07d797595ef11344549b8d08198e48c15364


+ 0 - 0
dulwich/tests/data/repos/a.git/objects/a2/96d0bb611188cabb256919f36bc30117cca005 → testdata/repos/a.git/objects/a2/96d0bb611188cabb256919f36bc30117cca005


+ 0 - 0
dulwich/tests/data/repos/a.git/objects/a9/0fa2d900a17e99b433217e988c4eb4a2e9a097 → testdata/repos/a.git/objects/a9/0fa2d900a17e99b433217e988c4eb4a2e9a097


+ 0 - 0
dulwich/tests/data/repos/a.git/objects/b0/931cadc54336e78a1d980420e3268903b57a50 → testdata/repos/a.git/objects/b0/931cadc54336e78a1d980420e3268903b57a50


+ 0 - 0
dulwich/tests/data/repos/a.git/objects/ff/d47d45845a8f6576491e1edb97e3fe6a850e7f → testdata/repos/a.git/objects/ff/d47d45845a8f6576491e1edb97e3fe6a850e7f


+ 0 - 0
dulwich/tests/data/repos/a.git/packed-refs → testdata/repos/a.git/packed-refs


+ 0 - 0
dulwich/tests/data/repos/a.git/refs/heads/master → testdata/repos/a.git/refs/heads/master


+ 0 - 0
dulwich/tests/data/repos/a.git/refs/tags/mytag → testdata/repos/a.git/refs/tags/mytag


+ 0 - 0
dulwich/tests/data/repos/empty.git/HEAD → testdata/repos/empty.git/HEAD


+ 0 - 0
dulwich/tests/data/repos/empty.git/config → testdata/repos/empty.git/config


+ 0 - 0
dulwich/tests/data/repos/empty.git/objects/info/.gitignore → testdata/repos/empty.git/objects/info/.gitignore


+ 0 - 0
dulwich/tests/data/repos/empty.git/objects/pack/.gitignore → testdata/repos/empty.git/objects/pack/.gitignore


+ 0 - 0
dulwich/tests/data/repos/empty.git/refs/heads/.gitignore → testdata/repos/empty.git/refs/heads/.gitignore


+ 0 - 0
dulwich/tests/data/repos/empty.git/refs/tags/.gitignore → testdata/repos/empty.git/refs/tags/.gitignore


+ 0 - 0
dulwich/tests/data/repos/issue88_expect_ack_nak_client.export → testdata/repos/issue88_expect_ack_nak_client.export


+ 0 - 0
dulwich/tests/data/repos/issue88_expect_ack_nak_other.export → testdata/repos/issue88_expect_ack_nak_other.export


+ 0 - 0
dulwich/tests/data/repos/issue88_expect_ack_nak_server.export → testdata/repos/issue88_expect_ack_nak_server.export


+ 0 - 0
dulwich/tests/data/repos/ooo_merge.git/HEAD → testdata/repos/ooo_merge.git/HEAD


+ 0 - 0
dulwich/tests/data/repos/ooo_merge.git/objects/29/69be3e8ee1c0222396a5611407e4769f14e54b → testdata/repos/ooo_merge.git/objects/29/69be3e8ee1c0222396a5611407e4769f14e54b


+ 0 - 0
dulwich/tests/data/repos/ooo_merge.git/objects/38/74e9c60a6d149c44c928140f250d81e6381520 → testdata/repos/ooo_merge.git/objects/38/74e9c60a6d149c44c928140f250d81e6381520


+ 0 - 0
dulwich/tests/data/repos/ooo_merge.git/objects/6f/670c0fb53f9463760b7295fbb814e965fb20c8 → testdata/repos/ooo_merge.git/objects/6f/670c0fb53f9463760b7295fbb814e965fb20c8


+ 0 - 0
dulwich/tests/data/repos/ooo_merge.git/objects/70/c190eb48fa8bbb50ddc692a17b44cb781af7f6 → testdata/repos/ooo_merge.git/objects/70/c190eb48fa8bbb50ddc692a17b44cb781af7f6


+ 0 - 0
dulwich/tests/data/repos/ooo_merge.git/objects/76/01d7f6231db6a57f7bbb79ee52e4d462fd44d1 → testdata/repos/ooo_merge.git/objects/76/01d7f6231db6a57f7bbb79ee52e4d462fd44d1


+ 0 - 0
dulwich/tests/data/repos/ooo_merge.git/objects/90/182552c4a85a45ec2a835cadc3451bebdfe870 → testdata/repos/ooo_merge.git/objects/90/182552c4a85a45ec2a835cadc3451bebdfe870


+ 0 - 0
dulwich/tests/data/repos/ooo_merge.git/objects/95/4a536f7819d40e6f637f849ee187dd10066349 → testdata/repos/ooo_merge.git/objects/95/4a536f7819d40e6f637f849ee187dd10066349


+ 0 - 0
dulwich/tests/data/repos/ooo_merge.git/objects/b2/a2766a2879c209ab1176e7e778b81ae422eeaa → testdata/repos/ooo_merge.git/objects/b2/a2766a2879c209ab1176e7e778b81ae422eeaa


+ 0 - 0
dulwich/tests/data/repos/ooo_merge.git/objects/f5/07291b64138b875c28e03469025b1ea20bc614 → testdata/repos/ooo_merge.git/objects/f5/07291b64138b875c28e03469025b1ea20bc614


+ 0 - 0
dulwich/tests/data/repos/ooo_merge.git/objects/f9/e39b120c68182a4ba35349f832d0e4e61f485c → testdata/repos/ooo_merge.git/objects/f9/e39b120c68182a4ba35349f832d0e4e61f485c


+ 0 - 0
dulwich/tests/data/repos/ooo_merge.git/objects/fb/5b0425c7ce46959bec94d54b9a157645e114f5 → testdata/repos/ooo_merge.git/objects/fb/5b0425c7ce46959bec94d54b9a157645e114f5


Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff