From 2c2f74c2ae95c007bf8c32bdff6310aca4721ffa Mon Sep 17 00:00:00 2001 From: Ian Duffy Date: Sat, 24 Oct 2026 18:49:48 +0100 Subject: [PATCH] fix(no-ticket): treat an empty trailing OWNER/REPO part as not given `cloudsmith repos get myorg/` sent a request for a repository with an empty name. The validator now drops empty optional trailing parts, so the command lists the `myorg` namespace. An empty OWNER before a REPO gives a usage error. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 4 ++++ cloudsmith_cli/cli/tests/test_validators.py | 24 +++++++++++++++++++++ cloudsmith_cli/cli/validators.py | 24 +++++++++++++++------ 3 files changed, 45 insertions(+), 7 deletions(-) create mode 100644 cloudsmith_cli/cli/tests/test_validators.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 0127743c..05c08259 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - After a SAML login, `cloudsmith auth` saves the Workspace to `config.ini`. It writes to the `[profile:NAME]` section of the selected profile, or to `[default]` when you do not select a profile. The CLI writes to the file or directory that `--config-file` gives, or else to your user-level `config.ini`. If the `config.ini` does not exist, the CLI creates it. +### Fixed + +- An empty optional trailing part in an `OWNER/REPO` argument now means that you did not give the part. For example, `cloudsmith repos get myorg/` lists the repositories in `myorg`. Before, it requested a repository with an empty name and printed a row of `None` values. `cloudsmith metrics entitlements myorg/` now works the same as `myorg`, and a `push` target such as `myorg/myrepo/ubuntu/` works the same as `myorg/myrepo/ubuntu`. An empty `OWNER` before a `REPO`, such as `/myrepo`, now gives a usage error. + ## [1.30.0] - 2026-10-22 ### Added diff --git a/cloudsmith_cli/cli/tests/test_validators.py b/cloudsmith_cli/cli/tests/test_validators.py new file mode 100644 index 00000000..73138c17 --- /dev/null +++ b/cloudsmith_cli/cli/tests/test_validators.py @@ -0,0 +1,24 @@ +"""Tests for the OWNER/REPO path validators.""" + +import click +import pytest + +from ..validators import validate_optional_owner_repo, validate_owner_repo + + +def test_optional_owner_repo_drops_empty_trailing_repo(): + assert validate_optional_owner_repo(None, None, "myorg/") == ["myorg"] + + +def test_optional_owner_repo_treats_all_empty_parts_as_not_given(): + assert validate_optional_owner_repo(None, None, "/") is None + + +def test_optional_owner_repo_rejects_blank_owner(): + with pytest.raises(click.BadParameter, match="cannot be blank"): + validate_optional_owner_repo(None, None, "/myrepo") + + +def test_required_repo_left_blank_raises(): + with pytest.raises(click.BadParameter, match="cannot be blank"): + validate_owner_repo(None, None, "myorg/") diff --git a/cloudsmith_cli/cli/validators.py b/cloudsmith_cli/cli/validators.py index 6560b3e7..80b1d686 100644 --- a/cloudsmith_cli/cli/validators.py +++ b/cloudsmith_cli/cli/validators.py @@ -152,8 +152,12 @@ def validate_untrusted_proxy(proxy, allowed_suffixes=()): def validate_slashes( - param, value, minimum=2, maximum=None, form=None, allow_blank=False -): + param: click.Parameter | None, + value: str, + minimum: int = 2, + maximum: int | None = None, + form: str | None = None, +) -> list[str]: """Ensure that parameter has slashes and minimum parts.""" try: value = value.split("/") @@ -168,20 +172,26 @@ def validate_slashes( raise click.BadParameter(f"Must be in the form of {form}", param=param) value = [v.strip() for v in value] - if not allow_blank and not all(value): + # Shell completion leaves "OWNER/", so an empty optional trailing part is not given. + while len(value) > minimum and not value[-1]: + value.pop() + + if not all(value): raise click.BadParameter("Individual values cannot be blank", param=param) return value -def validate_optional_owner_repo(ctx, param, value): +def validate_optional_owner_repo( + ctx: click.Context | None, param: click.Parameter | None, value: str | None +) -> list[str] | None: """Ensure that owner/repo is formatted correctly, where owner and repo are optional.""" # pylint: disable=unused-argument form = "OWNER/REPO" + if value is None: + return None - return validate_slashes( - param, value, minimum=0, maximum=2, form=form, allow_blank=True - ) + return validate_slashes(param, value, minimum=0, maximum=2, form=form) or None def validate_required_owner_optional_repo(ctx, param, value):