Skip to content
Open
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
22 changes: 22 additions & 0 deletions git/cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -995,6 +995,28 @@ def __getattribute__(self, name: str) -> Any:
_warn_use_shell(extra_danger=False)
return super().__getattribute__(name)

def latest_remote_tag(self, repository: PathLike) -> Optional[str]:
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new public convenience method has no docstring, but nearby public methods in Git are documented. Please add a docstring clarifying what “latest” means (it relies on --sort=-version:refname), how annotated tags are handled, and when None is returned.

Suggested change
def latest_remote_tag(self, repository: PathLike) -> Optional[str]:
def latest_remote_tag(self, repository: PathLike) -> Optional[str]:
"""Return the name of the latest tag from a remote repository.
The remote tags are obtained via :meth:`ls_remote` with
``--tags`` and ``--sort=-version:refname``. This means that tags
are sorted in descending order according to their version-like
refname, and the first suitable entry is considered the "latest".
Annotated tags are handled by normalizing peeled references:
``ls-remote --tags`` may return both ``refs/tags/<tag>`` and
``refs/tags/<tag>^{}; this method strips the ``^{}`
suffix so that the underlying tag name ``<tag>`` is returned.
:param repository: Remote repository to query, as accepted by
:meth:`ls_remote` (for example, a URL or remote name).
:return: The name of the latest tag according to
``--sort=-version:refname``, or ``None`` if the remote has no
tags or no tag references can be parsed from the command
output.
"""

Copilot uses AI. Check for mistakes.
output = self.ls_remote("--tags", "--sort=-version:refname", repository)
Comment on lines +998 to +999
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

repository is passed as a positional argument without a -- separator. If the value begins with -, git will treat it as an option (option injection), which is especially problematic for commands like ls-remote (e.g., it could alter config/transport behavior). Consider inserting -- before repository and also mirroring other URL-taking APIs by rejecting unsafe ext::-style transports by default (with an allow_unsafe_protocols escape hatch).

Suggested change
def latest_remote_tag(self, repository: PathLike) -> Optional[str]:
output = self.ls_remote("--tags", "--sort=-version:refname", repository)
def latest_remote_tag(
self,
repository: PathLike,
allow_unsafe_protocols: bool = False,
) -> Optional[str]:
repo_str = os.fspath(repository)
if repo_str.startswith("ext::") and not allow_unsafe_protocols:
raise UnsafeProtocolError(
f"Refusing to use unsafe protocol for repository {repo_str!r}"
)
output = self.ls_remote("--tags", "--sort=-version:refname", "--", repo_str)

Copilot uses AI. Check for mistakes.
if not output:
return None

for line in output.splitlines():
if not line:
continue
try:
_, ref = line.split("\t", 1)
except ValueError:
continue
if not ref.startswith("refs/tags/"):
continue
tag = ref[len("refs/tags/") :]
if tag.endswith("^{}"):
tag = tag[:-3]
if tag:
return tag

return None
Comment on lines +998 to +1018
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This introduces new parsing/selection behavior (handling empty output, malformed lines, and peeled ^{} refs) but there are no tests covering it. Since the project already has extensive Git wrapper tests (e.g. test/test_git.py), please add unit tests that mock Git.execute/ls_remote output to cover: empty output, a peeled annotated tag appearing first, and malformed lines being ignored.

Copilot uses AI. Check for mistakes.

def __getattr__(self, name: str) -> Any:
"""A convenience method as it allows to call the command as if it was an object.

Expand Down
Loading