diff --git a/src/scmrepo/git/__init__.py b/src/scmrepo/git/__init__.py index 5a9fad99..6b9265aa 100644 --- a/src/scmrepo/git/__init__.py +++ b/src/scmrepo/git/__init__.py @@ -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: diff --git a/src/scmrepo/git/backend/base.py b/src/scmrepo/git/backend/base.py index f68ee908..e0188590 100644 --- a/src/scmrepo/git/backend/base.py +++ b/src/scmrepo/git/backend/base.py @@ -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 diff --git a/src/scmrepo/git/backend/dulwich/__init__.py b/src/scmrepo/git/backend/dulwich/__init__.py index 4be03622..3674a14b 100644 --- a/src/scmrepo/git/backend/dulwich/__init__.py +++ b/src/scmrepo/git/backend/dulwich/__init__.py @@ -2,6 +2,7 @@ import locale import logging import os +import re import stat from contextlib import closing from functools import partial @@ -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: @@ -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, ) @@ -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 @@ -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.+)\s+<(?P.+)>") + + +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") diff --git a/src/scmrepo/git/backend/gitpython.py b/src/scmrepo/git/backend/gitpython.py index 83b3ceb8..0967a478 100644 --- a/src/scmrepo/git/backend/gitpython.py +++ b/src/scmrepo/git/backend/gitpython.py @@ -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 @@ -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( @@ -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, diff --git a/src/scmrepo/git/backend/pygit2/__init__.py b/src/scmrepo/git/backend/pygit2/__init__.py index 277cc26f..f35da8a6 100644 --- a/src/scmrepo/git/backend/pygit2/__init__.py +++ b/src/scmrepo/git/backend/pygit2/__init__.py @@ -30,6 +30,7 @@ if TYPE_CHECKING: + from pygit2 import Signature from pygit2.remote import Remote # type: ignore from pygit2.repository import Repository @@ -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 ( @@ -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 "", ) @@ -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 @@ -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): @@ -591,7 +628,7 @@ def _stash_push( try: oid = self.repo.stash( - self.default_signature, + self.committer, message=message, include_untracked=include_untracked, ) @@ -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], @@ -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, diff --git a/src/scmrepo/git/objects.py b/src/scmrepo/git/objects.py index 9cc874bd..25a0afc8 100644 --- a/src/scmrepo/git/objects.py +++ b/src/scmrepo/git/objects.py @@ -162,6 +162,12 @@ 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 @@ -169,6 +175,8 @@ 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