瀏覽代碼

Import upstream version 0.20.46

Jelmer Vernooij 2 年之前
父節點
當前提交
4d8b946632
共有 100 個文件被更改,包括 1126 次插入588 次删除
  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]
 [flake8]
-extend-ignore = E203, E266, E501, W293, W291
+extend-ignore = E203, E266, E501, W293, W291, W503
 max-line-length = 88
 max-line-length = 88
 max-complexity = 18
 max-complexity = 18
 select = B,C,E,F,W,T4,B9
 select = B,C,E,F,W,T4,B9

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

@@ -4,16 +4,16 @@ on:
   push:
   push:
   pull_request:
   pull_request:
   schedule:
   schedule:
-    - cron: '0 6 * * *'  # Daily 6AM UTC build
+    - cron: "0 6 * * *" # Daily 6AM UTC build
 
 
 jobs:
 jobs:
   build:
   build:
-
     runs-on: ${{ matrix.os }}
     runs-on: ${{ matrix.os }}
     strategy:
     strategy:
       matrix:
       matrix:
         os: [ubuntu-latest, macos-latest, windows-latest]
         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:
         exclude:
           # sqlite3 exit handling seems to get in the way
           # sqlite3 exit handling seems to get in the way
           - os: macos-latest
           - os: macos-latest
@@ -24,41 +24,38 @@ jobs:
       fail-fast: false
       fail-fast: false
 
 
     steps:
     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:
   push:
   pull_request:
   pull_request:
   schedule:
   schedule:
-    - cron: '0 6 * * *'  # Daily 6AM UTC build
+    - cron: "0 6 * * *" # Daily 6AM UTC build
 
 
 jobs:
 jobs:
   build:
   build:
-
     runs-on: ${{ matrix.os }}
     runs-on: ${{ matrix.os }}
     strategy:
     strategy:
       matrix:
       matrix:
         os: [macos-latest, windows-latest]
         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:
         include:
           - os: ubuntu-latest
           - os: ubuntu-latest
-            python-version: '3.x'
+            python-version: "3.x"
           # path encoding
           # path encoding
+        exclude:
+          - os: macos-latest
+            architecture: "x86"
       fail-fast: true
       fail-fast: true
 
 
     steps:
     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:
   publish:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
@@ -84,18 +88,18 @@ jobs:
     needs: build
     needs: build
     if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/dulwich-')
     if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/dulwich-')
     steps:
     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
 Examples of behavior that contributes to creating a positive environment
 include:
 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:
 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
 ## Our Responsibilities
 
 
@@ -67,10 +67,11 @@ members of the project's leadership.
 
 
 ## Attribution
 ## 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
 [homepage]: https://www.contributor-covenant.org
 
 
 For answers to common questions about this code of conduct, see
 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
 .PHONY: apidocs
 
 
 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
 0.20.44	2022-06-30
 
 
  * Fix reading of chunks in server. (Jelmer Vernooij, #977)
  * Fix reading of chunks in server. (Jelmer Vernooij, #977)
@@ -980,7 +1041,7 @@
     probing the filesystem for trustable permissions.
     probing the filesystem for trustable permissions.
     (Koen Martens)
     (Koen Martens)
 
 
-  * Fix ``porcelain.reset`` to respect the comittish argument.
+  * Fix ``porcelain.reset`` to respect the committish argument.
     (Koen Martens)
     (Koen Martens)
 
 
   * Fix dulwich.porcelain.ls_remote() on Python 3.
   * Fix dulwich.porcelain.ls_remote() on Python 3.
@@ -2017,7 +2078,7 @@ FEATURES
 
 
   * Provide strnlen() on mingw32 which doesn't have it. (Hans Kolek)
   * 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
  FEATURES
 
 
@@ -2101,7 +2162,7 @@ FEATURES
 
 
  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)
   * 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.
   * Fix RefsContainer.add_if_new to support dangling symrefs.
     (Dave Borowitz)
     (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)
     empty. (Dave Borowitz)
 
 
   * Always update ShaFile.id when the contents of the object get changed. 
   * Always update ShaFile.id when the contents of the object get changed. 

+ 2 - 1
PKG-INFO

@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Metadata-Version: 2.1
 Name: dulwich
 Name: dulwich
-Version: 0.20.44
+Version: 0.20.46
 Summary: Python Git Library
 Summary: Python Git Library
 Home-page: https://www.dulwich.io/
 Home-page: https://www.dulwich.io/
 Author: Jelmer Vernooij
 Author: Jelmer Vernooij
@@ -18,6 +18,7 @@ Classifier: Programming Language :: Python :: 3.7
 Classifier: Programming Language :: Python :: 3.8
 Classifier: Programming Language :: Python :: 3.8
 Classifier: Programming Language :: Python :: 3.9
 Classifier: Programming Language :: Python :: 3.9
 Classifier: Programming Language :: Python :: 3.10
 Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
 Classifier: Programming Language :: Python :: Implementation :: CPython
 Classifier: Programming Language :: Python :: Implementation :: CPython
 Classifier: Programming Language :: Python :: Implementation :: PyPy
 Classifier: Programming Language :: Python :: Implementation :: PyPy
 Classifier: Operating System :: POSIX
 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
 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
 for storing repositories. Each repository will be contained in
 a Swift container.
 a Swift container.
 
 
@@ -118,7 +118,7 @@ The other Git commands can be used the way you do usually against
 a regular repository.
 a regular repository.
 
 
 Note the daemon subcommands starts a Git server listening for the
 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).
 at all between the cGIT client and the GIT server (Dulwich).
 
 
 Note on the .info file for pack object
 Note on the .info file for pack object

+ 2 - 1
SECURITY.md

@@ -9,4 +9,5 @@
 
 
 ## Reporting a Vulnerability
 ## 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.ifconfig',
     'sphinx.ext.intersphinx',
     'sphinx.ext.intersphinx',
     'sphinx.ext.napoleon',
     'sphinx.ext.napoleon',
-    ]
+]
 
 
 autoclass_content = "both"
 autoclass_content = "both"
 
 
@@ -186,8 +186,8 @@ htmlhelp_basename = 'dulwichdoc'
 # (source start file, target name, title, author, documentclass
 # (source start file, target name, title, author, documentclass
 # [howto/manual]).
 # [howto/manual]).
 latex_documents = [
 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
 # 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.
 # If false, no module index is generated.
 # latex_use_modindex = True
 # 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
 .. _C git: https://github.com/git/git/blob/master/Documentation/i18n.txt
 
 
 The library should be able to read *all* existing git repositories,
 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.
 does not convert paths to unicode strings.
 
 
 A further consideration is that converting back and forth to unicode
 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")
   >>> 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
 Commit changes
 --------------
 --------------

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

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

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

@@ -89,6 +89,7 @@ dulwich/repo.py
 dulwich/server.py
 dulwich/server.py
 dulwich/stash.py
 dulwich/stash.py
 dulwich/stdint.h
 dulwich/stdint.h
+dulwich/submodule.py
 dulwich/walk.py
 dulwich/walk.py
 dulwich/web.py
 dulwich/web.py
 dulwich.egg-info/PKG-INFO
 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_utils.py
 dulwich/tests/compat/test_web.py
 dulwich/tests/compat/test_web.py
 dulwich/tests/compat/utils.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/clone.py
 examples/config.py
 examples/config.py
 examples/diff.py
 examples/diff.py
 examples/gcs.py
 examples/gcs.py
 examples/latest_change.py
 examples/latest_change.py
 examples/memoryrepo.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]
 fastimport
 fastimport
 
 
 [https]
 [https]
-urllib3[secure]>=1.24.1
+urllib3>=1.24.1
 
 
 [paramiko]
 [paramiko]
 paramiko
 paramiko

+ 1 - 1
dulwich/__init__.py

@@ -22,4 +22,4 @@
 
 
 """Python implementation of the Git file formats and protocols."""
 """Python implementation of the Git file formats and protocols."""
 
 
-__version__ = (0, 20, 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":
         if format == "gz":
             # Manually correct the gzip header file modification time so that
             # Manually correct the gzip header file modification time so that
             # archives created from the same Git tree are always identical.
             # 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:
             # accessible from the tarfile API, see:
             # https://bugs.python.org/issue31526
             # https://bugs.python.org/issue31526
             buf.seek(0)
             buf.seek(0)

+ 16 - 2
dulwich/cli.py

@@ -321,6 +321,14 @@ class cmd_rev_list(Command):
         porcelain.rev_list(".", args)
         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):
 class cmd_tag(Command):
     def run(self, args):
     def run(self, args):
         parser = optparse.OptionParser()
         parser = optparse.OptionParser()
@@ -499,7 +507,7 @@ class cmd_ls_tree(Command):
             "-r",
             "-r",
             "--recursive",
             "--recursive",
             action="store_true",
             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.")
         parser.add_option("--name-only", action="store_true", help="Only display name.")
         options, args = parser.parse_args(args)
         options, args = parser.parse_args(args)
@@ -553,10 +561,15 @@ class cmd_push(Command):
 
 
     def run(self, argv):
     def run(self, argv):
         parser = argparse.ArgumentParser()
         parser = argparse.ArgumentParser()
+        parser.add_argument('-f', '--force', action='store_true', help='Force')
         parser.add_argument('to_location', type=str)
         parser.add_argument('to_location', type=str)
         parser.add_argument('refspec', type=str, nargs='*')
         parser.add_argument('refspec', type=str, nargs='*')
         args = parser.parse_args(argv)
         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):
 class cmd_remote_add(Command):
@@ -721,6 +734,7 @@ commands = {
     "stash": cmd_stash,
     "stash": cmd_stash,
     "status": cmd_status,
     "status": cmd_status,
     "symbolic-ref": cmd_symbolic_ref,
     "symbolic-ref": cmd_symbolic_ref,
+    "submodule": cmd_submodule,
     "tag": cmd_tag,
     "tag": cmd_tag,
     "update-server-info": cmd_update_server_info,
     "update-server-info": cmd_update_server_info,
     "upload-pack": cmd_upload_pack,
     "upload-pack": cmd_upload_pack,

+ 59 - 86
dulwich/client.py

@@ -46,7 +46,18 @@ import select
 import socket
 import socket
 import subprocess
 import subprocess
 import sys
 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 (
 from urllib.parse import (
     quote as urlquote,
     quote as urlquote,
@@ -57,9 +68,12 @@ from urllib.parse import (
     urlunparse,
     urlunparse,
 )
 )
 
 
+if TYPE_CHECKING:
+    import urllib3
+
 
 
 import dulwich
 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 (
 from dulwich.errors import (
     GitProtocolError,
     GitProtocolError,
     NotGitRepository,
     NotGitRepository,
@@ -437,7 +451,6 @@ class _v1ReceivePackHeader(object):
         """Handle the head of a 'git-receive-pack' request.
         """Handle the head of a 'git-receive-pack' request.
 
 
         Args:
         Args:
-          proto: Protocol object to read from
           capabilities: List of negotiated capabilities
           capabilities: List of negotiated capabilities
           old_refs: Old refs, as received from the server
           old_refs: Old refs, as received from the server
           new_refs: Refs to change
           new_refs: Refs to change
@@ -950,6 +963,21 @@ class GitClient(object):
         negotiated_capabilities = self._fetch_capabilities & server_capabilities
         negotiated_capabilities = self._fetch_capabilities & server_capabilities
         return (negotiated_capabilities, symrefs, agent)
         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):
 def check_wants(wants, refs):
     """Check that a set of wants is valid.
     """Check that a set of wants is valid.
@@ -1533,31 +1561,6 @@ default_local_git_client_cls = LocalGitClient
 class SSHVendor(object):
 class SSHVendor(object):
     """A client side SSH implementation."""
     """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(
     def run_command(
         self,
         self,
         host,
         host,
@@ -1616,7 +1619,8 @@ class SubprocessSSHVendor(SSHVendor):
 
 
         if ssh_command:
         if ssh_command:
             import shlex
             import shlex
-            args = shlex.split(ssh_command) + ["-x"]
+            args = shlex.split(
+                ssh_command, posix=(sys.platform != 'win32')) + ["-x"]
         else:
         else:
             args = ["ssh", "-x"]
             args = ["ssh", "-x"]
 
 
@@ -1658,7 +1662,8 @@ class PLinkSSHVendor(SSHVendor):
 
 
         if ssh_command:
         if ssh_command:
             import shlex
             import shlex
-            args = shlex.split(ssh_command) + ["-ssh"]
+            args = shlex.split(
+                ssh_command, posix=(sys.platform != 'win32')) + ["-ssh"]
         elif sys.platform == "win32":
         elif sys.platform == "win32":
             args = ["plink.exe", "-ssh"]
             args = ["plink.exe", "-ssh"]
         else:
         else:
@@ -1781,7 +1786,7 @@ class SSHGitClient(TraditionalGitClient):
             kwargs["password"] = self.password
             kwargs["password"] = self.password
         if self.key_filename is not None:
         if self.key_filename is not None:
             kwargs["key_filename"] = self.key_filename
             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:
         if self.ssh_command is not None:
             kwargs["ssh_command"] = self.ssh_command
             kwargs["ssh_command"] = self.ssh_command
         con = self.ssh_vendor.run_command(
         con = self.ssh_vendor.run_command(
@@ -1807,8 +1812,8 @@ def default_user_agent_string():
 
 
 def default_urllib3_manager(   # noqa: C901
 def default_urllib3_manager(   # noqa: C901
     config, pool_manager_cls=None, proxy_manager_cls=None, **override_kwargs
     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.
     Honour detected proxy configurations.
 
 
@@ -1817,9 +1822,9 @@ def default_urllib3_manager(   # noqa: C901
       override_kwargs: Additional arguments for `urllib3.ProxyManager`
       override_kwargs: Additional arguments for `urllib3.ProxyManager`
 
 
     Returns:
     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
     proxy_server = user_agent = None
@@ -1858,7 +1863,9 @@ def default_urllib3_manager(   # noqa: C901
 
 
     headers = {"User-agent": user_agent}
     headers = {"User-agent": user_agent}
 
 
-    kwargs = {}
+    kwargs = {
+        "ca_certs" : ca_certs,
+    }
     if ssl_verify is True:
     if ssl_verify is True:
         kwargs["cert_reqs"] = "CERT_REQUIRED"
         kwargs["cert_reqs"] = "CERT_REQUIRED"
     elif ssl_verify is False:
     elif ssl_verify is False:
@@ -1867,19 +1874,8 @@ def default_urllib3_manager(   # noqa: C901
         # Default to SSL verification
         # Default to SSL verification
         kwargs["cert_reqs"] = "CERT_REQUIRED"
         kwargs["cert_reqs"] = "CERT_REQUIRED"
 
 
-    if ca_certs is not None:
-        kwargs["ca_certs"] = ca_certs
     kwargs.update(override_kwargs)
     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
     import urllib3
 
 
     if proxy_server is not None:
     if proxy_server is not None:
@@ -1919,9 +1915,9 @@ class AbstractHttpGitClient(GitClient):
           data: Request data.
           data: Request data.
 
 
         Returns:
         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.
           method for the response data.
 
 
         """
         """
@@ -2268,40 +2264,6 @@ def _win32_url_to_path(parsed) -> str:
     return url2pathname(netloc + path)  # type: ignore
     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(
 def get_transport_and_path_from_url(
         url: str, config: Optional[Config] = None,
         url: str, config: Optional[Config] = None,
         operation: Optional[str] = None, **kwargs) -> Tuple[GitClient, str]:
         operation: Optional[str] = None, **kwargs) -> Tuple[GitClient, str]:
@@ -2321,6 +2283,12 @@ def get_transport_and_path_from_url(
     """
     """
     if config is not None:
     if config is not None:
         url = apply_instead_of(config, url, push=(operation == "push"))
         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)
     parsed = urlparse(url)
     if parsed.scheme == "git":
     if parsed.scheme == "git":
         return (TCPGitClient.from_parsedurl(parsed, **kwargs), parsed.path)
         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(
 def get_transport_and_path(
     location: str,
     location: str,
+    config: Optional[Config] = None,
     operation: Optional[str] = None,
     operation: Optional[str] = None,
     **kwargs: Any
     **kwargs: Any
 ) -> Tuple[GitClient, str]:
 ) -> Tuple[GitClient, str]:
@@ -2380,9 +2349,13 @@ def get_transport_and_path(
       Tuple with client instance and relative 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
     # First, try to parse it as a URL
     try:
     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:
     except ValueError:
         pass
         pass
 
 

+ 80 - 52
dulwich/config.py

@@ -28,7 +28,6 @@ TODO:
 
 
 import os
 import os
 import sys
 import sys
-import warnings
 from typing import (
 from typing import (
     BinaryIO,
     BinaryIO,
     Iterable,
     Iterable,
@@ -45,7 +44,7 @@ from typing import (
 from dulwich.file import GitFile
 from dulwich.file import GitFile
 
 
 
 
-SENTINAL = object()
+SENTINEL = object()
 
 
 
 
 def lower_key(key):
 def lower_key(key):
@@ -112,13 +111,13 @@ class CaseInsensitiveOrderedMultiDict(MutableMapping):
     def __getitem__(self, item):
     def __getitem__(self, item):
         return self._keyed[lower_key(item)]
         return self._keyed[lower_key(item)]
 
 
-    def get(self, key, default=SENTINAL):
+    def get(self, key, default=SENTINEL):
         try:
         try:
             return self[key]
             return self[key]
         except KeyError:
         except KeyError:
             pass
             pass
 
 
-        if default is SENTINAL:
+        if default is SENTINEL:
             return type(self)()
             return type(self)()
 
 
         return default
         return default
@@ -129,7 +128,7 @@ class CaseInsensitiveOrderedMultiDict(MutableMapping):
             if lower_key(actual) == key:
             if lower_key(actual) == key:
                 yield value
                 yield value
 
 
-    def setdefault(self, key, default=SENTINAL):
+    def setdefault(self, key, default=SENTINEL):
         try:
         try:
             return self[key]
             return self[key]
         except KeyError:
         except KeyError:
@@ -231,29 +230,6 @@ class Config(object):
         """
         """
         raise NotImplementedError(self.items)
         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]:
     def sections(self) -> Iterator[Section]:
         """Iterate over the sections.
         """Iterate over the sections.
 
 
@@ -507,6 +483,46 @@ def _strip_comments(line: bytes) -> bytes:
     return line
     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):
 class ConfigFile(ConfigDict):
     """A Git configuration file, like .git/config or ~/.gitconfig."""
     """A Git configuration file, like .git/config or ~/.gitconfig."""
 
 
@@ -532,31 +548,8 @@ class ConfigFile(ConfigDict):
                 line = line[3:]
                 line = line[3:]
             line = line.lstrip()
             line = line.lstrip()
             if setting is None:
             if setting is None:
-                # Parse section header ("[bla]")
                 if len(line) > 0 and line[:1] == b"[":
                 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)
                     ret._values.setdefault(section)
                 if _strip_comments(line).strip() == b"":
                 if _strip_comments(line).strip() == b"":
                     continue
                     continue
@@ -772,3 +765,38 @@ def parse_submodules(config: ConfigFile) -> Iterator[Tuple[bytes, bytes, bytes]]
             sm_path = config.get(section, b"path")
             sm_path = config.get(section, b"path")
             sm_url = config.get(section, b"url")
             sm_url = config.get(section, b"url")
             yield (sm_path, sm_url, section_name)
             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():
 def main():
     argv = sys.argv
     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:
     if len(sys.argv) > 1:
         diffpath = argv[1]
         diffpath = argv[1]
         data = b""
         data = b""
@@ -197,7 +197,7 @@ def main():
 
 
     # if no path argument to a diff file is passed in, run
     # if no path argument to a diff file is passed in, run
     # a self test. The test case includes tricky things like
     # 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.
     # added files and removed files.
     # All extracted from Sigil-Ebook/Sigil's github repo with
     # All extracted from Sigil-Ebook/Sigil's github repo with
     # full permission to use under this license.
     # 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()
     __version__ = get_current_version()
 
 
 This example assumes the tags have a leading "v" like "v0.3", and that the
 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::
 EG::
 
 

+ 2 - 2
dulwich/contrib/swift.py

@@ -504,9 +504,9 @@ class SwiftPackReader(object):
     """A SwiftPackReader that mimic read and sync method
     """A SwiftPackReader that mimic read and sync method
 
 
     The reader allows to read a specified amount of bytes from
     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
     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.
     to read from Swift.
     """
     """
 
 

+ 1 - 1
dulwich/contrib/test_paramiko_vendor.py

@@ -131,7 +131,7 @@ class ParamikoSSHVendorTests(TestCase):
     def setUp(self):
     def setUp(self):
         import paramiko.transport
         import paramiko.transport
 
 
-        # reenable server functionality for tests
+        # re-enable server functionality for tests
         if hasattr(paramiko.transport, "SERVER_DISABLED_BY_GENTOO"):
         if hasattr(paramiko.transport, "SERVER_DISABLED_BY_GENTOO"):
             paramiko.transport.SERVER_DISABLED_BY_GENTOO = False
             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)
         swift.SwiftRepo.init_bare(self.scon, self.conf)
         tcp_client = client.TCPGitClient(self.server_address, port=self.port)
         tcp_client = client.TCPGitClient(self.server_address, port=self.port)
         remote_refs = tcp_client.fetch(self.fakerepo, local_repo)
         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)
         self.assertEqual(remote_refs, None)
 
 
     def test_push_commit(self):
     def test_push_commit(self):

+ 1 - 0
dulwich/file.py

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

+ 1 - 1
dulwich/graph.py

@@ -74,7 +74,7 @@ def _find_lcas(lookup_parents, c1, c2s):
                     cstates[pcmt] = flags
                     cstates[pcmt] = flags
                 wlst.append(pcmt)
                 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 = []
     results = []
     for cmt in cands:
     for cmt in cands:
         if not (cstates[cmt] & _DNC):
         if not (cstates[cmt] & _DNC):

+ 3 - 1
dulwich/hooks.py

@@ -100,7 +100,9 @@ class ShellHook(Hook):
             args = self.pre_exec_callback(*args)
             args = self.pre_exec_callback(*args)
 
 
         try:
         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 ret != 0:
                 if self.post_exec_callback is not None:
                 if self.post_exec_callback is not None:
                     self.post_exec_callback(0, *args)
                     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
     This returns an index value for files, symlinks
     and tree references. for directories and
     and tree references. for directories and
-    non-existant files it returns None
+    non-existent files it returns None
 
 
     Args:
     Args:
       path: Path to create an index entry for
       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):
 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:
     Args:
       root_path: Root path to access from
       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 the number of objects."""
         return len(list(self.itershas()))
         return len(list(self.itershas()))
 
 
-    def empty(self):
-        import warnings
-
-        warnings.warn("Use bool() instead.", DeprecationWarning)
-        return self._empty()
-
     def _empty(self):
     def _empty(self):
         it = self.itershas()
         it = self.itershas()
         try:
         try:

+ 56 - 8
dulwich/objects.py

@@ -34,7 +34,6 @@ from typing import (
     Union,
     Union,
     Type,
     Type,
 )
 )
-import warnings
 import zlib
 import zlib
 from hashlib import sha1
 from hashlib import sha1
 
 
@@ -1091,13 +1090,6 @@ class Tree(ShaFile):
           name: The name of the entry, as a string.
           name: The name of the entry, as a string.
           hexsha: The hex SHA 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._entries[name] = mode, hexsha
         self._needs_serialization = True
         self._needs_serialization = True
 
 
@@ -1428,6 +1420,62 @@ class Commit(ShaFile):
 
 
         # TODO: optionally check for duplicate parents
         # 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):
     def _serialize(self):
         chunks = []
         chunks = []
         tree_bytes = self._tree.id if isinstance(self._tree, Tree) else self._tree
         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
       diff_binary: Whether to diff files even if they
         are considered binary files by is_binary().
         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
     (old_path, old_mode, old_id) = old_file
     (new_path, new_mode, new_id) = new_file
     (new_path, new_mode, new_id) = new_file

+ 150 - 25
dulwich/porcelain.py

@@ -43,6 +43,7 @@ Currently implemented:
  * remote{_add}
  * remote{_add}
  * receive-pack
  * receive-pack
  * reset
  * reset
+ * submodule_list
  * rev-list
  * rev-list
  * tag{_create,_delete,_list}
  * tag{_create,_delete,_list}
  * upload-pack
  * upload-pack
@@ -86,6 +87,7 @@ from dulwich.client import (
     get_transport_and_path,
     get_transport_and_path,
 )
 )
 from dulwich.config import (
 from dulwich.config import (
+    ConfigFile,
     StackedConfig,
     StackedConfig,
 )
 )
 from dulwich.diff_tree import (
 from dulwich.diff_tree import (
@@ -189,6 +191,78 @@ class RemoteExists(Error):
     """Raised when the remote already exists."""
     """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):
 def open_repo(path_or_repo):
     """Open an argument that can be a repository or a path for a repository."""
     """Open an argument that can be a repository or a path for a repository."""
     if isinstance(path_or_repo, BaseRepo):
     if isinstance(path_or_repo, BaseRepo):
@@ -327,9 +401,12 @@ def commit(
     repo=".",
     repo=".",
     message=None,
     message=None,
     author=None,
     author=None,
+    author_timezone=None,
     committer=None,
     committer=None,
+    commit_timezone=None,
     encoding=None,
     encoding=None,
     no_verify=False,
     no_verify=False,
+    signoff=False,
 ):
 ):
     """Create a new commit.
     """Create a new commit.
 
 
@@ -337,25 +414,37 @@ def commit(
       repo: Path to repository
       repo: Path to repository
       message: Optional commit message
       message: Optional commit message
       author: Optional author name and email
       author: Optional author name and email
+      author_timezone: Author timestamp timezone
       committer: Optional committer name and email
       committer: Optional committer name and email
+      commit_timezone: Commit timestamp timezone
       no_verify: Skip pre-commit and commit-msg hooks
       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
     Returns: SHA1 of the new commit
     """
     """
     # FIXME: Support --all argument
     # FIXME: Support --all argument
-    # FIXME: Support --signoff argument
     if getattr(message, "encode", None):
     if getattr(message, "encode", None):
         message = message.encode(encoding or DEFAULT_ENCODING)
         message = message.encode(encoding or DEFAULT_ENCODING)
     if getattr(author, "encode", None):
     if getattr(author, "encode", None):
         author = author.encode(encoding or DEFAULT_ENCODING)
         author = author.encode(encoding or DEFAULT_ENCODING)
     if getattr(committer, "encode", None):
     if getattr(committer, "encode", None):
         committer = committer.encode(encoding or DEFAULT_ENCODING)
         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:
     with open_repo_closing(repo) as r:
         return r.do_commit(
         return r.do_commit(
             message=message,
             message=message,
             author=author,
             author=author,
+            author_timezone=author_timezone,
             committer=committer,
             committer=committer,
+            commit_timezone=commit_timezone,
             encoding=encoding,
             encoding=encoding,
             no_verify=no_verify,
             no_verify=no_verify,
+            sign=signoff if isinstance(signoff, (str, bool)) else None,
         )
         )
 
 
 
 
@@ -401,6 +490,7 @@ def clone(
     origin="origin",
     origin="origin",
     depth=None,
     depth=None,
     branch=None,
     branch=None,
+    config=None,
     **kwargs
     **kwargs
 ):
 ):
     """Clone a local or remote git repository.
     """Clone a local or remote git repository.
@@ -416,6 +506,7 @@ def clone(
       depth: Depth to fetch at
       depth: Depth to fetch at
       branch: Optional branch or tag to be used as HEAD in the new repository
       branch: Optional branch or tag to be used as HEAD in the new repository
         instead of the cloned repository's HEAD.
         instead of the cloned repository's HEAD.
+      config: Configuration to use
     Returns: The new repository
     Returns: The new repository
     """
     """
     if outstream is not None:
     if outstream is not None:
@@ -428,6 +519,9 @@ def clone(
         )
         )
         # TODO(jelmer): Capture logging output and stream to errstream
         # TODO(jelmer): Capture logging output and stream to errstream
 
 
+    if config is None:
+        config = StackedConfig.default()
+
     if checkout is None:
     if checkout is None:
         checkout = not bare
         checkout = not bare
     if checkout and bare:
     if checkout and bare:
@@ -438,7 +532,8 @@ def clone(
 
 
     mkdir = not os.path.exists(target)
     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(
     return client.clone(
         path,
         path,
@@ -858,13 +953,49 @@ def rev_list(repo, commits, outstream=sys.stdout):
             outstream.write(entry.commit.id + b"\n")
             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(
 def tag_create(
@@ -911,8 +1042,7 @@ def tag_create(
                 tag_time = int(time.time())
                 tag_time = int(time.time())
             tag_obj.tag_time = tag_time
             tag_obj.tag_time = tag_time
             if tag_timezone is None:
             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):
             elif isinstance(tag_timezone, str):
                 tag_timezone = parse_timezone(tag_timezone)
                 tag_timezone = parse_timezone(tag_timezone)
             tag_obj.tag_timezone = tag_timezone
             tag_obj.tag_timezone = tag_timezone
@@ -927,16 +1057,6 @@ def tag_create(
         r.refs[_make_tag_ref(tag)] = tag_id
         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):
 def tag_list(repo, outstream=sys.stdout):
     """List all tags.
     """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":
       untracked_files: How to handle untracked files, defaults to "all":
           "no": do not return untracked files
           "no": do not return untracked files
           "all": include all files in untracked directories
           "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.
           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,
     Returns: GitStatus tuple,
         staged -  dict with lists of staged paths (diff index/HEAD)
         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,
             exclude_ignored=not ignored,
             untracked_files=untracked_files,
             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)
         return GitStatus(tracked_changes, unstaged_changes, untracked_changes)
 
 
@@ -1456,7 +1581,7 @@ def branch_create(repo, name, objectish=None, force=False):
             objectish = "HEAD"
             objectish = "HEAD"
         object = parse_object(r, objectish)
         object = parse_object(r, objectish)
         refname = _make_branch_ref(name)
         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:
         if force:
             r.refs.set_if_equals(refname, None, object.id, message=ref_message)
             r.refs.set_if_equals(refname, None, object.id, message=ref_message)
         else:
         else:
@@ -1541,7 +1666,7 @@ def fetch(
     with open_repo_closing(repo) as r:
     with open_repo_closing(repo) as r:
         (remote_name, remote_location) = get_remote_repo(r, remote_location)
         (remote_name, remote_location) = get_remote_repo(r, remote_location)
         if message is None:
         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(
         client, path = get_transport_and_path(
             remote_location, config=r.get_config_stack(), **kwargs
             remote_location, config=r.get_config_stack(), **kwargs
         )
         )

+ 2 - 0
dulwich/protocol.py

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

+ 24 - 22
dulwich/refs.py

@@ -49,6 +49,14 @@ BAD_REF_CHARS = set(b"\177 ~^:?*[")
 ANNOTATED_TAG_SUFFIX = b"^{}"
 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):
 def parse_symref_value(contents):
     """Parse a symref value.
     """Parse a symref value.
 
 
@@ -231,7 +239,7 @@ class RefsContainer(object):
         for key in keys:
         for key in keys:
             try:
             try:
                 ret[key] = self[(base + b"/" + key).strip(b"/")]
                 ret[key] = self[(base + b"/" + key).strip(b"/")]
-            except KeyError:
+            except (SymrefLoop, KeyError):
                 continue  # Unable to resolve
                 continue  # Unable to resolve
 
 
         return ret
         return ret
@@ -294,21 +302,9 @@ class RefsContainer(object):
                 break
                 break
             depth += 1
             depth += 1
             if depth > 5:
             if depth > 5:
-                raise KeyError(name)
+                raise SymrefLoop(name, depth)
         return refnames, contents
         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):
     def __contains__(self, refname):
         if self.read_ref(refname):
         if self.read_ref(refname):
             return True
             return True
@@ -511,12 +507,12 @@ class DictRefsContainer(RefsContainer):
 
 
     def add_if_new(
     def add_if_new(
         self,
         self,
-        name,
-        ref,
+        name: bytes,
+        ref: bytes,
         committer=None,
         committer=None,
         timestamp=None,
         timestamp=None,
         timezone=None,
         timezone=None,
-        message=None,
+        message: Optional[bytes] = None,
     ):
     ):
         if name in self._refs:
         if name in self._refs:
             return False
             return False
@@ -883,12 +879,12 @@ class DiskRefsContainer(RefsContainer):
 
 
     def add_if_new(
     def add_if_new(
         self,
         self,
-        name,
-        ref,
+        name: bytes,
+        ref: bytes,
         committer=None,
         committer=None,
         timestamp=None,
         timestamp=None,
         timezone=None,
         timezone=None,
-        message=None,
+        message: Optional[bytes] = None,
     ):
     ):
         """Add a new reference only if it does not already exist.
         """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)
             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"/"
     origin_base = b"refs/remotes/" + origin + b"/"
     if branch:
     if branch:
         origin_ref = origin_base + 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
             head_ref = LOCAL_TAG_PREFIX + branch
         else:
         else:
             raise ValueError(
             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:
     elif origin_head:
         head_ref = origin_head
         head_ref = origin_head
@@ -1165,6 +1165,8 @@ def _set_default_branch(refs, origin, origin_head, branch, ref_message):
             )
             )
         except KeyError:
         except KeyError:
             pass
             pass
+    else:
+        raise ValueError('neither origin_head nor branch are provided')
     return head_ref
     return head_ref
 
 
 
 

+ 26 - 11
dulwich/repo.py

@@ -303,7 +303,7 @@ def _set_filesystem_hidden(path):
         if not SetFileAttributesW(path, FILE_ATTRIBUTE_HIDDEN):
         if not SetFileAttributesW(path, FILE_ATTRIBUTE_HIDDEN):
             pass  # Could raise or log `ctypes.WinError()` here
             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):
 class ParentsProvider(object):
@@ -399,7 +399,7 @@ class BaseRepo(object):
         raise NotImplementedError(self._put_named_file)
         raise NotImplementedError(self._put_named_file)
 
 
     def _del_named_file(self, path):
     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)
         raise NotImplementedError(self._del_named_file)
 
 
     def open_index(self):
     def open_index(self):
@@ -876,6 +876,7 @@ class BaseRepo(object):
         ref=b"HEAD",
         ref=b"HEAD",
         merge_heads=None,
         merge_heads=None,
         no_verify=False,
         no_verify=False,
+        sign=False,
     ):
     ):
         """Create a new commit.
         """Create a new commit.
 
 
@@ -899,6 +900,9 @@ class BaseRepo(object):
           ref: Optional ref to commit to (defaults to current branch)
           ref: Optional ref to commit to (defaults to current branch)
           merge_heads: Merge heads (defaults to .git/MERGE_HEAD)
           merge_heads: Merge heads (defaults to .git/MERGE_HEAD)
           no_verify: Skip pre-commit and commit-msg hooks
           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:
         Returns:
           New commit SHA1
           New commit SHA1
@@ -970,14 +974,20 @@ class BaseRepo(object):
         except KeyError:  # no hook defined, message not modified
         except KeyError:  # no hook defined, message not modified
             c.message = message
             c.message = message
 
 
+        keyid = sign if isinstance(sign, str) else None
+
         if ref is None:
         if ref is None:
             # Create a dangling commit
             # Create a dangling commit
             c.parents = merge_heads
             c.parents = merge_heads
+            if sign:
+                c.sign(keyid)
             self.object_store.add_object(c)
             self.object_store.add_object(c)
         else:
         else:
             try:
             try:
                 old_head = self.refs[ref]
                 old_head = self.refs[ref]
                 c.parents = [old_head] + merge_heads
                 c.parents = [old_head] + merge_heads
+                if sign:
+                    c.sign(keyid)
                 self.object_store.add_object(c)
                 self.object_store.add_object(c)
                 ok = self.refs.set_if_equals(
                 ok = self.refs.set_if_equals(
                     ref,
                     ref,
@@ -990,6 +1000,8 @@ class BaseRepo(object):
                 )
                 )
             except KeyError:
             except KeyError:
                 c.parents = merge_heads
                 c.parents = merge_heads
+                if sign:
+                    c.sign(keyid)
                 self.object_store.add_object(c)
                 self.object_store.add_object(c)
                 ok = self.refs.add_if_new(
                 ok = self.refs.add_if_new(
                     ref,
                     ref,
@@ -1041,7 +1053,7 @@ class UnsupportedVersion(Exception):
 class Repo(BaseRepo):
 class Repo(BaseRepo):
     """A git repository backed by local disk.
     """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.
     the path of the repository.
 
 
     To create a new repository, use the Repo.init class method.
     To create a new repository, use the Repo.init class method.
@@ -1052,11 +1064,14 @@ class Repo(BaseRepo):
 
 
     Attributes:
     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)
         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__(
     def __init__(
         self,
         self,
         root: str,
         root: str,
@@ -1065,11 +1080,11 @@ class Repo(BaseRepo):
     ) -> None:
     ) -> None:
         hidden_path = os.path.join(root, CONTROLDIR)
         hidden_path = os.path.join(root, CONTROLDIR)
         if bare is None:
         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
                 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
                 bare = True
             else:
             else:
                 raise NotGitRepository(
                 raise NotGitRepository(
@@ -1356,7 +1371,7 @@ class Repo(BaseRepo):
         from dulwich.index import (
         from dulwich.index import (
             IndexEntry,
             IndexEntry,
             _fs_to_tree_path,
             _fs_to_tree_path,
-            )
+        )
 
 
         index = self.open_index()
         index = self.open_index()
         try:
         try:
@@ -1375,7 +1390,7 @@ class Repo(BaseRepo):
                 tree_entry = self.object_store[tree_id].lookup_path(
                 tree_entry = self.object_store[tree_id].lookup_path(
                     self.object_store.__getitem__, tree_path)
                     self.object_store.__getitem__, tree_path)
             except KeyError:
             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
                 # remove index entry
                 try:
                 try:
                     del index[tree_path]
                     del index[tree_path]

+ 1 - 1
dulwich/server.py

@@ -1068,7 +1068,7 @@ class ReceivePackHandler(PackHandler):
         client_refs = []
         client_refs = []
         ref = self.proto.read_pkt_line()
         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:
         if ref is None:
             return
             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):
     def setUp(self):
         super(TestCase, self).setUp()
         super(TestCase, self).setUp()
         self._old_home = os.environ.get("HOME")
         self._old_home = os.environ.get("HOME")
-        os.environ["HOME"] = "/nonexistant"
+        os.environ["HOME"] = "/nonexistent"
         os.environ["GIT_CONFIG_NOSYSTEM"] = "1"
         os.environ["GIT_CONFIG_NOSYSTEM"] = "1"
 
 
     def tearDown(self):
     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.
 # Copyright (C) 2010 Google, Inc.
 #
 #
 # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
 # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
@@ -18,7 +18,7 @@
 # License, Version 2.0.
 # 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
 import copy
 from io import BytesIO
 from io import BytesIO
@@ -429,6 +429,11 @@ class DulwichTCPClientTest(CompatTestCase, DulwichClientTestBase):
         def test_fetch_pack_no_side_band_64k(self):
         def test_fetch_pack_no_side_band_64k(self):
             DulwichClientTestBase.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):
 class TestSSHVendor(object):
     @staticmethod
     @staticmethod

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

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

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

@@ -43,7 +43,9 @@ from dulwich.tests import (
 _DEFAULT_GIT = "git"
 _DEFAULT_GIT = "git"
 _VERSION_LEN = 4
 _VERSION_LEN = 4
 _REPOS_DATA_DIR = os.path.abspath(
 _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:
     Args:
       required_version: A tuple of ints of the form (major, minor, point,
       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
       git_path: Path to the git executable; defaults to the version in
         the system path.
         the system path.
     Raises:
     Raises:
@@ -137,6 +139,7 @@ def run_git(
 
 
     env = popen_kwargs.pop("env", {})
     env = popen_kwargs.pop("env", {})
     env["LC_ALL"] = env["LANG"] = "C"
     env["LC_ALL"] = env["LANG"] = "C"
+    env["PATH"] = os.getenv("PATH")
 
 
     args = [git_path] + args
     args = [git_path] + args
     popen_kwargs["stdin"] = subprocess.PIPE
     popen_kwargs["stdin"] = subprocess.PIPE

+ 0 - 29
dulwich/tests/test_client.py

@@ -52,7 +52,6 @@ from dulwich.client import (
     PLinkSSHVendor,
     PLinkSSHVendor,
     HangupException,
     HangupException,
     GitProtocolError,
     GitProtocolError,
-    apply_instead_of,
     check_wants,
     check_wants,
     default_urllib3_manager,
     default_urllib3_manager,
     get_credentials_from_store,
     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,
     _escape_value,
     _parse_string,
     _parse_string,
     parse_submodules,
     parse_submodules,
+    apply_instead_of,
 )
 )
 from dulwich.tests import (
 from dulwich.tests import (
     TestCase,
     TestCase,
@@ -103,6 +104,10 @@ class ConfigFileTests(TestCase):
         cf = self.from_file(b'[branch "foo#bar"] # a comment\nbar= foo\n')
         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)
         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):
     def test_from_file_section(self):
         cf = self.from_file(b"[core]\nfoo = bar\n")
         cf = self.from_file(b"[core]\nfoo = bar\n")
         self.assertEqual(b"bar", cf.get((b"core",), b"foo"))
         self.assertEqual(b"bar", cf.get((b"core",), b"foo"))
@@ -300,7 +305,7 @@ class StackedConfigTests(TestCase):
     def test_default_backends(self):
     def test_default_backends(self):
         StackedConfig.default_backends()
         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):
     def test_windows_config_from_path(self):
         from dulwich.config import get_win_system_paths
         from dulwich.config import get_win_system_paths
 
 
@@ -316,7 +321,7 @@ class StackedConfigTests(TestCase):
             paths,
             paths,
         )
         )
 
 
-    @skipIf(sys.platform != "win32", "Windows specfic config location.")
+    @skipIf(sys.platform != "win32", "Windows specific config location.")
     def test_windows_config_from_reg(self):
     def test_windows_config_from_reg(self):
         import winreg
         import winreg
 
 
@@ -428,3 +433,31 @@ class SubmodulesTests(TestCase):
             ],
             ],
             got,
             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(
         finder = GreenThreadsMissingObjectFinder(
             self.store, wants[0 : int(self.cmt_amount / 2)], wants
             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.sha_done), (self.cmt_amount / 2) * 2)
         self.assertEqual(len(finder.objects_to_send), self.cmt_amount / 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):
 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):
     def get_simple_index(self, name):
         return Index(os.path.join(self.datadir, 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 os
 import stat
 import stat
-import warnings
 from contextlib import contextmanager
 from contextlib import contextmanager
 
 
 from dulwich.errors import (
 from dulwich.errors import (
@@ -87,7 +86,7 @@ class BlobReadTests(TestCase):
     """Test decompression of blobs"""
     """Test decompression of blobs"""
 
 
     def get_sha_file(self, cls, base, sha):
     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))
         return cls.from_path(hex_to_filename(dir, sha))
 
 
     def get_blob(self, sha):
     def get_blob(self, sha):
@@ -840,17 +839,6 @@ class TreeTests(ShaFileCheckTests):
         self.assertEqual(x[b"myname"], (0o100755, myhexsha))
         self.assertEqual(x[b"myname"], (0o100755, myhexsha))
         self.assertEqual(b"100755 myname\0" + hex_to_sha(myhexsha), x.as_raw_string())
         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):
     def test_simple(self):
         myhexsha = b"d80c186a03f423a81b39df39dc87fd269736ca86"
         myhexsha = b"d80c186a03f423a81b39df39dc87fd269736ca86"
         x = Tree()
         x = Tree()
@@ -878,7 +866,7 @@ class TreeTests(ShaFileCheckTests):
         self.assertEqual(_SORTED_TREE_ITEMS, x.items())
         self.assertEqual(_SORTED_TREE_ITEMS, x.items())
 
 
     def _do_test_parse_tree(self, parse_tree):
     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))
         o = Tree.from_path(hex_to_filename(dir, tree_sha))
         self.assertEqual(
         self.assertEqual(
             [(b"a", 0o100644, a_sha), (b"b", 0o100644, b_sha)],
             [(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.tempdir = tempfile.mkdtemp()
         self.addCleanup(shutil.rmtree, self.tempdir)
         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):
     def get_pack_index(self, sha):
         """Returns a PackIndex from the datadir with the given 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,
     make_wsgi_chain,
 )
 )
 
 
+try:
+    import gpg
+except ImportError:
+    gpg = None
+
 
 
 def flat_walk_dir(dir_to_walk):
 def flat_walk_dir(dir_to_walk):
     for dirpath, _, filenames in os.walk(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)
         self.assertLess(time.time() - ts, 50)
 
 
 
 
+@skipIf(gpg is None, "gpg is not available")
 class PorcelainGpgTestCase(PorcelainTestCase):
 class PorcelainGpgTestCase(PorcelainTestCase):
     DEFAULT_KEY = """
     DEFAULT_KEY = """
 -----BEGIN PGP PRIVATE KEY BLOCK-----
 -----BEGIN PGP PRIVATE KEY BLOCK-----
@@ -271,7 +277,10 @@ ya6JVZCRbMXfdCy8lVPgtNQ6VlHaj8Wvnn2FLbWWO2n2r3s=
         super(PorcelainGpgTestCase, self).setUp()
         super(PorcelainGpgTestCase, self).setUp()
         self.gpg_dir = os.path.join(self.test_dir, "gpg")
         self.gpg_dir = os.path.join(self.test_dir, "gpg")
         os.mkdir(self.gpg_dir, mode=0o700)
         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")
         self._old_gnupghome = os.environ.get("GNUPGHOME")
         os.environ["GNUPGHOME"] = self.gpg_dir
         os.environ["GNUPGHOME"] = self.gpg_dir
         if self._old_gnupghome is None:
         if self._old_gnupghome is None:
@@ -412,6 +421,195 @@ class CommitTests(PorcelainTestCase):
         self.assertIsInstance(sha, bytes)
         self.assertIsInstance(sha, bytes)
         self.assertEqual(len(sha), 40)
         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):
 class CleanTests(PorcelainTestCase):
     def put_files(self, tracked, ignored, untracked, empty_dirs):
     def put_files(self, tracked, ignored, untracked, empty_dirs):
@@ -647,7 +845,7 @@ class CloneTests(PorcelainTestCase):
         with tempfile.TemporaryDirectory() as parent:
         with tempfile.TemporaryDirectory() as parent:
             target_path = os.path.join(parent, "target")
             target_path = os.path.join(parent, "target")
             self.assertRaises(
             self.assertRaises(
-                Exception, porcelain.clone, "/nonexistant/repo", target_path
+                Exception, porcelain.clone, "/nonexistent/repo", target_path
             )
             )
             self.assertFalse(os.path.exists(target_path))
             self.assertFalse(os.path.exists(target_path))
 
 
@@ -1128,8 +1326,6 @@ class RevListTests(PorcelainTestCase):
 class TagCreateSignTests(PorcelainGpgTestCase):
 class TagCreateSignTests(PorcelainGpgTestCase):
 
 
     def test_default_key(self):
     def test_default_key(self):
-        import gpg
-
         c1, c2, c3 = build_commit_graph(
         c1, c2, c3 = build_commit_graph(
             self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
             self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
         )
         )
@@ -1408,6 +1604,28 @@ class ResetFileTests(PorcelainTestCase):
             self.assertEqual('hello', f.read())
             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):
 class PushTests(PorcelainTestCase):
     def test_simple(self):
     def test_simple(self):
         """
         """
@@ -1897,6 +2115,16 @@ class StatusTests(PorcelainTestCase):
         with self.assertRaises(ValueError):
         with self.assertRaises(ValueError):
             porcelain.status(self.repo.path, untracked_files="antani")
             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):
     def test_status_crlf_mismatch(self):
         # First make a commit as if the file has been added on a Linux system
         # First make a commit as if the file has been added on a Linux system
         # or with core.autocrlf=True
         # 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 (
 from dulwich.refs import (
     DictRefsContainer,
     DictRefsContainer,
     InfoRefsContainer,
     InfoRefsContainer,
+    SymrefLoop,
     check_ref_format,
     check_ref_format,
     _split_ref_line,
     _split_ref_line,
     parse_symref_value,
     parse_symref_value,
@@ -246,9 +247,9 @@ class RefsContainerTests(object):
         self.assertEqual(nines, self._refs[b"refs/heads/master"])
         self.assertEqual(nines, self._refs[b"refs/heads/master"])
 
 
         self.assertTrue(
         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):
     def test_add_if_new(self):
         nines = b"9" * 40
         nines = b"9" * 40
@@ -518,7 +519,7 @@ class DiskRefsContainerTests(RefsContainerTests, TestCase):
             ),
             ),
             self._refs.follow(b"refs/heads/master"),
             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):
     def test_delitem(self):
         RefsContainerTests.test_delitem(self)
         RefsContainerTests.test_delitem(self)
@@ -622,7 +623,7 @@ class DiskRefsContainerTests(RefsContainerTests, TestCase):
             b"42d06bd4b77fed026b154d16493e5deab78f02ec",
             b"42d06bd4b77fed026b154d16493e5deab78f02ec",
             self._refs.read_ref(b"refs/heads/packed"),
             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):
     def test_read_loose_ref(self):
         self._refs[b"refs/heads/foo"] = b"df6800012397fb85c56e7418dd4eb9405dee075c"
         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):
     def test_clone_no_head(self):
         temp_dir = self.mkdtemp()
         temp_dir = self.mkdtemp()
         self.addCleanup(shutil.rmtree, temp_dir)
         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")
         dest_dir = os.path.join(temp_dir, "a.git")
         shutil.copytree(os.path.join(repo_dir, "a.git"), dest_dir, symlinks=True)
         shutil.copytree(os.path.join(repo_dir, "a.git"), dest_dir, symlinks=True)
         r = Repo(dest_dir)
         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:
     if temp_dir is None:
         temp_dir = tempfile.mkdtemp()
         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)
     temp_repo_dir = os.path.join(temp_dir, name)
     shutil.copytree(repo_dir, temp_repo_dir, symlinks=True)
     shutil.copytree(repo_dir, temp_repo_dir, symlinks=True)
     return Repo(temp_repo_dir)
     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
 #!/usr/bin/python3
 # encoding: utf-8
 # encoding: utf-8
 # Setup file for dulwich
 # 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 io
 import os
 import os
 import sys
 import sys
-from typing import Dict, Any
 
 
 
 
 if sys.version_info < (3, 6):
 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')
         '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):
 class DulwichDistribution(Distribution):
@@ -53,8 +45,8 @@ if sys.platform == 'darwin' and os.path.exists('/usr/bin/xcodebuild'):
     for line in out.splitlines():
     for line in out.splitlines():
         line = line.decode("utf8")
         line = line.decode("utf8")
         # Also parse only first digit, because 3.2.1 can't be parsed nicely
         # 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'] = ''
             os.environ['ARCHFLAGS'] = ''
 
 
 tests_require = ['fastimport']
 tests_require = ['fastimport']
@@ -71,26 +63,7 @@ ext_modules = [
     Extension('dulwich._diff_tree', ['dulwich/_diff_tree.c']),
     Extension('dulwich._diff_tree', ['dulwich/_diff_tree.c']),
 ]
 ]
 
 
-setup_kwargs = {}  # type: Dict[str, Any]
 scripts = ['bin/dul-receive-pack', 'bin/dul-upload-pack']
 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"),
 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",
           "GitHub": "https://github.com/dulwich/dulwich",
       },
       },
       keywords="git vcs",
       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']},
       package_data={'': ['../docs/tutorial/*.txt', 'py.typed']},
       scripts=scripts,
       scripts=scripts,
       ext_modules=ext_modules,
       ext_modules=ext_modules,
       zip_safe=False,
       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=[
       classifiers=[
           'Development Status :: 4 - Beta',
           'Development Status :: 4 - Beta',
           'License :: OSI Approved :: Apache Software License',
           'License :: OSI Approved :: Apache Software License',
@@ -126,11 +107,16 @@ setup(name='dulwich',
           'Programming Language :: Python :: 3.8',
           'Programming Language :: Python :: 3.8',
           'Programming Language :: Python :: 3.9',
           'Programming Language :: Python :: 3.9',
           'Programming Language :: Python :: 3.10',
           'Programming Language :: Python :: 3.10',
+          'Programming Language :: Python :: 3.11',
           'Programming Language :: Python :: Implementation :: CPython',
           'Programming Language :: Python :: Implementation :: CPython',
           'Programming Language :: Python :: Implementation :: PyPy',
           'Programming Language :: Python :: Implementation :: PyPy',
           'Operating System :: POSIX',
           'Operating System :: POSIX',
           'Operating System :: Microsoft :: Windows',
           'Operating System :: Microsoft :: Windows',
           'Topic :: Software Development :: Version Control',
           '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:
 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


部分文件因文件數量過多而無法顯示