Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Interface with libgit2 directly #224

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
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: 2 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ fail_under = 100
exclude_also =
def __repr__
if log.isEnabledFor\([^\)]*\):
if TYPE_CHECKING:
@overload
show_missing = True
1 change: 0 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ repository.
Dependencies:

* Python >= 3.9
* pygit2 >= 1.4
* rpmautospec-core >= 0.1.4

Optional dependencies:
Expand Down
2 changes: 1 addition & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,6 @@ exclude = [
[tool.poetry.dependencies]
python = "^3.9"
rpmautospec_core = "^0.1.4"
pygit2 = [
{ version = "^1.4.0,<1.16.0", python = ">=3.9.0,<3.10.0" },
{ version = "^1.4.0", python = "^3.10.0" }
]
rpm = "*"
click = "^8"
click-plugins = "^1.1.1"
Expand All @@ -63,6 +59,10 @@ pytest-xdist = [
]
coverage = "^7.2.0"
ruff = "^0.2.0 || ^0.3.0 || ^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0 || ^0.8.0"
pygit2 = [
{ version = "^1.4.0,<1.16.0", python = ">=3.9.0,<3.10.0" },
{ version = "^1.4.0", python = "^3.10.0" }
]

[tool.poetry.scripts]
rpmautospec = "rpmautospec.cli:cli"
Expand Down
22 changes: 21 additions & 1 deletion rpmautospec/compat.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
from importlib.metadata import EntryPoint, entry_points
from io import BytesIO

import pygit2
try:
import pygit2
except ImportError:
from . import minigit2 as pygit2
uses_minigit2 = True
else:
import pygit2.enums
uses_minigit2 = False

needs_minimal_blobio = False
if uses_minigit2:
needs_minimal_blobio = True
else:
try:
from pygit2 import BlobIO
except ImportError: # pragma: no cover
needs_minimal_blobio = True


class MinimalBlobIO:
Expand All @@ -20,6 +36,10 @@ def __exit__(self, exc_type, exc_value, traceback):
pass


if needs_minimal_blobio:
BlobIO = MinimalBlobIO


def cli_plugin_entry_points() -> tuple[EntryPoint]:
"""Find entry points for CLI plugins.

Expand Down
26 changes: 26 additions & 0 deletions rpmautospec/minigit2/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""Minimal wrapper for libgit2

This package wraps the functionality of libgit2 used by rpmautospec (and
only that), aiming to mimic the pygit2 API (of the minimum version
supported, 1.1).

The reason we want to be able to bypass pygit2 itself is because it
makes bootstrapping rpmautospec hairy (e.g. for a new Python version).
"""

from . import enums, settings
from .blob import Blob
from .commit import Commit
from .constants import (
GIT_CONFIG_LEVEL_GLOBAL,
GIT_CONFIG_LEVEL_LOCAL,
GIT_CONFIG_LEVEL_SYSTEM,
GIT_CONFIG_LEVEL_XDG,
GIT_REPOSITORY_OPEN_NO_SEARCH,
)
from .exc import GitError
from .oid import Oid
from .repository import Repository
from .tree import Tree

init_repository = Repository.init_repository
29 changes: 29 additions & 0 deletions rpmautospec/minigit2/blob.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""Minimal wrapper for libgit2 - Blob"""

from ctypes import c_char, memmove
from functools import cached_property
from typing import Optional

from .native_adaptation import git_blob_p, git_object_t
from .object_ import Object


class Blob(Object):
"""Represent a git blob."""

_libgit2_native_finalizer = "git_blob_free"

_object_type = git_blob_p
_object_t = git_object_t.BLOB

_real_native: Optional[git_blob_p] = None

@cached_property
def data(self) -> bytes:
rawsize = self._lib.git_blob_rawsize(self._native)
rawcontent_p = self._lib.git_blob_rawcontent(self._native)
self.raise_if_error(not rawcontent_p, "Error accessing blob content: {message}")

buf = (c_char * rawsize)()
memmove(buf, rawcontent_p, rawsize)
return bytes(buf)
71 changes: 71 additions & 0 deletions rpmautospec/minigit2/commit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""Minimal wrapper for libgit2 - Commit"""

from functools import cached_property
from typing import Optional, Union

from .native_adaptation import git_commit_p, git_object_t, git_tree_p
from .object_ import Object
from .oid import Oid
from .signature import Signature
from .tree import Tree

CommitTypes = Union[git_commit_p, Oid, str, bytes]


class Commit(Object):
"""Represent a git commit."""

_libgit2_native_finalizer = "git_commit_free"

_object_type = git_commit_p
_object_t = git_object_t.COMMIT

_real_native: Optional[git_commit_p] = None

@cached_property
def parents(self) -> list["Commit"]:
n_parents = self._lib.git_commit_parentcount(self._native)
parents = []
for n in range(n_parents):
native = git_commit_p()
error_code = self._lib.git_commit_parent(native, self._native, n)
self.raise_if_error(error_code, "Error getting parent: {message}")
parents.append(Commit(_repo=self._repo, _native=native))
return parents

@cached_property
def tree(self) -> "Tree":
native = git_tree_p()
error_code = self._lib.git_commit_tree(native, self._native)
self.raise_if_error(error_code, "Error retrieving tree: {message}")
return Tree(_repo=self._repo, _native=native)

@cached_property
def commit_time(self) -> int:
return self._lib.git_commit_time(self._native)

@cached_property
def commit_time_offset(self) -> int:
return self._lib.git_commit_time_offset(self._native)

@cached_property
def author(self) -> "Signature":
return Signature(native=self._lib.git_commit_author(self._native), _owner=self)

@cached_property
def committer(self) -> "Signature":
return Signature(native=self._lib.git_commit_committer(self._native), _owner=self)

@cached_property
def message_encoding(self) -> Optional[str]:
encoding = self._lib.git_commit_message_encoding(self._native)
if encoding:
encoding = encoding.decode("ascii")
else:
encoding = "utf-8"
return encoding

@cached_property
def message(self) -> str:
message = self._lib.git_commit_message(self._native)
return message.decode(encoding=self.message_encoding, errors="replace")
15 changes: 15 additions & 0 deletions rpmautospec/minigit2/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from . import native_adaptation

GIT_CHECKOUT_FORCE = (1 << 1)

GIT_DIFF_OPTIONS_VERSION = 1

GIT_OID_RAWSZ = GIT_OID_SHA1_SIZE = 20
GIT_OID_HEXSZ = GIT_OID_SHA1_HEXSIZE = GIT_OID_SHA1_SIZE * 2

GIT_REPOSITORY_OPEN_NO_SEARCH = native_adaptation.git_repository_open_flag_t.NO_SEARCH

GIT_CONFIG_LEVEL_SYSTEM = native_adaptation.git_config_level_t.SYSTEM
GIT_CONFIG_LEVEL_XDG = native_adaptation.git_config_level_t.XDG
GIT_CONFIG_LEVEL_GLOBAL = native_adaptation.git_config_level_t.GLOBAL
GIT_CONFIG_LEVEL_LOCAL = native_adaptation.git_config_level_t.LOCAL
48 changes: 48 additions & 0 deletions rpmautospec/minigit2/diff.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""Minimal wrapper for libgit2 - Diff & DiffStat"""

from functools import cached_property
from typing import TYPE_CHECKING, Optional

from .native_adaptation import (
git_diff_p,
git_diff_stats_p,
)
from .wrapper import WrapperOfWrappings

if TYPE_CHECKING:
from .repository import Repository


class DiffStats(WrapperOfWrappings):
"""Represent diff statistics."""

_libgit2_native_finalizer = "git_diff_stats_free"

_diff: "Diff"
_real_native: Optional[git_diff_stats_p] = None

def __init__(self, diff: "Diff", native: git_diff_stats_p) -> None:
self._diff = diff
super().__init__(native=native)

@property
def files_changed(self) -> int:
return self._lib.git_diff_stats_files_changed(self._native)


class Diff(WrapperOfWrappings):
"""Represent a diff."""

_repo: "Repository"
_real_native: Optional[git_diff_p] = None

def __init__(self, repo: "Repository", native: git_diff_p) -> None:
self._repo = repo
super().__init__(native=native)

@cached_property
def stats(self) -> DiffStats:
native = git_diff_stats_p()
error_code = self._lib.git_diff_get_stats(native, self._native)
self.raise_if_error(error_code, "Can’t get diff stats: {message}")
return DiffStats(diff=self, native=native)
1 change: 1 addition & 0 deletions rpmautospec/minigit2/enums.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .native_adaptation import git_delta_t as DeltaStatus # noqa: F401
32 changes: 32 additions & 0 deletions rpmautospec/minigit2/exc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""Minimal wrapper for libgit2 - Exceptions and Warnings"""


class Libgit2Error(Exception):
pass


class Libgit2NotFoundError(Libgit2Error):
pass


class Libgit2VersionError(Libgit2Error):
pass


class Libgit2VersionWarning(UserWarning):
pass


# Exception classes present in pygit2


class AlreadyExistsError(ValueError):
pass


class GitError(Exception):
pass


class InvalidSpecError(ValueError):
pass
47 changes: 47 additions & 0 deletions rpmautospec/minigit2/index.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""Minimal wrapper for libgit2 - Index"""

from typing import TYPE_CHECKING, Optional

from .constants import GIT_DIFF_OPTIONS_VERSION
from .diff import Diff
from .native_adaptation import git_diff_option_t, git_diff_options, git_diff_p, git_index_p
from .wrapper import WrapperOfWrappings

if TYPE_CHECKING:
from .repository import Repository


class Index(WrapperOfWrappings):
"""Represent the git index."""

_libgit2_native_finalizer = "git_index_free"

_repo: "Repository"
_real_native: Optional[git_index_p]

def __init__(self, repo: "Repository", native: git_index_p) -> None:
self._repo = repo
super().__init__(native=native)

def diff_to_workdir(
self,
flags: git_diff_option_t = git_diff_option_t.NORMAL,
context_lines: int = 3,
interhunk_lines: int = 0,
) -> Diff:
diff_options = git_diff_options()
error_code = self._lib.git_diff_options_init(diff_options, GIT_DIFF_OPTIONS_VERSION)
self.raise_if_error(error_code)

diff_options.flags = flags
diff_options.context_lines = context_lines
diff_options.interhunk_lines = interhunk_lines

diff_p = git_diff_p()

error_code = self._lib.git_diff_index_to_workdir(
diff_p, self._repo._native, self._native, diff_options
)
self.raise_if_error(error_code)

return Diff(repo=self._repo, native=diff_p)
Loading
Loading