From 2848b8b1b2c6c58aab6eccf598c73955dd739250 Mon Sep 17 00:00:00 2001 From: Peter Rowlands Date: Tue, 1 Aug 2023 16:28:15 +0900 Subject: [PATCH 1/6] git: implement list_tags/branches in all backends --- src/scmrepo/git/backend/dulwich/__init__.py | 13 +++++++++++-- src/scmrepo/git/backend/pygit2/__init__.py | 6 ++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/scmrepo/git/backend/dulwich/__init__.py b/src/scmrepo/git/backend/dulwich/__init__.py index 4be03622..c285ec97 100644 --- a/src/scmrepo/git/backend/dulwich/__init__.py +++ b/src/scmrepo/git/backend/dulwich/__init__.py @@ -423,10 +423,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 diff --git a/src/scmrepo/git/backend/pygit2/__init__.py b/src/scmrepo/git/backend/pygit2/__init__.py index 277cc26f..29414508 100644 --- a/src/scmrepo/git/backend/pygit2/__init__.py +++ b/src/scmrepo/git/backend/pygit2/__init__.py @@ -308,10 +308,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 From a20eb8160798a3e97eb70a81a9cdc0915e397a32 Mon Sep 17 00:00:00 2001 From: Peter Rowlands Date: Tue, 1 Aug 2023 16:43:17 +0900 Subject: [PATCH 2/6] pygit2: support reading GIT_COMMITTER name/email from environment --- src/scmrepo/git/backend/pygit2/__init__.py | 35 ++++++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/src/scmrepo/git/backend/pygit2/__init__.py b/src/scmrepo/git/backend/pygit2/__init__.py index 29414508..15a56f2b 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,35 @@ 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 + sig.name = os.environ.get(f"{name}_NAME", sig.name) + sig.email = os.environ.get(f"{name}_EMAIL", sig.email) + 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 ( @@ -284,7 +308,7 @@ def tag(self, tag: str, annotated: bool = False, message: Optional[str] = None): tag, self.repo.head.target, GIT_OBJ_COMMIT, - self.default_signature, + self.committer, message or "", ) @@ -593,7 +617,7 @@ def _stash_push( try: oid = self.repo.stash( - self.default_signature, + self.committer, message=message, include_untracked=include_untracked, ) @@ -878,12 +902,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], From 743450184f52e2f5c96bc523e5957083321fb620 Mon Sep 17 00:00:00 2001 From: Peter Rowlands Date: Tue, 1 Aug 2023 16:52:36 +0900 Subject: [PATCH 3/6] git: support tagging a specific target object or commit --- src/scmrepo/git/backend/base.py | 8 +++++++- src/scmrepo/git/backend/dulwich/__init__.py | 9 ++++++++- src/scmrepo/git/backend/gitpython.py | 10 ++++++++-- src/scmrepo/git/backend/pygit2/__init__.py | 13 +++++++++---- 4 files changed, 32 insertions(+), 8 deletions(-) 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 c285ec97..2611db48 100644 --- a/src/scmrepo/git/backend/dulwich/__init__.py +++ b/src/scmrepo/git/backend/dulwich/__init__.py @@ -390,7 +390,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 +405,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, ) diff --git a/src/scmrepo/git/backend/gitpython.py b/src/scmrepo/git/backend/gitpython.py index 83b3ceb8..7d88a5d5 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 diff --git a/src/scmrepo/git/backend/pygit2/__init__.py b/src/scmrepo/git/backend/pygit2/__init__.py index 15a56f2b..6e0f804b 100644 --- a/src/scmrepo/git/backend/pygit2/__init__.py +++ b/src/scmrepo/git/backend/pygit2/__init__.py @@ -143,8 +143,6 @@ def _get_signature(self, name: str) -> "Signature": from pygit2 import Signature sig = self.default_signature - sig.name = os.environ.get(f"{name}_NAME", sig.name) - sig.email = os.environ.get(f"{name}_EMAIL", sig.email) if os.environ.get(f"{name}_DATE"): raise NotImplementedError("signature date override unsupported") return Signature( @@ -298,15 +296,22 @@ 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.committer, message or "", From bbb2750d3cca5bdab392157fd872a802646cc41f Mon Sep 17 00:00:00 2001 From: Peter Rowlands Date: Tue, 1 Aug 2023 17:11:49 +0900 Subject: [PATCH 4/6] git: return tagger identity in get_tag() --- src/scmrepo/git/backend/dulwich/__init__.py | 14 ++++++++++++++ src/scmrepo/git/backend/gitpython.py | 2 ++ src/scmrepo/git/backend/pygit2/__init__.py | 2 ++ src/scmrepo/git/objects.py | 2 ++ 4 files changed, 20 insertions(+) diff --git a/src/scmrepo/git/backend/dulwich/__init__.py b/src/scmrepo/git/backend/dulwich/__init__.py index 2611db48..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 @@ -906,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 7d88a5d5..0c572349 100644 --- a/src/scmrepo/git/backend/gitpython.py +++ b/src/scmrepo/git/backend/gitpython.py @@ -701,6 +701,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 6e0f804b..8b01c102 100644 --- a/src/scmrepo/git/backend/pygit2/__init__.py +++ b/src/scmrepo/git/backend/pygit2/__init__.py @@ -940,6 +940,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..5ed06429 100644 --- a/src/scmrepo/git/objects.py +++ b/src/scmrepo/git/objects.py @@ -169,6 +169,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 From e4935f90cb9220761a522ea0c891971df04be324 Mon Sep 17 00:00:00 2001 From: Peter Rowlands Date: Tue, 8 Aug 2023 18:15:18 +0900 Subject: [PATCH 5/6] GitCommit: include commit author/committer signatures --- src/scmrepo/git/backend/gitpython.py | 6 ++++++ src/scmrepo/git/backend/pygit2/__init__.py | 6 ++++++ src/scmrepo/git/objects.py | 6 ++++++ 3 files changed, 18 insertions(+) diff --git a/src/scmrepo/git/backend/gitpython.py b/src/scmrepo/git/backend/gitpython.py index 0c572349..0967a478 100644 --- a/src/scmrepo/git/backend/gitpython.py +++ b/src/scmrepo/git/backend/gitpython.py @@ -398,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( diff --git a/src/scmrepo/git/backend/pygit2/__init__.py b/src/scmrepo/git/backend/pygit2/__init__.py index 8b01c102..f35da8a6 100644 --- a/src/scmrepo/git/backend/pygit2/__init__.py +++ b/src/scmrepo/git/backend/pygit2/__init__.py @@ -387,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): diff --git a/src/scmrepo/git/objects.py b/src/scmrepo/git/objects.py index 5ed06429..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 From b9c633e9fdbc3147603fc8a8701f773413231adb Mon Sep 17 00:00:00 2001 From: Peter Rowlands Date: Tue, 8 Aug 2023 18:15:47 +0900 Subject: [PATCH 6/6] git: import objects with scmrepo.git --- src/scmrepo/git/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) 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: