repligit 0.2.0__tar.gz → 0.3.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.
- {repligit-0.2.0 → repligit-0.3.0}/.envrc +1 -1
- {repligit-0.2.0 → repligit-0.3.0}/.github/dependabot.yml +6 -2
- {repligit-0.2.0 → repligit-0.3.0}/.github/workflows/ci.yml +2 -2
- {repligit-0.2.0 → repligit-0.3.0}/.github/workflows/coverage.yml +5 -5
- {repligit-0.2.0 → repligit-0.3.0}/.github/workflows/label.yml +1 -1
- {repligit-0.2.0 → repligit-0.3.0}/.github/workflows/publish.yml +5 -5
- repligit-0.3.0/.github/workflows/requirements/publish.txt +2 -0
- repligit-0.3.0/.github/workflows/requirements/security.txt +1 -0
- repligit-0.3.0/.github/workflows/requirements/style.txt +3 -0
- repligit-0.3.0/.github/workflows/requirements/unit-tests.txt +5 -0
- {repligit-0.2.0 → repligit-0.3.0}/.github/workflows/security.yml +6 -6
- {repligit-0.2.0 → repligit-0.3.0}/.github/workflows/style.yml +6 -2
- {repligit-0.2.0 → repligit-0.3.0}/.github/workflows/unit-tests.yml +3 -3
- {repligit-0.2.0 → repligit-0.3.0}/PKG-INFO +8 -1
- {repligit-0.2.0 → repligit-0.3.0}/examples/git_mirror.py +4 -0
- {repligit-0.2.0 → repligit-0.3.0}/examples/git_mirror_async.py +4 -0
- {repligit-0.2.0 → repligit-0.3.0}/pyproject.toml +6 -1
- {repligit-0.2.0/spack → repligit-0.3.0}/spack.yaml +3 -3
- {repligit-0.2.0 → repligit-0.3.0}/src/repligit/asyncio/client.py +45 -16
- {repligit-0.2.0 → repligit-0.3.0}/src/repligit/asyncio/parse.py +1 -1
- {repligit-0.2.0 → repligit-0.3.0}/src/repligit/client.py +54 -17
- repligit-0.3.0/src/repligit/exceptions.py +18 -0
- {repligit-0.2.0 → repligit-0.3.0}/src/repligit/parse.py +4 -4
- {repligit-0.2.0 → repligit-0.3.0}/tests/test_client.py +6 -6
- repligit-0.2.0/.github/workflows/requirements/publish.txt +0 -2
- repligit-0.2.0/.github/workflows/requirements/security.txt +0 -1
- repligit-0.2.0/.github/workflows/requirements/style.txt +0 -2
- repligit-0.2.0/.github/workflows/requirements/unit-tests.txt +0 -5
- {repligit-0.2.0 → repligit-0.3.0}/.github/ISSUE_TEMPLATE/issue-feature-request.md +0 -0
- {repligit-0.2.0 → repligit-0.3.0}/.github/labeler.yml +0 -0
- {repligit-0.2.0 → repligit-0.3.0}/.gitignore +0 -0
- {repligit-0.2.0 → repligit-0.3.0}/LICENSE +0 -0
- {repligit-0.2.0 → repligit-0.3.0}/README.md +0 -0
- {repligit-0.2.0 → repligit-0.3.0}/spack/repos/spack_repo/repligit/packages/py_repligit/package.py +0 -0
- {repligit-0.2.0 → repligit-0.3.0}/spack/repos/spack_repo/repligit/repo.yaml +0 -0
- {repligit-0.2.0 → repligit-0.3.0}/src/repligit/__init__.py +0 -0
- {repligit-0.2.0 → repligit-0.3.0}/src/repligit/asyncio/__init__.py +0 -0
- {repligit-0.2.0 → repligit-0.3.0}/tests/__init__.py +0 -0
|
@@ -3,9 +3,13 @@ updates:
|
|
|
3
3
|
- package-ecosystem: "github-actions"
|
|
4
4
|
directory: "/"
|
|
5
5
|
schedule:
|
|
6
|
-
interval: "
|
|
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: "
|
|
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@
|
|
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@
|
|
31
|
+
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d
|
|
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@
|
|
16
|
+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
|
17
17
|
with:
|
|
18
18
|
fetch-depth: 2 # https://stackoverflow.com/a/74268200
|
|
19
|
-
- uses: actions/setup-python@
|
|
19
|
+
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
|
|
20
20
|
with:
|
|
21
21
|
python-version: '3.13'
|
|
22
22
|
cache: 'pip'
|
|
@@ -33,7 +33,7 @@ jobs:
|
|
|
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@
|
|
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@
|
|
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@
|
|
113
|
+
uses: actions/upload-artifact@v7
|
|
114
114
|
with:
|
|
115
115
|
name: base-coverage-${{ github.sha }}
|
|
116
116
|
path: base_coverage.json
|
|
@@ -7,12 +7,12 @@ jobs:
|
|
|
7
7
|
build:
|
|
8
8
|
runs-on: ubuntu-latest
|
|
9
9
|
steps:
|
|
10
|
-
- uses: actions/checkout@
|
|
10
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
|
11
11
|
with:
|
|
12
12
|
persist-credentials: false
|
|
13
13
|
|
|
14
14
|
- name: Set up Python
|
|
15
|
-
uses: actions/setup-python@
|
|
15
|
+
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
|
|
16
16
|
with:
|
|
17
17
|
cache: 'pip'
|
|
18
18
|
cache-dependency-path: '.github/workflows/requirements/publish.txt'
|
|
@@ -26,7 +26,7 @@ jobs:
|
|
|
26
26
|
python3 -m build
|
|
27
27
|
|
|
28
28
|
- name: Save distribution files as artifact
|
|
29
|
-
uses: actions/upload-artifact@
|
|
29
|
+
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f
|
|
30
30
|
with:
|
|
31
31
|
name: python-package-distributions
|
|
32
32
|
path: dist/
|
|
@@ -41,10 +41,10 @@ jobs:
|
|
|
41
41
|
id-token: write
|
|
42
42
|
steps:
|
|
43
43
|
- name: Download distribution files from artifact
|
|
44
|
-
uses: actions/download-artifact@
|
|
44
|
+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
|
|
45
45
|
with:
|
|
46
46
|
name: python-package-distributions
|
|
47
47
|
path: dist/
|
|
48
48
|
|
|
49
49
|
- name: Publish package to PyPI
|
|
50
|
-
uses: pypa/gh-action-pypi-publish@
|
|
50
|
+
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
bandit[sarif,toml]==1.9.4
|
|
@@ -10,11 +10,11 @@ jobs:
|
|
|
10
10
|
bandit-scan:
|
|
11
11
|
runs-on: ubuntu-latest
|
|
12
12
|
steps:
|
|
13
|
-
- uses: actions/checkout@
|
|
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@
|
|
17
|
+
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
|
|
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@
|
|
33
|
+
uses: github/codeql-action/upload-sarif@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a
|
|
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@
|
|
46
|
+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
|
47
47
|
|
|
48
48
|
- name: Initialize CodeQL
|
|
49
|
-
uses: github/codeql-action/init@
|
|
49
|
+
uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a
|
|
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@
|
|
55
|
+
uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a
|
|
56
56
|
with:
|
|
57
57
|
category: "/language:${{matrix.language}}"
|
|
@@ -9,10 +9,10 @@ jobs:
|
|
|
9
9
|
lint:
|
|
10
10
|
runs-on: ubuntu-latest
|
|
11
11
|
steps:
|
|
12
|
-
- uses: actions/checkout@
|
|
12
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
|
13
13
|
|
|
14
14
|
- name: Set up Python 3.10
|
|
15
|
-
uses: actions/setup-python@
|
|
15
|
+
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
|
|
16
16
|
with:
|
|
17
17
|
python-version: '3.10'
|
|
18
18
|
cache: 'pip'
|
|
@@ -21,6 +21,7 @@ jobs:
|
|
|
21
21
|
- name: Install Python dependencies
|
|
22
22
|
run: |
|
|
23
23
|
pip install -r .github/workflows/requirements/style.txt
|
|
24
|
+
pip install -e .[aiohttp]
|
|
24
25
|
|
|
25
26
|
- name: Run Ruff
|
|
26
27
|
run: |
|
|
@@ -28,5 +29,8 @@ jobs:
|
|
|
28
29
|
ruff check --select I --diff
|
|
29
30
|
ruff format --check --diff
|
|
30
31
|
|
|
32
|
+
- name: Run Type Checker
|
|
33
|
+
run: ty check
|
|
34
|
+
|
|
31
35
|
- name: Run Typos
|
|
32
36
|
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@
|
|
16
|
-
- uses: actions/setup-python@
|
|
15
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
|
16
|
+
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
|
|
17
17
|
with:
|
|
18
18
|
python-version: ${{ matrix.python-version }}
|
|
19
19
|
cache: 'pip'
|
|
@@ -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@
|
|
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
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: repligit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.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
|
|
@@ -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,
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "repligit"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
dependencies = []
|
|
9
9
|
authors = [
|
|
10
10
|
{ name="Alec Scott" },
|
|
@@ -25,6 +25,7 @@ Issues = "https://github.com/llnl/repligit/issues"
|
|
|
25
25
|
|
|
26
26
|
[project.optional-dependencies]
|
|
27
27
|
aiohttp = ["aiohttp"]
|
|
28
|
+
dev = ["ty", "ruff", "typos", "coverage", "pytest", "pytest-mock"]
|
|
28
29
|
|
|
29
30
|
[tool.pytest.ini_options]
|
|
30
31
|
pythonpath = [
|
|
@@ -39,3 +40,7 @@ exclude = ["spack"]
|
|
|
39
40
|
exclude_dirs = []
|
|
40
41
|
# allow use of assert
|
|
41
42
|
skips = ["B101"]
|
|
43
|
+
|
|
44
|
+
[tool.ty.src]
|
|
45
|
+
# exclude spack directory from type checking
|
|
46
|
+
exclude = ["spack/**"]
|
|
@@ -3,9 +3,8 @@
|
|
|
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
|
-
|
|
7
|
+
repligit: spack/repos/spack_repo/repligit
|
|
9
8
|
specs:
|
|
10
9
|
- py-codespell
|
|
11
10
|
- py-pytest
|
|
@@ -13,10 +12,11 @@ spack:
|
|
|
13
12
|
- py-bandit
|
|
14
13
|
- py-ruff
|
|
15
14
|
- py-pip
|
|
15
|
+
- py-ty
|
|
16
16
|
view: true
|
|
17
17
|
concretizer:
|
|
18
18
|
unify: true
|
|
19
19
|
develop:
|
|
20
20
|
py-repligit:
|
|
21
21
|
spec: py-repligit@=main
|
|
22
|
-
path:
|
|
22
|
+
path: .
|
|
@@ -1,23 +1,32 @@
|
|
|
1
|
-
from typing import
|
|
1
|
+
from typing import Iterable
|
|
2
2
|
|
|
3
3
|
import aiohttp
|
|
4
4
|
|
|
5
5
|
from repligit.asyncio.parse import decode_lines, iter_lines
|
|
6
|
+
from repligit.exceptions import (
|
|
7
|
+
RefUpdateRejected,
|
|
8
|
+
RemoteError,
|
|
9
|
+
UnexpectedResponse,
|
|
10
|
+
UnpackFailed,
|
|
11
|
+
)
|
|
6
12
|
from repligit.parse import generate_fetch_pack_request, generate_send_pack_header
|
|
7
13
|
|
|
8
14
|
|
|
9
|
-
async def ls_remote(
|
|
15
|
+
async def ls_remote(
|
|
16
|
+
url: str, username: str | None = None, password: str | None = None
|
|
17
|
+
) -> dict[str, str]:
|
|
10
18
|
"""Get commit hash of remote master branch, return SHA-1 hex string or
|
|
11
19
|
None if no remote commits.
|
|
12
20
|
"""
|
|
13
21
|
|
|
14
22
|
url = f"{url}/info/refs?service=git-upload-pack"
|
|
15
|
-
auth = aiohttp.BasicAuth(username, password) if
|
|
23
|
+
auth = aiohttp.BasicAuth(username or "", password) if password else None
|
|
16
24
|
async with aiohttp.ClientSession(auth=auth) as session:
|
|
17
25
|
async with session.get(url, raise_for_status=True) as resp:
|
|
18
26
|
lines = decode_lines(iter_lines(resp, encoding="utf-8"))
|
|
19
27
|
service_line = await anext(lines)
|
|
20
|
-
|
|
28
|
+
if service_line != "# service=git-upload-pack":
|
|
29
|
+
raise UnexpectedResponse(f"invalid service line: {service_line!r}")
|
|
21
30
|
|
|
22
31
|
# `async for` inside `dict()` not supported so no dict comprehension
|
|
23
32
|
result = {}
|
|
@@ -30,15 +39,18 @@ async def ls_remote(url: str, username: str = None, password: str = None):
|
|
|
30
39
|
|
|
31
40
|
|
|
32
41
|
async def fetch_pack(
|
|
33
|
-
url: str,
|
|
34
|
-
|
|
42
|
+
url: str,
|
|
43
|
+
want_sha: str,
|
|
44
|
+
have_shas: Iterable[str],
|
|
45
|
+
username: str | None = None,
|
|
46
|
+
password: str | None = None,
|
|
47
|
+
) -> bytes | None:
|
|
35
48
|
"""Download a packfile from a remote server."""
|
|
36
49
|
# ensure have_shas is a set, else packfile errors will occur
|
|
37
|
-
|
|
38
|
-
have_shas = set(have_shas)
|
|
50
|
+
have_shas = set(have_shas)
|
|
39
51
|
|
|
40
52
|
url = f"{url}/git-upload-pack"
|
|
41
|
-
auth = aiohttp.BasicAuth(username, password) if
|
|
53
|
+
auth = aiohttp.BasicAuth(username or "", password) if password else None
|
|
42
54
|
|
|
43
55
|
request = generate_fetch_pack_request(want_sha, have_shas)
|
|
44
56
|
|
|
@@ -56,6 +68,11 @@ async def fetch_pack(
|
|
|
56
68
|
line_length = int(length_bytes, 16)
|
|
57
69
|
|
|
58
70
|
line = await resp.content.readexactly(line_length - 4)
|
|
71
|
+
|
|
72
|
+
# e.g. "ERR upload-pack: not our ref <sha>"
|
|
73
|
+
if line[:3] == b"ERR":
|
|
74
|
+
raise RemoteError(line.decode("utf-8").strip())
|
|
75
|
+
|
|
59
76
|
if line[:3] == b"NAK" or line[:3] == b"ACK":
|
|
60
77
|
# this is a difference in API between sync and async
|
|
61
78
|
# has to be read within this context to be used in the caller
|
|
@@ -69,13 +86,13 @@ async def send_pack(
|
|
|
69
86
|
ref: str,
|
|
70
87
|
from_sha: str,
|
|
71
88
|
to_sha: str,
|
|
72
|
-
packfile,
|
|
73
|
-
username: str = None,
|
|
74
|
-
password: str = None,
|
|
75
|
-
):
|
|
89
|
+
packfile: bytes,
|
|
90
|
+
username: str | None = None,
|
|
91
|
+
password: str | None = None,
|
|
92
|
+
) -> None:
|
|
76
93
|
"""Send a packfile to a remote server."""
|
|
77
94
|
url = f"{url}/git-receive-pack"
|
|
78
|
-
auth = aiohttp.BasicAuth(username, password) if
|
|
95
|
+
auth = aiohttp.BasicAuth(username or "", password) if password else None
|
|
79
96
|
|
|
80
97
|
header = generate_send_pack_header(ref, from_sha, to_sha)
|
|
81
98
|
# unlike in the sync version the packfile is already read into memory
|
|
@@ -91,5 +108,17 @@ async def send_pack(
|
|
|
91
108
|
raise_for_status=True,
|
|
92
109
|
) as resp:
|
|
93
110
|
lines = decode_lines(iter_lines(resp, encoding="utf-8"))
|
|
94
|
-
|
|
95
|
-
|
|
111
|
+
unpack_status = await anext(lines)
|
|
112
|
+
if unpack_status != "unpack ok":
|
|
113
|
+
raise UnpackFailed(unpack_status)
|
|
114
|
+
|
|
115
|
+
# "ng <ref> <reason>" (ng = not good) means the remote rejected the
|
|
116
|
+
# update. The reason may be non-fast-forward, hook declined, etc.
|
|
117
|
+
ref_status = await anext(lines)
|
|
118
|
+
prefix = f"ng {ref} "
|
|
119
|
+
if ref_status.startswith(prefix):
|
|
120
|
+
reason_str = ref_status[len(prefix) :]
|
|
121
|
+
raise RefUpdateRejected(reason_str)
|
|
122
|
+
|
|
123
|
+
if ref_status != f"ok {ref}":
|
|
124
|
+
raise UnexpectedResponse(f"unexpected ref status line: {ref_status!r}")
|
|
@@ -23,7 +23,7 @@ async def decode_lines(line_stream: AsyncIterable) -> AsyncIterator:
|
|
|
23
23
|
|
|
24
24
|
async def iter_lines(
|
|
25
25
|
resp: aiohttp.ClientResponse, encoding: str = "utf-8", chunk_size: int = 16 * 1024
|
|
26
|
-
):
|
|
26
|
+
) -> AsyncIterator[str]:
|
|
27
27
|
"""
|
|
28
28
|
Asynchronously iterate over the lines of an HTTP response.
|
|
29
29
|
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import urllib.request
|
|
2
|
-
from
|
|
3
|
-
|
|
2
|
+
from http.client import HTTPResponse
|
|
3
|
+
from typing import BinaryIO, Iterable
|
|
4
|
+
|
|
5
|
+
from repligit.exceptions import (
|
|
6
|
+
RefUpdateRejected,
|
|
7
|
+
RemoteError,
|
|
8
|
+
UnexpectedResponse,
|
|
9
|
+
UnpackFailed,
|
|
10
|
+
)
|
|
4
11
|
from repligit.parse import (
|
|
5
12
|
decode_lines,
|
|
6
13
|
generate_fetch_pack_request,
|
|
@@ -9,7 +16,13 @@ from repligit.parse import (
|
|
|
9
16
|
)
|
|
10
17
|
|
|
11
18
|
|
|
12
|
-
def http_request(
|
|
19
|
+
def http_request(
|
|
20
|
+
url: str,
|
|
21
|
+
headers: dict[str, str] | None = None,
|
|
22
|
+
username: str | None = None,
|
|
23
|
+
password: str | None = None,
|
|
24
|
+
data: bytes | None = None,
|
|
25
|
+
) -> HTTPResponse:
|
|
13
26
|
"""
|
|
14
27
|
Constructs and executes an HTTP request using urllib. (GET by default,
|
|
15
28
|
POST if "data" is not None).
|
|
@@ -25,7 +38,9 @@ def http_request(url, headers=None, username=None, password=None, data=None):
|
|
|
25
38
|
file-like object: The response file handler from the request
|
|
26
39
|
"""
|
|
27
40
|
password_manager = urllib.request.HTTPPasswordMgrWithDefaultRealm()
|
|
28
|
-
|
|
41
|
+
|
|
42
|
+
if password:
|
|
43
|
+
password_manager.add_password(None, url, username or "", password)
|
|
29
44
|
|
|
30
45
|
auth_handler = urllib.request.HTTPBasicAuthHandler(password_manager)
|
|
31
46
|
opener = urllib.request.build_opener(auth_handler)
|
|
@@ -39,7 +54,9 @@ def http_request(url, headers=None, username=None, password=None, data=None):
|
|
|
39
54
|
return opener.open(request)
|
|
40
55
|
|
|
41
56
|
|
|
42
|
-
def ls_remote(
|
|
57
|
+
def ls_remote(
|
|
58
|
+
url: str, username: str | None = None, password: str | None = None
|
|
59
|
+
) -> dict[str, str]:
|
|
43
60
|
"""Get commit hash of remote master branch, return SHA-1 hex string or
|
|
44
61
|
None if no remote commits.
|
|
45
62
|
"""
|
|
@@ -49,18 +66,22 @@ def ls_remote(url: str, username: str = None, password: str = None):
|
|
|
49
66
|
|
|
50
67
|
lines = decode_lines(iter_lines(resp))
|
|
51
68
|
service_line = next(lines)
|
|
52
|
-
|
|
69
|
+
if service_line != "# service=git-upload-pack":
|
|
70
|
+
raise UnexpectedResponse(f"invalid service line: {service_line!r}")
|
|
53
71
|
|
|
54
|
-
return
|
|
72
|
+
return {ref: sha for sha, ref in (line.split() for line in lines if line)}
|
|
55
73
|
|
|
56
74
|
|
|
57
75
|
def fetch_pack(
|
|
58
|
-
url: str,
|
|
59
|
-
|
|
76
|
+
url: str,
|
|
77
|
+
want_sha: str,
|
|
78
|
+
have_shas: Iterable[str],
|
|
79
|
+
username: str | None = None,
|
|
80
|
+
password: str | None = None,
|
|
81
|
+
) -> HTTPResponse | None:
|
|
60
82
|
"""Download a packfile from a remote server."""
|
|
61
83
|
# ensure have_shas is a set, else packfile errors will occur
|
|
62
|
-
|
|
63
|
-
have_shas = set(have_shas)
|
|
84
|
+
have_shas = set(have_shas)
|
|
64
85
|
|
|
65
86
|
url = f"{url}/git-upload-pack"
|
|
66
87
|
request = generate_fetch_pack_request(want_sha, have_shas)
|
|
@@ -78,6 +99,10 @@ def fetch_pack(
|
|
|
78
99
|
line_length = int(resp.read(4), 16)
|
|
79
100
|
line = resp.read(line_length - 4)
|
|
80
101
|
|
|
102
|
+
# e.g. "ERR upload-pack: not our ref <sha>"
|
|
103
|
+
if line[:3] == b"ERR":
|
|
104
|
+
raise RemoteError(line.decode("utf-8").strip())
|
|
105
|
+
|
|
81
106
|
if line[:3] == b"NAK" or line[:3] == b"ACK":
|
|
82
107
|
return resp
|
|
83
108
|
else:
|
|
@@ -89,10 +114,10 @@ def send_pack(
|
|
|
89
114
|
ref: str,
|
|
90
115
|
from_sha: str,
|
|
91
116
|
to_sha: str,
|
|
92
|
-
packfile,
|
|
93
|
-
username: str = None,
|
|
94
|
-
password: str = None,
|
|
95
|
-
):
|
|
117
|
+
packfile: BinaryIO,
|
|
118
|
+
username: str | None = None,
|
|
119
|
+
password: str | None = None,
|
|
120
|
+
) -> None:
|
|
96
121
|
"""Send a packfile to a remote server."""
|
|
97
122
|
url = f"{url}/git-receive-pack"
|
|
98
123
|
|
|
@@ -110,5 +135,17 @@ def send_pack(
|
|
|
110
135
|
)
|
|
111
136
|
|
|
112
137
|
lines = decode_lines(iter_lines(resp))
|
|
113
|
-
|
|
114
|
-
|
|
138
|
+
unpack_status = next(lines)
|
|
139
|
+
if unpack_status != "unpack ok":
|
|
140
|
+
raise UnpackFailed(unpack_status)
|
|
141
|
+
|
|
142
|
+
# "ng <ref> <reason>" (ng = not good) means the remote rejected the update.
|
|
143
|
+
# The reason can be non-fast-forward, hook declined, missing objects, etc.
|
|
144
|
+
ref_status = next(lines)
|
|
145
|
+
prefix = f"ng {ref} "
|
|
146
|
+
if ref_status.startswith(prefix):
|
|
147
|
+
reason_str = ref_status[len(prefix) :]
|
|
148
|
+
raise RefUpdateRejected(reason_str)
|
|
149
|
+
|
|
150
|
+
if ref_status != f"ok {ref}":
|
|
151
|
+
raise UnexpectedResponse(f"unexpected ref status line: {ref_status!r}")
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
class RepligitError(Exception):
|
|
2
|
+
"""Base class for all repligit errors."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class RefUpdateRejected(RepligitError):
|
|
6
|
+
"""Raised when the remote rejects a ref update during send-pack."""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RemoteError(RepligitError):
|
|
10
|
+
"""Raised when the remote sends an ERR packet (e.g. want SHA not found)."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class UnpackFailed(RepligitError):
|
|
14
|
+
"""Raised when the remote fails to unpack the packfile during send-pack."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class UnexpectedResponse(RepligitError):
|
|
18
|
+
"""Raised when the remote sends a response repligit does not recognize."""
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
from typing import IO, Generator,
|
|
1
|
+
from typing import IO, Generator, Iterable, Set
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
def iter_lines(
|
|
5
5
|
data: IO, encoding: str = "utf-8", chunk_size: int = 16 * 1024
|
|
6
|
-
) -> Generator[str,
|
|
6
|
+
) -> Generator[str, None, None]:
|
|
7
7
|
"""
|
|
8
8
|
Iterate over the lines of a file-like object, yielding one line at a time.
|
|
9
9
|
|
|
@@ -30,7 +30,7 @@ def iter_lines(
|
|
|
30
30
|
yield incomplete_line.rstrip(b"\r").decode(encoding)
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
def decode_lines(lines:
|
|
33
|
+
def decode_lines(lines: Iterable[str]) -> Generator[str, None, None]:
|
|
34
34
|
"""
|
|
35
35
|
Decode lines from the git transfer protocol into usable lines.
|
|
36
36
|
|
|
@@ -49,7 +49,7 @@ def decode_lines(lines: Generator[str, str, str]) -> Generator[str, str, str]:
|
|
|
49
49
|
yield line[4:line_length]
|
|
50
50
|
|
|
51
51
|
|
|
52
|
-
def encode_lines(lines:
|
|
52
|
+
def encode_lines(lines: Iterable[bytes | str]) -> bytes:
|
|
53
53
|
"""
|
|
54
54
|
Encode a list of lines into a byte string format for git transmission.
|
|
55
55
|
|
|
@@ -3,15 +3,15 @@ from repligit.parse import decode_lines, encode_lines, generate_send_pack_header
|
|
|
3
3
|
|
|
4
4
|
def test_decode_lines():
|
|
5
5
|
raw_lines = [
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
"003fbef547a59eec448284136f03984dce0f2f8239a9 refs/pull/95/head",
|
|
7
|
+
"003f358aa046cd57dbca306e80d4c3fbb86edc5b36af refs/pull/96/head",
|
|
8
|
+
"0000",
|
|
9
9
|
]
|
|
10
10
|
|
|
11
11
|
decoded_lines = [
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
"bef547a59eec448284136f03984dce0f2f8239a9 refs/pull/95/head",
|
|
13
|
+
"358aa046cd57dbca306e80d4c3fbb86edc5b36af refs/pull/96/head",
|
|
14
|
+
"",
|
|
15
15
|
]
|
|
16
16
|
|
|
17
17
|
lines = list(decode_lines(raw_lines))
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
bandit[sarif,toml]==1.9.2
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{repligit-0.2.0 → repligit-0.3.0}/spack/repos/spack_repo/repligit/packages/py_repligit/package.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|