diff --git a/CHANGELOG.md b/CHANGELOG.md index eab040e9..6042260d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - **Breaking:** `cloudsmith login`, `cloudsmith logout`, and `--save-config` on `cloudsmith auth` and `cloudsmith tokens` now set `credentials.ini` to mode `0600`. Only your user can read or change the file and its API key. Previously, the CLI created the file with mode `0660`, so all members of your group could read and change the API key. When the CLI writes an existing `credentials.ini` that you own, it also sets the mode to `0600`. The CLI does not change the mode when it only reads the file. Other users in your group can no longer read a `credentials.ini` that the CLI writes. On Windows, the CLI does not change the access to the file. - `cloudsmith copy`, `move`, and `resync` with `--skip-errors` now ignore an API error, the same as `cloudsmith push`. The CLI prints the error one time, then `Skipping error and moving on.`, and exits with code 0. Previously, the CLI also printed the error a second time as an unexpected error (`Error: ApiException: ...`), sent an error report, and exited with code 1. - The `--skip-errors` help text no longer says "when copying packages" for `move`, `resync`, and `push`. +- 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. - `cloudsmith credential-helper install pnpm`, `uninstall pnpm`, and `list` now read and write `.npmrc` as UTF-8, as npm and pnpm do. On Windows, a home directory with characters such as Cyrillic or CJK letters caused a `UnicodeEncodeError`, and UTF-8 text in `.npmrc` was read incorrectly. The CLI keeps bytes that are not UTF-8 unchanged, and it reads a `.npmrc` that starts with a UTF-8 byte order mark. ## [1.30.0] - 2026-10-22 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 d749d26c..81a360ea 100644 --- a/cloudsmith_cli/cli/validators.py +++ b/cloudsmith_cli/cli/validators.py @@ -158,8 +158,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("/") @@ -174,20 +178,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):