Add a first-class API to get the latest tag from a remote via git ls-remote#2119
Conversation
…git ls-remote` Signed-off-by: Nguyen Huy Hoang <[email protected]>
There was a problem hiding this comment.
Pull request overview
Adds a first-class convenience API on git.cmd.Git to fetch the newest tag from a remote by running git ls-remote and parsing its output, so callers don’t need to manually parse tab-delimited results.
Changes:
- Add
Git.latest_remote_tag(repository)that callsls-remote --tags --sort=-version:refname. - Parse
ls-remoteoutput to return the first valid tag, handling empty output, malformed lines, and peeled^{}annotated-tag refs.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def latest_remote_tag(self, repository: PathLike) -> Optional[str]: | ||
| output = self.ls_remote("--tags", "--sort=-version:refname", repository) |
There was a problem hiding this comment.
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).
| 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) |
| _warn_use_shell(extra_danger=False) | ||
| return super().__getattribute__(name) | ||
|
|
||
| def latest_remote_tag(self, repository: PathLike) -> Optional[str]: |
There was a problem hiding this comment.
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.
| 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. | |
| """ |
| def latest_remote_tag(self, repository: PathLike) -> Optional[str]: | ||
| output = self.ls_remote("--tags", "--sort=-version:refname", repository) | ||
| 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 |
There was a problem hiding this comment.
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.
Summary
Introduce a dedicated convenience method on
Gitfor the issue use case (querying a remote URL/repo and returning its newest tag). This avoids requiring users to manually runls_remote(...)and parse tab-delimited output themselves. The method should encapsulate stable parsing behavior and edge-case handling (empty output, annotated tag peeled refs, malformed lines).Files changed
git/cmd.py(modified)Testing
Closes #1071