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
1 change: 1 addition & 0 deletions src/scmrepo/git/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,7 @@ def fetch_refspecs(
merge = partialmethod(_backend_func, "merge")
validate_git_remote = partialmethod(_backend_func, "validate_git_remote")
check_ref_format = partialmethod(_backend_func, "check_ref_format")
get_tag = partialmethod(_backend_func, "get_tag")

get_tree_obj = partialmethod(_backend_func, "get_tree_obj")

Expand Down
17 changes: 15 additions & 2 deletions src/scmrepo/git/backend/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
if TYPE_CHECKING:
from scmrepo.progress import GitProgressEvent

from ..objects import GitCommit
from ..objects import GitCommit, GitTag


class NoGitBackendError(SCMError):
Expand Down Expand Up @@ -110,7 +110,7 @@ def branch(self, branch: str):
pass

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

@abstractmethod
Expand Down Expand Up @@ -391,3 +391,16 @@ def validate_git_remote(self, url: str, **kwargs):
@abstractmethod
def check_ref_format(self, refname: str) -> bool:
"""Check if a reference name is well formed."""

@abstractmethod
def get_tag(self, name: str) -> Optional[Union[str, "GitTag"]]:
"""Return the specified tag object.

Args:
name: Tag name (without 'refs/tags/' prefix).

Returns:
None if the specified tag does not exist.
String SHA for the target object if the tag is a lightweight tag.
GitTag object if the tag is an annotated tag.
"""
37 changes: 34 additions & 3 deletions src/scmrepo/git/backend/dulwich/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from scmrepo.progress import GitProgressReporter
from scmrepo.utils import relpath

from ...objects import GitObject
from ...objects import GitObject, GitTag
from ..base import BaseGitBackend, SyncStatus

if TYPE_CHECKING:
Expand Down Expand Up @@ -390,8 +390,18 @@ def branch(self, branch: str):
except Error as exc:
raise SCMError(f"Failed to create branch '{branch}'") from exc

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

if annotated and not message:
raise SCMError("message is required for annotated tag")
with reraise(Error, SCMError("Failed to create tag")):
tag_create(
self.repo,
os.fsencode(tag),
annotated=annotated,
message=message.encode("utf-8") if message else None,
)

def untracked_files(self) -> Iterable[str]:
_staged, _unstaged, untracked = self.status()
Expand Down Expand Up @@ -868,3 +878,24 @@ def check_ref_format(self, refname: str) -> bool:
from dulwich.refs import check_ref_format

return check_ref_format(refname.encode())

def get_tag(self, name: str) -> Optional[Union[str, "GitTag"]]:
from dulwich.objects import Tag

name_b = os.fsencode(f"refs/tags/{name}")
try:
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
return GitTag(
os.fsdecode(tag.name),
tag.id,
target_sha.decode("ascii"),
tag.tag_time,
tag.tag_timezone,
tag.message.decode("utf-8"),
)
return os.fsdecode(ref)
26 changes: 23 additions & 3 deletions src/scmrepo/git/backend/gitpython.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
)
from scmrepo.utils import relpath

from ..objects import GitCommit, GitObject
from ..objects import GitCommit, GitObject, GitTag
from .base import BaseGitBackend, SyncStatus

if TYPE_CHECKING:
Expand Down Expand Up @@ -294,8 +294,10 @@ def push(self):
def branch(self, branch):
self.repo.git.branch(branch)

def tag(self, tag):
self.repo.git.tag(tag)
def tag(self, tag: str, 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)

def untracked_files(self):
files = self.repo.untracked_files
Expand Down Expand Up @@ -682,3 +684,21 @@ def validate_git_remote(self, url: str, **kwargs):

def check_ref_format(self, refname: str):
raise NotImplementedError

def get_tag(self, name: str) -> Optional[Union[str, "GitTag"]]:
try:
ref = self.repo.tags[name]
if not ref.tag:
return ref.commit.hexsha
tag = ref.tag
return GitTag(
tag.tag,
tag.hexsha,
tag.object.hexsha,
tag.tagged_date,
tag.tagger_tz_offset,
tag.message,
)
except IndexError:
pass
return None
41 changes: 38 additions & 3 deletions src/scmrepo/git/backend/pygit2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

from scmrepo.exceptions import CloneError, MergeConflictError, RevError, SCMError
from scmrepo.git.backend.base import BaseGitBackend, SyncStatus
from scmrepo.git.objects import GitCommit, GitObject
from scmrepo.git.objects import GitCommit, GitObject, GitTag
from scmrepo.utils import relpath

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -274,8 +274,19 @@ def branch(self, branch: str):
except GitError as exc:
raise SCMError(f"Failed to create branch '{branch}'") from exc

def tag(self, tag: str):
raise NotImplementedError
def tag(self, tag: str, 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")
with reraise(GitError, SCMError("Failed to create tag")):
self.repo.create_tag(
tag,
self.repo.head.target,
GIT_OBJ_COMMIT,
self.default_signature,
message or "",
)

def untracked_files(self) -> Iterable[str]:
raise NotImplementedError
Expand Down Expand Up @@ -882,3 +893,27 @@ def validate_git_remote(self, url: str, **kwargs):

def check_ref_format(self, refname: str):
raise NotImplementedError

def get_tag(self, name: str) -> Optional[Union[str, "GitTag"]]:
from pygit2 import InvalidSpecError, Tag

try:
ref = self.repo.references.get(f"refs/tags/{name}")
except InvalidSpecError:
return None
if not ref:
return None
try:
tag = self.repo[ref.target]
if isinstance(tag, Tag):
return GitTag(
tag.name,
str(tag.oid),
str(tag.target),
tag.tagger.time,
tag.tagger.offset,
tag.message,
)
except KeyError:
pass
return str(ref.target)
10 changes: 10 additions & 0 deletions src/scmrepo/git/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,13 @@ class GitCommit:
commit_time_offset: int
message: str
parents: List[str]


@dataclass
class GitTag:
name: str
hexsha: str # SHA for the tag object itself
target: str # SHA for the object the tag points to
tag_time: int
tag_time_offset: int
message: str
32 changes: 31 additions & 1 deletion tests/test_git.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

from scmrepo.exceptions import InvalidRemote, MergeConflictError, RevError, SCMError
from scmrepo.git import Git
from scmrepo.git.objects import GitTag

from .conftest import backends

Expand Down Expand Up @@ -201,7 +202,7 @@ def test_get_ref(tmp_dir: TmpDir, scm: Git, git: Git):
os.path.join(".git", "refs", "foo", "baz"): "ref: refs/heads/master",
}
)
scm.tag(["-a", "annotated", "-m", "Annotated Tag"])
scm.tag("annotated", annotated=True, message="Annotated Tag")

assert init_rev == git.get_ref("refs/foo/bar")
assert init_rev == git.get_ref("refs/foo/baz")
Expand Down Expand Up @@ -1099,3 +1100,32 @@ def test_backend_func(
mock = mocker.spy(backend, "add")
scm.add(["foo"])
mock.assert_called_once_with(["foo"])


def test_tag(tmp_dir: TmpDir, scm: Git, git: Git):
tmp_dir.gen("foo", "foo")
scm.add_commit("foo", message="init")
rev = scm.get_rev()

git.tag("lightweight")
assert scm.resolve_rev("lightweight") == rev

git.tag("annotated", annotated=True, message="message")
assert scm.resolve_rev("annotated") == rev


def test_get_tag(tmp_dir, scm: Git, git: Git):
tmp_dir.gen("foo", "foo")
scm.add_commit("foo", message="init")
rev = scm.get_rev()

assert git.get_tag("nonexistent") is None

scm.tag("lightweight")
assert git.get_tag("lightweight") == rev

scm.tag("annotated", annotated=True, message="message")
tag = git.get_tag("annotated")
assert isinstance(tag, GitTag)
assert tag.target == rev
assert tag.message.strip() == "message"
2 changes: 1 addition & 1 deletion tests/test_pygit2.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def test_pygit_resolve_refish(tmp_dir: TmpDir, scm: Git, use_sha: str):
scm.add_commit("foo", message="foo")
head = scm.get_rev()
tag = "my_tag"
scm.gitpython.git.tag("-a", tag, "-m", "create annotated tag")
scm.tag(tag, annotated=True, message="create annotated tag")

if use_sha:
# refish will be annotated tag SHA (not commit SHA)
Expand Down