repligit 0.2.0__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. {repligit-0.2.0 → repligit-0.4.0}/.envrc +1 -1
  2. {repligit-0.2.0 → repligit-0.4.0}/.github/dependabot.yml +6 -2
  3. {repligit-0.2.0 → repligit-0.4.0}/.github/workflows/ci.yml +2 -2
  4. {repligit-0.2.0 → repligit-0.4.0}/.github/workflows/coverage.yml +6 -6
  5. {repligit-0.2.0 → repligit-0.4.0}/.github/workflows/label.yml +1 -1
  6. {repligit-0.2.0 → repligit-0.4.0}/.github/workflows/publish.yml +6 -5
  7. repligit-0.4.0/.github/workflows/requirements/publish.txt +3 -0
  8. repligit-0.4.0/.github/workflows/requirements/security.txt +1 -0
  9. repligit-0.4.0/.github/workflows/requirements/style.txt +3 -0
  10. repligit-0.4.0/.github/workflows/requirements/unit-tests.txt +6 -0
  11. {repligit-0.2.0 → repligit-0.4.0}/.github/workflows/security.yml +6 -6
  12. {repligit-0.2.0 → repligit-0.4.0}/.github/workflows/style.yml +11 -3
  13. {repligit-0.2.0 → repligit-0.4.0}/.github/workflows/unit-tests.yml +4 -4
  14. {repligit-0.2.0 → repligit-0.4.0}/PKG-INFO +11 -5
  15. {repligit-0.2.0 → repligit-0.4.0}/README.md +2 -3
  16. {repligit-0.2.0 → repligit-0.4.0}/examples/git_mirror.py +5 -1
  17. {repligit-0.2.0 → repligit-0.4.0}/examples/git_mirror_async.py +5 -1
  18. {repligit-0.2.0 → repligit-0.4.0}/pyproject.toml +10 -3
  19. {repligit-0.2.0 → repligit-0.4.0}/spack/repos/spack_repo/repligit/packages/py_repligit/package.py +1 -0
  20. {repligit-0.2.0/spack → repligit-0.4.0}/spack.yaml +6 -4
  21. {repligit-0.2.0 → repligit-0.4.0}/src/repligit/__init__.py +1 -1
  22. {repligit-0.2.0 → repligit-0.4.0}/src/repligit/asyncio/__init__.py +1 -1
  23. repligit-0.4.0/src/repligit/asyncio/client.py +130 -0
  24. repligit-0.4.0/src/repligit/asyncio/parse.py +88 -0
  25. repligit-0.4.0/src/repligit/client.py +168 -0
  26. repligit-0.4.0/src/repligit/exceptions.py +18 -0
  27. repligit-0.4.0/src/repligit/parse.py +172 -0
  28. repligit-0.4.0/tests/test_client.py +217 -0
  29. repligit-0.2.0/.github/workflows/requirements/publish.txt +0 -2
  30. repligit-0.2.0/.github/workflows/requirements/security.txt +0 -1
  31. repligit-0.2.0/.github/workflows/requirements/style.txt +0 -2
  32. repligit-0.2.0/.github/workflows/requirements/unit-tests.txt +0 -5
  33. repligit-0.2.0/src/repligit/asyncio/client.py +0 -95
  34. repligit-0.2.0/src/repligit/asyncio/parse.py +0 -51
  35. repligit-0.2.0/src/repligit/client.py +0 -114
  36. repligit-0.2.0/src/repligit/parse.py +0 -103
  37. repligit-0.2.0/tests/test_client.py +0 -63
  38. {repligit-0.2.0 → repligit-0.4.0}/.github/ISSUE_TEMPLATE/issue-feature-request.md +0 -0
  39. {repligit-0.2.0 → repligit-0.4.0}/.github/labeler.yml +0 -0
  40. {repligit-0.2.0 → repligit-0.4.0}/.gitignore +0 -0
  41. {repligit-0.2.0 → repligit-0.4.0}/LICENSE +0 -0
  42. {repligit-0.2.0 → repligit-0.4.0}/spack/repos/spack_repo/repligit/repo.yaml +0 -0
  43. {repligit-0.2.0 → repligit-0.4.0}/tests/__init__.py +0 -0
@@ -6,5 +6,5 @@
6
6
  #------------------------------------------------------------------------
7
7
  if type spack &>/dev/null; then
8
8
  . $SPACK_ROOT/share/spack/setup-env.sh
9
- spack env activate -d spack/
9
+ spack env activate -d .
10
10
  fi
@@ -3,9 +3,13 @@ updates:
3
3
  - package-ecosystem: "github-actions"
4
4
  directory: "/"
5
5
  schedule:
6
- interval: "weekly"
6
+ interval: "monthly"
7
+ groups:
8
+ codeql:
9
+ patterns:
10
+ - "github/codeql-action*"
7
11
 
8
12
  - package-ecosystem: "pip"
9
13
  directory: "/.github/workflows/requirements"
10
14
  schedule:
11
- interval: "weekly"
15
+ interval: "monthly"
@@ -22,13 +22,13 @@ jobs:
22
22
  security: ${{ steps.filter.outputs.security }}
23
23
  unit-tests: ${{ steps.filter.outputs.unit-tests }}
24
24
  steps:
25
- - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # @v2
25
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # @v2
26
26
  if: ${{ github.event_name == 'push' }}
27
27
  with:
28
28
  fetch-depth: 0
29
29
 
30
30
  # For pull requests it's not necessary to checkout the code
31
- - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36
31
+ - uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706
32
32
  id: filter
33
33
  with:
34
34
  filters: |
@@ -13,10 +13,10 @@ jobs:
13
13
  runs-on: ubuntu-latest
14
14
  steps:
15
15
  - name: checkout PR branch
16
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3
16
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
17
17
  with:
18
18
  fetch-depth: 2 # https://stackoverflow.com/a/74268200
19
- - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548
19
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
20
20
  with:
21
21
  python-version: '3.13'
22
22
  cache: 'pip'
@@ -26,14 +26,14 @@ jobs:
26
26
 
27
27
  - name: Install Python dependencies
28
28
  run: |
29
- pip install .
29
+ pip install .[aiohttp]
30
30
  pip install -r .github/workflows/requirements/unit-tests.txt
31
31
 
32
32
  - name: Download PR coverage artifact
33
33
  if: github.event_name == 'pull_request'
34
34
  id: download_pr_cov
35
35
  continue-on-error: true
36
- uses: actions/download-artifact@v6
36
+ uses: actions/download-artifact@v8.0.1
37
37
  with:
38
38
  name: pr-coverage-${{ github.sha }}
39
39
 
@@ -64,7 +64,7 @@ jobs:
64
64
  if: github.event_name == 'pull_request'
65
65
  id: download_base
66
66
  continue-on-error: true
67
- uses: actions/download-artifact@v6
67
+ uses: actions/download-artifact@v8.0.1
68
68
  with:
69
69
  name: base-coverage-${{ env.MERGE_BASE_COMMIT }}
70
70
 
@@ -110,7 +110,7 @@ jobs:
110
110
 
111
111
  - name: Upload base coverage artifact
112
112
  if: github.ref_name == 'main' && github.event_name == 'push'
113
- uses: actions/upload-artifact@v5
113
+ uses: actions/upload-artifact@v7
114
114
  with:
115
115
  name: base-coverage-${{ github.sha }}
116
116
  path: base_coverage.json
@@ -18,4 +18,4 @@ jobs:
18
18
  contents: read
19
19
  pull-requests: write
20
20
  steps:
21
- - uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b
21
+ - uses: actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13
@@ -7,12 +7,13 @@ jobs:
7
7
  build:
8
8
  runs-on: ubuntu-latest
9
9
  steps:
10
- - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3
10
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
11
11
  with:
12
12
  persist-credentials: false
13
+ fetch-depth: 0
13
14
 
14
15
  - name: Set up Python
15
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548
16
+ uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
16
17
  with:
17
18
  cache: 'pip'
18
19
  cache-dependency-path: '.github/workflows/requirements/publish.txt'
@@ -26,7 +27,7 @@ jobs:
26
27
  python3 -m build
27
28
 
28
29
  - name: Save distribution files as artifact
29
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
30
+ uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f
30
31
  with:
31
32
  name: python-package-distributions
32
33
  path: dist/
@@ -41,10 +42,10 @@ jobs:
41
42
  id-token: write
42
43
  steps:
43
44
  - name: Download distribution files from artifact
44
- uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53
45
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
45
46
  with:
46
47
  name: python-package-distributions
47
48
  path: dist/
48
49
 
49
50
  - name: Publish package to PyPI
50
- uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e
51
+ uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33
@@ -0,0 +1,3 @@
1
+ hatchling==1.31.0
2
+ hatch-vcs==0.5.0
3
+ build==1.5.0
@@ -0,0 +1 @@
1
+ bandit[sarif,toml]==1.9.4
@@ -0,0 +1,3 @@
1
+ ruff==0.15.8
2
+ ty==0.0.65
3
+ typos==1.48.0
@@ -0,0 +1,6 @@
1
+ hatchling==1.31.0
2
+ hatch-vcs==0.5.0
3
+ build==1.5.0
4
+ pytest==9.0.2
5
+ pytest-mock==3.15.1
6
+ coverage==7.15.2
@@ -10,11 +10,11 @@ jobs:
10
10
  bandit-scan:
11
11
  runs-on: ubuntu-latest
12
12
  steps:
13
- - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3
13
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
14
14
 
15
15
  # https://bandit.readthedocs.io/en/latest/faq.html#under-which-version-of-python-should-i-install-bandit
16
16
  - name: Set up Python 3.10
17
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548
17
+ uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
18
18
  with:
19
19
  python-version: '3.10'
20
20
  cache: 'pip'
@@ -30,7 +30,7 @@ jobs:
30
30
  # the PR if any issues are found
31
31
  - name: Upload SARIF file
32
32
  if: always()
33
- uses: github/codeql-action/upload-sarif@fe4161a26a8629af62121b670040955b330f9af2
33
+ uses: github/codeql-action/upload-sarif@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81
34
34
  with:
35
35
  sarif_file: results.sarif
36
36
  category: bandit
@@ -43,15 +43,15 @@ jobs:
43
43
  language: [actions, python]
44
44
  steps:
45
45
  - name: Checkout repository
46
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3
46
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
47
47
 
48
48
  - name: Initialize CodeQL
49
- uses: github/codeql-action/init@fe4161a26a8629af62121b670040955b330f9af2
49
+ uses: github/codeql-action/init@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81
50
50
  with:
51
51
  languages: ${{ matrix.language }}
52
52
  build-mode: none
53
53
 
54
54
  - name: Perform CodeQL Analysis
55
- uses: github/codeql-action/analyze@fe4161a26a8629af62121b670040955b330f9af2
55
+ uses: github/codeql-action/analyze@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81
56
56
  with:
57
57
  category: "/language:${{matrix.language}}"
@@ -9,18 +9,23 @@ jobs:
9
9
  lint:
10
10
  runs-on: ubuntu-latest
11
11
  steps:
12
- - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3
12
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
13
13
 
14
14
  - name: Set up Python 3.10
15
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548
15
+ uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
16
16
  with:
17
17
  python-version: '3.10'
18
18
  cache: 'pip'
19
- cache-dependency-path: '.github/workflows/requirements/style.txt'
19
+ cache-dependency-path: |
20
+ pyproject.toml
21
+ .github/workflows/requirements/unit-tests.txt
22
+ .github/workflows/requirements/style.txt
20
23
 
21
24
  - name: Install Python dependencies
22
25
  run: |
26
+ pip install -r .github/workflows/requirements/unit-tests.txt
23
27
  pip install -r .github/workflows/requirements/style.txt
28
+ pip install -e .[aiohttp]
24
29
 
25
30
  - name: Run Ruff
26
31
  run: |
@@ -28,5 +33,8 @@ jobs:
28
33
  ruff check --select I --diff
29
34
  ruff format --check --diff
30
35
 
36
+ - name: Run Type Checker
37
+ run: ty check
38
+
31
39
  - name: Run Typos
32
40
  run: typos
@@ -12,8 +12,8 @@ jobs:
12
12
  matrix:
13
13
  python-version: ['3.10', '3.13']
14
14
  steps:
15
- - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3
16
- - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548
15
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
16
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
17
17
  with:
18
18
  python-version: ${{ matrix.python-version }}
19
19
  cache: 'pip'
@@ -23,8 +23,8 @@ jobs:
23
23
 
24
24
  - name: Install Python dependencies
25
25
  run: |
26
- pip install .
27
26
  pip install -r .github/workflows/requirements/unit-tests.txt
27
+ pip install -e .[aiohttp]
28
28
 
29
29
  - name: Run Unit Tests (coverage on 3.13)
30
30
  run: |
@@ -41,7 +41,7 @@ jobs:
41
41
  - name: Upload coverage artifact
42
42
  # this is needed so we can compare diffs in coverage.yml
43
43
  if: ${{ matrix.python-version == '3.13' }}
44
- uses: actions/upload-artifact@v5
44
+ uses: actions/upload-artifact@v7
45
45
  with:
46
46
  name: pr-coverage-${{ github.sha }}
47
47
  path: pr_coverage.json
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: repligit
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: A python library implementing the git http transfer protocol.
5
5
  Project-URL: Homepage, https://github.com/llnl/repligit
6
6
  Project-URL: Issues, https://github.com/llnl/repligit/issues
@@ -12,6 +12,13 @@ Classifier: Programming Language :: Python :: 3
12
12
  Requires-Python: >=3.10
13
13
  Provides-Extra: aiohttp
14
14
  Requires-Dist: aiohttp; extra == 'aiohttp'
15
+ Provides-Extra: dev
16
+ Requires-Dist: coverage; extra == 'dev'
17
+ Requires-Dist: pytest; extra == 'dev'
18
+ Requires-Dist: pytest-mock; extra == 'dev'
19
+ Requires-Dist: ruff; extra == 'dev'
20
+ Requires-Dist: ty; extra == 'dev'
21
+ Requires-Dist: typos; extra == 'dev'
15
22
  Description-Content-Type: text/markdown
16
23
 
17
24
  # repligit
@@ -57,7 +64,7 @@ def main():
57
64
  gl_refs = ls_remote(
58
65
  dest_remote_url,
59
66
  username=dest_username,
60
- password=dest_password
67
+ password=dest_password,
61
68
  )
62
69
 
63
70
  want_sha = gh_refs[target_ref]
@@ -65,7 +72,7 @@ def main():
65
72
 
66
73
  from_sha = gl_refs.get(target_ref) or ("0" * 40)
67
74
 
68
- if want_sha in have_shas:
75
+ if from_sha == want_sha:
69
76
  print("Everything is up to date")
70
77
  return
71
78
 
@@ -92,7 +99,6 @@ def main():
92
99
 
93
100
  if __name__ == "__main__":
94
101
  main()
95
-
96
102
  ```
97
103
 
98
104
  ## License
@@ -41,7 +41,7 @@ def main():
41
41
  gl_refs = ls_remote(
42
42
  dest_remote_url,
43
43
  username=dest_username,
44
- password=dest_password
44
+ password=dest_password,
45
45
  )
46
46
 
47
47
  want_sha = gh_refs[target_ref]
@@ -49,7 +49,7 @@ def main():
49
49
 
50
50
  from_sha = gl_refs.get(target_ref) or ("0" * 40)
51
51
 
52
- if want_sha in have_shas:
52
+ if from_sha == want_sha:
53
53
  print("Everything is up to date")
54
54
  return
55
55
 
@@ -76,7 +76,6 @@ def main():
76
76
 
77
77
  if __name__ == "__main__":
78
78
  main()
79
-
80
79
  ```
81
80
 
82
81
  ## License
@@ -27,7 +27,7 @@ def main():
27
27
 
28
28
  from_sha = gl_refs.get(target_ref) or ("0" * 40)
29
29
 
30
- if want_sha in have_shas:
30
+ if from_sha == want_sha:
31
31
  print("Everything is up to date")
32
32
  return
33
33
 
@@ -40,6 +40,10 @@ def main():
40
40
  # password=src_password, # Uncomment if source repo requires auth
41
41
  )
42
42
 
43
+ if packfile is None:
44
+ print("Failed to fetch packfile")
45
+ return
46
+
43
47
  # Upload packfile to destination repository
44
48
  send_pack(
45
49
  dest_remote_url,
@@ -31,7 +31,7 @@ async def main():
31
31
 
32
32
  from_sha = gl_refs.get(target_ref) or ("0" * 40)
33
33
 
34
- if want_sha in have_shas:
34
+ if from_sha == want_sha:
35
35
  print("Everything is up to date")
36
36
  return
37
37
 
@@ -44,6 +44,10 @@ async def main():
44
44
  # password=src_password, # Uncomment if source repo requires auth
45
45
  )
46
46
 
47
+ if packfile is None:
48
+ print("Failed to fetch packfile")
49
+ return
50
+
47
51
  # Upload packfile to destination repository
48
52
  await send_pack(
49
53
  dest_remote_url,
@@ -1,11 +1,10 @@
1
1
  [build-system]
2
- requires = ["hatchling"]
2
+ requires = ["hatchling", "hatch-vcs"]
3
3
  build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "repligit"
7
- version = "0.2.0"
8
- dependencies = []
7
+ dynamic = ["version"]
9
8
  authors = [
10
9
  { name="Alec Scott" },
11
10
  { name="Caetano Melone"},
@@ -25,6 +24,10 @@ Issues = "https://github.com/llnl/repligit/issues"
25
24
 
26
25
  [project.optional-dependencies]
27
26
  aiohttp = ["aiohttp"]
27
+ dev = ["ty", "ruff", "typos", "coverage", "pytest", "pytest-mock"]
28
+
29
+ [tool.hatch.version]
30
+ source = "vcs"
28
31
 
29
32
  [tool.pytest.ini_options]
30
33
  pythonpath = [
@@ -39,3 +42,7 @@ exclude = ["spack"]
39
42
  exclude_dirs = []
40
43
  # allow use of assert
41
44
  skips = ["B101"]
45
+
46
+ [tool.ty.src]
47
+ # exclude spack directory from type checking
48
+ exclude = ["spack/**"]
@@ -28,5 +28,6 @@ class PyRepligit(PythonPackage):
28
28
 
29
29
  depends_on("python@3.10:", type=("build", "run"))
30
30
  depends_on("py-hatchling", type="build")
31
+ depends_on("py-hatch-vcs", type="build", when="@main")
31
32
 
32
33
  depends_on("py-aiohttp", type=("build", "run"), when="+aiohttp")
@@ -3,20 +3,22 @@
3
3
  # It describes a set of packages to be installed, along with
4
4
  # configuration settings.
5
5
  spack:
6
- # add package specs to the `specs` list
7
6
  repos:
8
- - $env/repos/spack_repo/repligit
7
+ repligit: spack/repos/spack_repo/repligit
9
8
  specs:
10
9
  - py-codespell
11
10
  - py-pytest
12
- - py-repligit
11
+ - py-repligit +aiohttp
13
12
  - py-bandit
14
13
  - py-ruff
15
14
  - py-pip
15
+ - py-ty
16
+ - py-build
17
+ - py-hatch-vcs
16
18
  view: true
17
19
  concretizer:
18
20
  unify: true
19
21
  develop:
20
22
  py-repligit:
21
23
  spec: py-repligit@=main
22
- path: $env/../
24
+ path: .
@@ -1,3 +1,3 @@
1
1
  from repligit.client import fetch_pack, ls_remote, send_pack
2
2
 
3
- __all__ = ["ls_remote", "fetch_pack", "send_pack"]
3
+ __all__ = ["fetch_pack", "ls_remote", "send_pack"]
@@ -5,4 +5,4 @@ except ModuleNotFoundError:
5
5
 
6
6
  from repligit.asyncio.client import fetch_pack, ls_remote, send_pack
7
7
 
8
- __all__ = ["ls_remote", "fetch_pack", "send_pack"]
8
+ __all__ = ["fetch_pack", "ls_remote", "send_pack"]
@@ -0,0 +1,130 @@
1
+ from typing import Iterable
2
+
3
+ import aiohttp
4
+
5
+ from repligit.asyncio.parse import read_pkt_lines
6
+ from repligit.exceptions import (
7
+ RefUpdateRejected,
8
+ RemoteError,
9
+ UnexpectedResponse,
10
+ UnpackFailed,
11
+ )
12
+ from repligit.parse import generate_fetch_pack_request, generate_send_pack_header
13
+
14
+
15
+ async def ls_remote(
16
+ url: str, username: str | None = None, password: str | None = None
17
+ ) -> dict[str, str]:
18
+ """Get commit hash of remote master branch, return SHA-1 hex string or
19
+ None if no remote commits.
20
+ """
21
+
22
+ url = f"{url}/info/refs?service=git-upload-pack"
23
+ auth = aiohttp.BasicAuth(username or "", password) if password else None
24
+ async with aiohttp.ClientSession(auth=auth) as session:
25
+ async with session.get(url, raise_for_status=True) as resp:
26
+ lines = read_pkt_lines(resp.content)
27
+ service_line = await anext(lines)
28
+ if service_line != "# service=git-upload-pack":
29
+ raise UnexpectedResponse(f"invalid service line: {service_line!r}")
30
+
31
+ refs: dict[str, str] = {}
32
+ async for line in lines:
33
+ # The first ref line carries the server capabilities after a NUL byte.
34
+ sha, ref = line.split("\x00", 1)[0].split()
35
+ refs[ref] = sha
36
+ return refs
37
+
38
+
39
+ async def fetch_pack(
40
+ url: str,
41
+ want_sha: str,
42
+ have_shas: Iterable[str],
43
+ username: str | None = None,
44
+ password: str | None = None,
45
+ ) -> bytes | None:
46
+ """Download a packfile from a remote server."""
47
+ # ensure have_shas is a set, else packfile errors will occur
48
+ have_shas = set(have_shas)
49
+
50
+ url = f"{url}/git-upload-pack"
51
+ auth = aiohttp.BasicAuth(username or "", password) if password else None
52
+
53
+ request = generate_fetch_pack_request(want_sha, have_shas)
54
+
55
+ async with aiohttp.ClientSession(auth=auth) as session:
56
+ async with session.post(
57
+ url,
58
+ headers={
59
+ "Content-type": "application/x-git-upload-pack-request",
60
+ },
61
+ data=request,
62
+ raise_for_status=True,
63
+ timeout=None,
64
+ ) as resp:
65
+ # Consume negotiation pkt-lines. With multi-ack the server may
66
+ # send several "ACK <sha> continue|common|ready" lines before the
67
+ # terminating "NAK" or final "ACK <sha>", then the packfile.
68
+ while True:
69
+ length_bytes = await resp.content.readexactly(4)
70
+ line_length = int(length_bytes, 16)
71
+
72
+ line = await resp.content.readexactly(line_length - 4)
73
+
74
+ prefix = line[:3]
75
+
76
+ # e.g. "ERR upload-pack: not our ref <sha>"
77
+ if prefix == b"ERR":
78
+ raise RemoteError(line.decode("utf-8").strip())
79
+
80
+ if prefix not in (b"NAK", b"ACK"):
81
+ return None
82
+ if prefix == b"NAK" or len(line.split()) == 2:
83
+ # "NAK" or final "ACK <sha>": negotiation done, packfile
84
+ # follows. Unlike the sync API, the body must be read
85
+ # within this context to be usable by the caller.
86
+ return await resp.content.read()
87
+ # intermediate multi-ack line: "ACK <sha> continue|common|ready"
88
+
89
+
90
+ async def send_pack(
91
+ url: str,
92
+ ref: str,
93
+ from_sha: str,
94
+ to_sha: str,
95
+ packfile: bytes,
96
+ username: str | None = None,
97
+ password: str | None = None,
98
+ ) -> None:
99
+ """Send a packfile to a remote server."""
100
+ url = f"{url}/git-receive-pack"
101
+ auth = aiohttp.BasicAuth(username or "", password) if password else None
102
+
103
+ header = generate_send_pack_header(ref, from_sha, to_sha)
104
+ # unlike in the sync version the packfile is already read into memory
105
+ receive_pack_request = header + packfile
106
+
107
+ async with aiohttp.ClientSession(auth=auth) as session:
108
+ async with session.post(
109
+ url,
110
+ headers={
111
+ "Content-type": "application/x-git-receive-pack-request",
112
+ },
113
+ data=receive_pack_request,
114
+ raise_for_status=True,
115
+ ) as resp:
116
+ lines = read_pkt_lines(resp.content)
117
+ unpack_status = await anext(lines)
118
+ if unpack_status != "unpack ok":
119
+ raise UnpackFailed(unpack_status)
120
+
121
+ # "ng <ref> <reason>" (ng = not good) means the remote rejected the
122
+ # update. The reason may be non-fast-forward, hook declined, etc.
123
+ ref_status = await anext(lines)
124
+ prefix = f"ng {ref} "
125
+ if ref_status.startswith(prefix):
126
+ reason_str = ref_status[len(prefix) :]
127
+ raise RefUpdateRejected(reason_str)
128
+
129
+ if ref_status != f"ok {ref}":
130
+ raise UnexpectedResponse(f"unexpected ref status line: {ref_status!r}")