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
6 changes: 6 additions & 0 deletions src/scmrepo/git/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@
from .backend.dulwich import DulwichBackend
from .backend.gitpython import GitPythonBackend
from .backend.pygit2 import Pygit2Backend
from .objects import ( # noqa: F401, pylint: disable=unused-import
GitCommit,
GitObject,
GitTag,
GitTrie,
)
from .stash import Stash

if TYPE_CHECKING:
Expand Down
8 changes: 7 additions & 1 deletion src/scmrepo/git/backend/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,13 @@ def branch(self, branch: str):
pass

@abstractmethod
def tag(self, tag: str, annotated: bool = False, message: Optional[str] = None):
def tag(
self,
tag: str,
target: Optional[str] = None,
annotated: bool = False,
message: Optional[str] = None,
):
pass

@abstractmethod
Expand Down
36 changes: 33 additions & 3 deletions src/scmrepo/git/backend/dulwich/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import locale
import logging
import os
import re
import stat
from contextlib import closing
from functools import partial
Expand Down Expand Up @@ -390,7 +391,13 @@ def branch(self, branch: str):
except Error as exc:
raise SCMError(f"Failed to create branch '{branch}'") from exc

def tag(self, tag: str, annotated: bool = False, message: Optional[str] = None):
def tag(
self,
tag: str,
target: Optional[str] = None,
annotated: bool = False,
message: Optional[str] = None,
):
from dulwich.porcelain import Error, tag_create

if annotated and not message:
Expand All @@ -399,6 +406,7 @@ def tag(self, tag: str, annotated: bool = False, message: Optional[str] = None):
tag_create(
self.repo,
os.fsencode(tag),
objectish=target or "HEAD",
annotated=annotated,
message=message.encode("utf-8") if message else None,
)
Expand All @@ -423,10 +431,19 @@ def active_branch(self) -> str:
raise NotImplementedError

def list_branches(self) -> Iterable[str]:
raise NotImplementedError
from dulwich.refs import LOCAL_BRANCH_PREFIX

return sorted(
os.fsdecode(ref[len(LOCAL_BRANCH_PREFIX) :])
for ref in self.repo.refs.keys()
)

def list_tags(self) -> Iterable[str]:
raise NotImplementedError
from dulwich.refs import LOCAL_TAG_PREFIX

return sorted(
os.fsdecode(ref[len(LOCAL_TAG_PREFIX) :]) for ref in self.repo.refs.keys()
)

def list_all_commits(self) -> Iterable[str]:
raise NotImplementedError
Expand Down Expand Up @@ -890,12 +907,25 @@ def get_tag(self, name: str) -> Optional[Union[str, "GitTag"]]:
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"),
)
return os.fsdecode(ref)


_IDENTITY_RE = re.compile(r"(?P<name>.+)\s+<(?P<email>.+)>")


def _parse_identity(identity: str) -> Tuple[str, str]:
m = _IDENTITY_RE.match(identity)
if not m:
raise SCMError("Could not parse tagger identity '{identity}'")
return m.group("name"), m.group("email")
18 changes: 16 additions & 2 deletions src/scmrepo/git/backend/gitpython.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,10 +294,16 @@ def push(self):
def branch(self, branch):
self.repo.git.branch(branch)

def tag(self, tag: str, annotated: bool = False, message: Optional[str] = None):
def tag(
self,
tag: str,
target: Optional[str] = None,
annotated: bool = False,
message: Optional[str] = None,
):
if annotated and not message:
raise SCMError("message is required for annotated tag")
self.repo.git.tag(tag, a=annotated, m=message)
self.repo.git.tag(tag, target or "HEAD", a=annotated, m=message)

def untracked_files(self):
files = self.repo.untracked_files
Expand Down Expand Up @@ -392,6 +398,12 @@ def resolve_commit(self, rev: str) -> "GitCommit":
commit.committer_tz_offset,
commit.message,
[str(parent) for parent in commit.parents],
commit.committer.name,
commit.committer.email,
commit.author.name,
commit.author.email,
commit.authored_date,
commit.author_tz_offset,
)

def set_ref(
Expand Down Expand Up @@ -695,6 +707,8 @@ def get_tag(self, name: str) -> Optional[Union[str, "GitTag"]]:
tag.tag,
tag.hexsha,
tag.object.hexsha,
tag.tagger.name,
tag.tagger.email,
tag.tagged_date,
tag.tagger_tz_offset,
tag.message,
Expand Down
58 changes: 48 additions & 10 deletions src/scmrepo/git/backend/pygit2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@


if TYPE_CHECKING:
from pygit2 import Signature
from pygit2.remote import Remote # type: ignore
from pygit2.repository import Repository

Expand Down Expand Up @@ -124,12 +125,33 @@ def _resolve_refish(self, refish: str):
return commit, ref

@property
def default_signature(self):
def default_signature(self) -> "Signature":
try:
return self.repo.default_signature
except KeyError as exc:
raise SCMError("Git username and email must be configured") from exc

@property
def author(self) -> "Signature":
return self._get_signature("GIT_AUTHOR")

@property
def committer(self) -> "Signature":
return self._get_signature("GIT_COMMITTER")

def _get_signature(self, name: str) -> "Signature":
from pygit2 import Signature

sig = self.default_signature
if os.environ.get(f"{name}_DATE"):
raise NotImplementedError("signature date override unsupported")
return Signature(
name=os.environ.get(f"{name}_NAME", sig.name),
email=os.environ.get(f"{name}_EMAIL", sig.email),
time=sig.time,
offset=sig.offset,
)

@staticmethod
def _get_checkout_strategy(strategy: Optional[int] = None):
from pygit2 import (
Expand Down Expand Up @@ -274,17 +296,24 @@ def branch(self, branch: str):
except GitError as exc:
raise SCMError(f"Failed to create branch '{branch}'") from exc

def tag(self, tag: str, annotated: bool = False, message: Optional[str] = None):
def tag(
self,
tag: str,
target: Optional[str] = None,
annotated: bool = False,
message: Optional[str] = None,
):
from pygit2 import GIT_OBJ_COMMIT, GitError

if annotated and not message:
raise SCMError("message is required for annotated tag")
target_obj = self.repo.revparse_single(target or "HEAD")
with reraise(GitError, SCMError("Failed to create tag")):
self.repo.create_tag(
tag,
self.repo.head.target,
target_obj.id,
GIT_OBJ_COMMIT,
self.default_signature,
self.committer,
message or "",
)

Expand All @@ -308,10 +337,12 @@ def active_branch(self) -> str:
return self.repo.head.shorthand

def list_branches(self) -> Iterable[str]:
raise NotImplementedError
base = "refs/heads/"
return sorted(ref[len(base) :] for ref in self.iter_refs(base))

def list_tags(self) -> Iterable[str]:
raise NotImplementedError
base = "refs/tags/"
return sorted(ref[len(base) :] for ref in self.iter_refs(base))

def list_all_commits(self) -> Iterable[str]:
raise NotImplementedError
Expand Down Expand Up @@ -356,6 +387,12 @@ def resolve_commit(self, rev: str) -> "GitCommit":
commit.commit_time_offset,
commit.message,
[str(parent) for parent in commit.parent_ids],
commit.committer.name,
commit.committer.email,
commit.author.name,
commit.author.email,
commit.author.time,
commit.author.offset,
)

def _get_stash(self, ref: str):
Expand Down Expand Up @@ -591,7 +628,7 @@ def _stash_push(

try:
oid = self.repo.stash(
self.default_signature,
self.committer,
message=message,
include_untracked=include_untracked,
)
Expand Down Expand Up @@ -876,12 +913,11 @@ def _merge_ff(self, rev: str, obj) -> str:
def _merge_commit(self, msg: Optional[str], obj) -> str:
if not msg:
raise SCMError("Merge commit message is required")
user = self.default_signature
tree = self.repo.index.write_tree()
merge_commit = self.repo.create_commit(
"HEAD",
user,
user,
self.author,
self.committer,
msg,
tree,
[self.repo.head.target, obj.id],
Expand Down Expand Up @@ -910,6 +946,8 @@ def get_tag(self, name: str) -> Optional[Union[str, "GitTag"]]:
tag.name,
str(tag.oid),
str(tag.target),
tag.tagger.name,
tag.tagger.email,
tag.tagger.time,
tag.tagger.offset,
tag.message,
Expand Down
8 changes: 8 additions & 0 deletions src/scmrepo/git/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,13 +162,21 @@ class GitCommit:
commit_time_offset: int
message: str
parents: List[str]
committer_name: str
committer_email: str
author_name: str
author_email: str
author_time: int
author_time_offset: int


@dataclass
class GitTag:
name: str
hexsha: str # SHA for the tag object itself
target: str # SHA for the object the tag points to
tagger_name: str
tagger_email: str
tag_time: int
tag_time_offset: int
message: str