Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
pyv: ['3.9', '3.10', '3.11', '3.12', '3.13']
pyv: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14']

steps:
- name: Check out the repository
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ repos:
- id: sort-simple-yaml
- id: trailing-whitespace
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: 'v0.13.1'
rev: 'v0.14.3'
hooks:
- id: ruff-check
args: [--fix, --exit-non-zero-on-fix]
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ classifiers = [
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Development Status :: 4 - Beta",
]
requires-python = ">=3.9"
Expand Down Expand Up @@ -52,7 +53,7 @@ tests = [
"proxy.py",
]
dev = [
"mypy==1.17.1",
"mypy==1.18.2",
"scmrepo[tests]",
"types-certifi",
"types-mock",
Expand Down
84 changes: 49 additions & 35 deletions src/scmrepo/git/backend/dulwich/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ def clone(
url,
target=to_path,
errstream=(
DulwichProgressReporter(progress) if progress else NoneStream()
DulwichProgressReporter(progress) if progress else NoneStream() # type: ignore[arg-type]
),
bare=bare,
)
Expand Down Expand Up @@ -278,8 +278,11 @@ def clone(
def _set_default_tracking_branch(repo: "Repo"):
from dulwich.refs import LOCAL_BRANCH_PREFIX, parse_symref_value

head_ref = repo.refs.read_ref(b"HEAD")
if head_ref is None:
return
try:
ref = parse_symref_value(repo.refs.read_ref(b"HEAD"))
ref = parse_symref_value(head_ref)
except ValueError:
return
if ref.startswith(LOCAL_BRANCH_PREFIX):
Expand Down Expand Up @@ -307,7 +310,7 @@ def _set_mirror(
fetch(
repo,
remote_location=b"origin",
errstream=(DulwichProgressReporter(progress) if progress else NoneStream()),
errstream=(DulwichProgressReporter(progress) if progress else NoneStream()), # type: ignore[arg-type]
)

@staticmethod
Expand Down Expand Up @@ -599,11 +602,11 @@ def iter_remote_refs(self, url: str, base: Optional[str] = None, **kwargs):
if base:
yield from (
os.fsdecode(ref)
for ref in client.get_refs(path)
for ref in client.get_refs(path.encode())
if ref.startswith(os.fsencode(base))
)
else:
yield from (os.fsdecode(ref) for ref in client.get_refs(path))
yield from (os.fsdecode(ref) for ref in client.get_refs(path.encode()))
except NotGitRepository as exc:
raise InvalidRemote(url) from exc
except HTTPUnauthorized as exc:
Expand Down Expand Up @@ -634,7 +637,7 @@ def push_refspecs( # noqa: C901
raise SCMError(f"'{url}' is not a valid Git remote or URL") from exc

change_result = {}
selected_refs = []
selected_refs: list[tuple[Optional[bytes], Optional[bytes], bool]] = []

def update_refs(refs):
from dulwich.objects import ZERO_SHA
Expand All @@ -649,6 +652,7 @@ def update_refs(refs):
)
new_refs = {}
for lh, rh, _ in selected_refs:
assert rh is not None
refname = os.fsdecode(rh)
if rh in refs and lh is not None:
if refs[rh] == self.repo.refs[lh]:
Expand Down Expand Up @@ -679,9 +683,9 @@ def update_refs(refs):

try:
result = client.send_pack(
path,
path.encode(),
update_refs,
generate_pack_data=self.repo.generate_pack_data,
generate_pack_data=self.repo.generate_pack_data, # type: ignore[arg-type]
progress=(DulwichProgressReporter(progress) if progress else None),
)
except (NotGitRepository, SendPackError) as exc:
Expand Down Expand Up @@ -717,7 +721,7 @@ def fetch_refspecs(
from dulwich.porcelain import DivergedBranches, check_diverged, get_remote_repo
from dulwich.refs import DictRefsContainer

fetch_refs = []
fetch_refs: list[tuple[Optional[bytes], Optional[bytes], bool]] = []

def determine_wants(
remote_refs: dict[bytes, bytes],
Expand All @@ -736,7 +740,7 @@ def determine_wants(
return [
remote_refs[lh]
for (lh, _, _) in fetch_refs
if remote_refs[lh] not in self.repo.object_store
if lh is not None and remote_refs[lh] not in self.repo.object_store
]

with reraise(Exception, SCMError(f"'{url}' is not a valid Git remote or URL")):
Expand All @@ -749,41 +753,42 @@ def determine_wants(
SCMError(f"Git failed to fetch ref from '{url}'"),
):
fetch_result = client.fetch(
path,
path.encode(),
self.repo,
progress=DulwichProgressReporter(progress) if progress else None,
determine_wants=determine_wants,
determine_wants=determine_wants, # type: ignore[arg-type]
)

result = {}

for lh, rh, _ in fetch_refs:
assert rh is not None
refname = os.fsdecode(rh)
assert lh is not None
fetch_ref_lh = fetch_result.refs[lh]
assert fetch_ref_lh is not None
if rh in self.repo.refs:
if self.repo.refs[rh] == fetch_result.refs[lh]:
if self.repo.refs[rh] == fetch_ref_lh:
result[refname] = SyncStatus.UP_TO_DATE
continue
try:
check_diverged(
self.repo,
self.repo.refs[rh],
fetch_result.refs[lh],
fetch_ref_lh,
)
except DivergedBranches:
if not force:
overwrite = (
on_diverged(
os.fsdecode(rh),
os.fsdecode(fetch_result.refs[lh]),
)
on_diverged(os.fsdecode(rh), os.fsdecode(fetch_ref_lh))
if on_diverged
else False
)
if not overwrite:
result[refname] = SyncStatus.DIVERGED
continue

self.repo.refs[rh] = fetch_result.refs[lh]
self.repo.refs[rh] = fetch_ref_lh
result[refname] = SyncStatus.SUCCESS
return result

Expand Down Expand Up @@ -865,6 +870,7 @@ def _describe(
return results

def diff(self, rev_a: str, rev_b: str, binary=False) -> str:
from dulwich.objects import Commit
from dulwich.patch import write_tree_diff

try:
Expand All @@ -873,6 +879,9 @@ def diff(self, rev_a: str, rev_b: str, binary=False) -> str:
except KeyError as exc:
raise RevError("Invalid revision") from exc

assert isinstance(commit_a, Commit)
assert isinstance(commit_b, Commit)

buf = BytesIO()
write_tree_diff(buf, self.repo.object_store, commit_a.tree, commit_b.tree)
return buf.getvalue().decode("utf-8")
Expand Down Expand Up @@ -958,20 +967,22 @@ def get_tag(self, name: str) -> Optional[Union[str, "GitTag"]]:
ref = self.repo.refs[name_b]
except KeyError:
return None
if ref in self.repo and isinstance(self.repo[ref], Tag):
tag = self.repo[ref]
_typ, target_sha = tag.object
tagger_name, tagger_email = _parse_identity(tag.tagger.decode("utf-8"))
return GitTag(
os.fsdecode(tag.name),
tag.id,
target_sha.decode("ascii"),
tagger_name,
tagger_email,
tag.tag_time,
tag.tag_timezone,
tag.message.decode("utf-8"),
)
if ref in self.repo:
shafile = self.repo[ref]
if isinstance(shafile, Tag):
tag = shafile
_typ, target_sha = tag.object
tagger_name, tagger_email = _parse_identity(tag.tagger.decode("utf-8"))
return GitTag(
os.fsdecode(tag.name),
tag.id.decode("ascii"),
target_sha.decode("ascii"),
tagger_name,
tagger_email,
tag.tag_time,
tag.tag_timezone,
tag.message.decode("utf-8"),
)
return os.fsdecode(ref)

def get_config(self, path: Optional[str] = None) -> "Config":
Expand Down Expand Up @@ -1000,13 +1011,16 @@ def _parse_identity(identity: str) -> tuple[str, str]:
return m.group("name"), m.group("email")


def ls_remote(url: str) -> dict[str, str]:
def ls_remote(url: str) -> dict[str, Optional[str]]:
from dulwich import porcelain
from dulwich.client import HTTPUnauthorized

try:
refs = porcelain.ls_remote(url).refs
return {os.fsdecode(ref): sha.decode("ascii") for ref, sha in refs.items()}
return {
os.fsdecode(ref): sha.decode("ascii") if sha is not None else None
for ref, sha in refs.items()
}
except HTTPUnauthorized as exc:
raise AuthError(url) from exc
except Exception as exc:
Expand Down
2 changes: 1 addition & 1 deletion src/scmrepo/git/backend/dulwich/asyncssh_vendor.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ async def public_key_auth_requested( # noqa: C901
pubkey = read_public_key(pubkey_to_load)
except (OSError, KeyImportError):
pubkey = None
return SSHLocalKeyPair(key, pubkey)
return SSHLocalKeyPair(key, pubkey, cert=None, enc_key=None)
return None

async def _read_private_key_interactive(self, path: "FilePath") -> "SSHKey":
Expand Down
43 changes: 25 additions & 18 deletions src/scmrepo/git/backend/pygit2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@


if TYPE_CHECKING:
from pygit2 import Commit, Oid, Signature
from pygit2 import Commit, Oid, Reference, Signature, Tag
from pygit2.config import Config as _Pygit2Config
from pygit2.enums import CheckoutStrategy
from pygit2.remotes import Remote
Expand Down Expand Up @@ -67,15 +67,11 @@ def open(
else:
pass
if raw:
blob_kwargs = {}
blobio = BlobIO(self.obj)
else:
assert key is not None
path = "/".join(key)
blob_kwargs = {
"as_path": path,
"commit_id": commit.id,
}
blobio = BlobIO(self.obj, **blob_kwargs)
blobio = BlobIO(self.obj, as_path=path, commit_id=commit.id)
if mode == "rb":
return blobio
return TextIOWrapper(blobio, encoding=encoding)
Expand Down Expand Up @@ -190,14 +186,17 @@ def _refdb(self):

return RefdbFsBackend(self.repo)

def _resolve_refish(self, refish: str):
def _resolve_refish(
self, refish: str
) -> tuple["Commit", Union["Reference", "Tag"]]:
from pygit2 import Tag
from pygit2.enums import ObjectType

commit, ref = self.repo.resolve_refish(refish) # type: ignore[attr-defined]
ref: Union[Reference, Tag]
commit, ref = self.repo.resolve_refish(refish)
if isinstance(commit, Tag):
ref = commit
commit = commit.peel(ObjectType.COMMIT) # type: ignore[call-overload]
commit = commit.peel(ObjectType.COMMIT)
return commit, ref

@property
Expand Down Expand Up @@ -313,6 +312,7 @@ def _set_mirror(
# duplicate config section for each remote config entry. We just edit
# the config directly so that it creates a single section to be
# consistent with CLI Git
assert url is not None
repo.config["remote.origin.url"] = url
repo.config["remote.origin.fetch"] = "+refs/*:refs/*"
repo.config["remote.origin.mirror"] = True
Expand Down Expand Up @@ -347,7 +347,7 @@ def checkout(
force: bool = False,
**kwargs,
):
from pygit2 import GitError
from pygit2 import Commit, GitError
from pygit2.enums import CheckoutStrategy

strategy = self._get_checkout_strategy(
Expand All @@ -356,9 +356,10 @@ def checkout(

with self.release_odb_handles():
if create_new:
commit = self.repo.revparse_single("HEAD")
new_branch = self.repo.branches.local.create(branch, commit) # type: ignore[arg-type]
self.repo.checkout(new_branch, strategy=strategy) # type: ignore[attr-defined]
_commit = self.repo.revparse_single("HEAD")
assert isinstance(_commit, Commit)
new_branch = self.repo.branches.local.create(branch, _commit)
self.repo.checkout(new_branch, strategy=strategy)
else:
if branch == "-":
branch = "@{-1}"
Expand Down Expand Up @@ -582,7 +583,7 @@ def get_ref(self, name, follow: bool = True) -> Optional[str]:

def remove_ref(self, name: str, old_ref: Optional[str] = None):
ref = self.repo.references.get(name)
if not ref and not old_ref:
if not ref:
return
if old_ref and old_ref != str(ref.target):
raise SCMError(f"Failed to remove '{name}'")
Expand Down Expand Up @@ -676,6 +677,7 @@ def _get_remote(self, url: str) -> Iterator["Remote"]:
"""Return a pygit2.Remote suitable for the specified Git URL or remote name."""
try:
remote = self.repo.remotes[url]
assert remote.url is not None
url = remote.url
except ValueError:
pass
Expand All @@ -685,6 +687,7 @@ def _get_remote(self, url: str) -> Iterator["Remote"]:
if os.name == "nt":
url = url.removeprefix("file://")
remote = self.repo.remotes.create_anonymous(url)
assert remote.url is not None
parsed = urlparse(remote.url)
if parsed.scheme in ("git", "git+ssh", "ssh") or is_scp_style_url(remote.url):
raise NotImplementedError
Expand Down Expand Up @@ -838,7 +841,7 @@ def _apply(index):
_apply(i)
return

self.set_ref(Stash.DEFAULT_STASH, commit.id, message=commit.message)
self.set_ref(Stash.DEFAULT_STASH, commit.id, message=commit.message) # type: ignore[arg-type]
try:
_apply(0)
finally:
Expand Down Expand Up @@ -936,7 +939,9 @@ def checkout_index(
assert self.root_dir
path = os.path.join(self.root_dir, entry.path)
with open(path, "wb") as fobj:
fobj.write(self.repo.get(entry.id).read_raw()) # type: ignore[attr-defined]
obj = self.repo.get(entry.id)
assert obj is not None
fobj.write(obj.read_raw())
index.add(entry.path)
index.write()

Expand Down Expand Up @@ -1074,7 +1079,9 @@ def validate_git_remote(self, url: str, **kwargs):

def get_remote_url(self, remote: str) -> str:
try:
return self.repo.remotes[remote].url
url = self.repo.remotes[remote].url
assert url is not None
return url
except KeyError as exc:
raise InvalidRemote(remote) from exc

Expand Down
Loading
Loading