diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 771b0af3ea..9f9bcfb2ef 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,7 +12,7 @@ repos: - id: no-commit-to-branch - repo: https://github.com/commitizen-tools/commitizen - rev: v2.28.1 # automatically updated by Commitizen + rev: v2.29.0 # automatically updated by Commitizen hooks: - id: commitizen stages: [commit-msg] diff --git a/CHANGELOG.md b/CHANGELOG.md index 0acfd67771..f269842919 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,16 @@ +## v2.29.0 (2022-07-22) + +### Feat + +- use chardet to get correct encoding +- **bump**: add signed tag support for bump command + +### Fix + +- avoid that pytest overrides existing gpg config +- **test**: set git to work with gpg + ## v2.28.1 (2022-07-22) ### Fix diff --git a/commitizen/__version__.py b/commitizen/__version__.py index f604b6f4ce..ccaf0f0336 100644 --- a/commitizen/__version__.py +++ b/commitizen/__version__.py @@ -1 +1 @@ -__version__ = "2.28.1" +__version__ = "2.29.0" diff --git a/commitizen/cli.py b/commitizen/cli.py index 0f2d52efc9..82b08db67f 100644 --- a/commitizen/cli.py +++ b/commitizen/cli.py @@ -160,6 +160,12 @@ "help": "create annotated tag instead of lightweight one", "action": "store_true", }, + { + "name": ["--gpg-sign", "-s"], + "help": "sign tag instead of lightweight one", + "default": False, + "action": "store_true", + }, { "name": ["--changelog-to-stdout"], "action": "store_true", diff --git a/commitizen/cmd.py b/commitizen/cmd.py index fb041c9faf..f65f5c8e76 100644 --- a/commitizen/cmd.py +++ b/commitizen/cmd.py @@ -1,6 +1,8 @@ import subprocess from typing import NamedTuple +import chardet + class Command(NamedTuple): out: str @@ -20,4 +22,10 @@ def run(cmd: str) -> Command: ) stdout, stderr = process.communicate() return_code = process.returncode - return Command(stdout.decode(), stderr.decode(), stdout, stderr, return_code) + return Command( + stdout.decode(chardet.detect(stdout)["encoding"] or "utf-8"), + stderr.decode(chardet.detect(stderr)["encoding"] or "utf-8"), + stdout, + stderr, + return_code, + ) diff --git a/commitizen/commands/bump.py b/commitizen/commands/bump.py index e0779cd625..17f929bdb6 100644 --- a/commitizen/commands/bump.py +++ b/commitizen/commands/bump.py @@ -40,6 +40,7 @@ def __init__(self, config: BaseConfig, arguments: dict): "prerelease", "increment", "bump_message", + "gpg_sign", "annotated_tag", ] if arguments[key] is not None @@ -231,6 +232,8 @@ def __call__(self): # noqa: C901 raise BumpCommitFailedError(f'2nd git.commit error: "{c.err.strip()}"') c = git.tag( new_tag_version, + signed=self.bump_settings.get("gpg_sign", False) + or bool(self.config.settings.get("gpg_sign", False)), annotated=self.bump_settings.get("annotated_tag", False) or bool(self.config.settings.get("annotated_tag", False)), ) diff --git a/commitizen/git.py b/commitizen/git.py index 98ee40f2f2..3c86135f8c 100644 --- a/commitizen/git.py +++ b/commitizen/git.py @@ -1,4 +1,5 @@ import os +import re from pathlib import Path from tempfile import NamedTemporaryFile from typing import List, Optional @@ -58,8 +59,13 @@ def from_line(cls, line: str, inner_delimiter: str) -> "GitTag": return cls(name=name, rev=obj, date=date) -def tag(tag: str, annotated: bool = False) -> cmd.Command: - c = cmd.run(f"git tag -a {tag} -m {tag}" if annotated else f"git tag {tag}") +def tag(tag: str, annotated: bool = False, signed: bool = False) -> cmd.Command: + _opt = "" + if annotated: + _opt = f"-a {tag} -m" + if signed: + _opt = f"-s {tag} -m" + c = cmd.run(f"git tag {_opt} {tag}") return c @@ -136,6 +142,14 @@ def tag_exist(tag: str) -> bool: return tag in c.out +def is_signed_tag(tag: str) -> bool: + c = cmd.run(f"git tag -v {tag}") + _ret = False + if re.match("gpg: Signature made [0-9/:A-Za-z ]*", c.err): + _ret = True + return _ret + + def get_latest_tag_name() -> Optional[str]: c = cmd.run("git describe --abbrev=0 --tags") if c.err: diff --git a/docs/bump.md b/docs/bump.md index 8062044a76..bdccb30fbe 100644 --- a/docs/bump.md +++ b/docs/bump.md @@ -58,7 +58,7 @@ usage: cz bump [-h] [--dry-run] [--files-only] [--local-version] [--changelog] [--no-verify] [--yes] [--tag-format TAG_FORMAT] [--bump-message BUMP_MESSAGE] [--prerelease {alpha,beta,rc}] [--increment {MAJOR,MINOR,PATCH}] [--check-consistency] - [--annotated-tag] [--changelog-to-stdout] [--retry] + [--annotated-tag] [--gpg-sign] [--changelog-to-stdout] [--retry] options: -h, --help show this help message and exit @@ -82,6 +82,7 @@ options: --check-consistency, -cc check consistency among versions defined in commitizen configuration and version_files + --gpg-sign, -s create a signed tag instead of lightweight one or annotated tag --annotated-tag, -at create annotated tag instead of lightweight one --changelog-to-stdout Output changelog to the stdout @@ -361,6 +362,16 @@ When set to `true` commitizen will create annotated tags. ```toml [tool.commitizen] + +--- + +### `gpg_sign` + +When set to `true` commitizen will create gpg signed tags. + +```toml +[tool.commitizen] +gpg_sign = true annotated_tag = true ``` diff --git a/docs/config.md b/docs/config.md index 6a7838e245..6ac36914b0 100644 --- a/docs/config.md +++ b/docs/config.md @@ -123,12 +123,13 @@ commitizen: ## Settings | Variable | Type | Default | Description | -| -------------------------- | ------ | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +|----------------------------| ------ | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | `str` | `"cz_conventional_commits"` | Name of the committing rules to use | | `version` | `str` | `None` | Current version. Example: "0.1.2" | | `version_files` | `list` | `[ ]` | Files were the version will be updated. A pattern to match a line, can also be specified, separated by `:` [See more][version_files] | | `tag_format` | `str` | `None` | Format for the git tag, useful for old projects, that use a convention like `"v1.2.1"`. [See more][tag_format] | | `update_changelog_on_bump` | `bool` | `false` | Create changelog when running `cz bump` | +| `gpg_sign` | `bool` | `false` | Use gpg signed tags instead of lightweight tags. | | `annotated_tag` | `bool` | `false` | Use annotated tags instead of lightweight tags. [See difference][annotated-tags-vs-lightweight] | | `bump_message` | `str` | `None` | Create custom commit message, useful to skip ci. [See more][bump_message] | | `allow_abort` | `bool` | `false` | Disallow empty commit messages, useful in ci. [See more][allow_abort] | diff --git a/pyproject.toml b/pyproject.toml index 5bec7cfb46..a832db6dbd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [tool.commitizen] -version = "2.28.1" +version = "2.29.0" tag_format = "v$version" version_files = [ "pyproject.toml:version", @@ -30,7 +30,7 @@ exclude = ''' [tool.poetry] name = "commitizen" -version = "2.28.1" +version = "2.29.0" description = "Python commitizen client tool" authors = ["Santiago Fraire "] license = "MIT" @@ -56,6 +56,7 @@ jinja2 = ">=2.10.3" pyyaml = ">=3.08" argcomplete = "^1.12.1" typing-extensions = "^4.0.1" +chardet = "^5.0.0" [tool.poetry.dev-dependencies] ipython = "^7.2" @@ -81,6 +82,7 @@ mkdocs = "^1.0" mkdocs-material = "^4.1" pydocstyle = "^5.0.2" pytest-xdist = "^2.5.0" +types-chardet = "^5.0.2" [tool.poetry.scripts] cz = "commitizen.cli:main" diff --git a/tests/commands/test_bump_command.py b/tests/commands/test_bump_command.py index da15174c5d..daf3d42bac 100644 --- a/tests/commands/test_bump_command.py +++ b/tests/commands/test_bump_command.py @@ -66,6 +66,24 @@ def test_bump_minor_increment_annotated(commit_msg, mocker): cmd_res = cmd.run('git for-each-ref refs/tags --format "%(objecttype):%(refname)"') assert tag_exists is True and "tag:refs/tags/0.2.0\n" in cmd_res.out + _is_signed = git.is_signed_tag("0.2.0") + assert _is_signed is False + + +@pytest.mark.parametrize("commit_msg", ("feat: new file", "feat(user): new file")) +@pytest.mark.usefixtures("tmp_commitizen_project_with_gpg") +def test_bump_minor_increment_signed(commit_msg, mocker): + create_file_and_commit(commit_msg) + testargs = ["cz", "bump", "--yes", "--gpg-sign"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + tag_exists = git.tag_exist("0.2.0") + cmd_res = cmd.run('git for-each-ref refs/tags --format "%(objecttype):%(refname)"') + assert tag_exists is True and "tag:refs/tags/0.2.0\n" in cmd_res.out + + _is_signed = git.is_signed_tag("0.2.0") + assert _is_signed is True + @pytest.mark.parametrize("commit_msg", ("feat: new file", "feat(user): new file")) def test_bump_minor_increment_annotated_config_file( @@ -83,6 +101,27 @@ def test_bump_minor_increment_annotated_config_file( cmd_res = cmd.run('git for-each-ref refs/tags --format "%(objecttype):%(refname)"') assert tag_exists is True and "tag:refs/tags/0.2.0\n" in cmd_res.out + _is_signed = git.is_signed_tag("0.2.0") + assert _is_signed is False + + +@pytest.mark.parametrize("commit_msg", ("feat: new file", "feat(user): new file")) +def test_bump_minor_increment_signed_config_file( + commit_msg, mocker, tmp_commitizen_project_with_gpg +): + tmp_commitizen_cfg_file = tmp_commitizen_project_with_gpg.join("pyproject.toml") + tmp_commitizen_cfg_file.write(f"{tmp_commitizen_cfg_file.read()}\n" f"gpg_sign = 1") + create_file_and_commit(commit_msg) + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + tag_exists = git.tag_exist("0.2.0") + cmd_res = cmd.run('git for-each-ref refs/tags --format "%(objecttype):%(refname)"') + assert tag_exists is True and "tag:refs/tags/0.2.0\n" in cmd_res.out + + _is_signed = git.is_signed_tag("0.2.0") + assert _is_signed is True + @pytest.mark.usefixtures("tmp_commitizen_project") @pytest.mark.parametrize( diff --git a/tests/conftest.py b/tests/conftest.py index aec79c2db3..9b91daf9ba 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,5 @@ import os +import re import pytest @@ -23,6 +24,38 @@ def tmp_commitizen_project(tmp_git_project): yield tmp_git_project +@pytest.fixture(scope="function") +def tmp_commitizen_project_with_gpg(tmp_commitizen_project): + _gpg_file = tmp_commitizen_project.join("gpg_setup") + _signer_mail = "action@github.com" + with open(_gpg_file, "w", newline="") as f: + f.write("Key-Type: RSA" + os.linesep) + f.write("Key-Length: 2048" + os.linesep) + f.write("Subkey-Type: RSA" + os.linesep) + f.write("Subkey-Length: 2048" + os.linesep) + f.write("Name-Real: GitHub Action" + os.linesep) + f.write("Name-Comment: with stupid passphrase" + os.linesep) + f.write(f"Name-Email: {_signer_mail}" + os.linesep) + f.write("Expire-Date: 1" + os.linesep) + + cmd.run( + f"gpg --batch --passphrase '' --pinentry-mode loopback --generate-key {_gpg_file}" + ) + + _new_key = cmd.run(f"gpg --list-secret-keys {_signer_mail}") + _m = re.search( + rf"[a-zA-Z0-9 \[\]-_]*{os.linesep}[ ]*([0-9A-Za-z]*){os.linesep}[{os.linesep}a-zA-Z0-9 \[\]-_<>@]*", + _new_key.out, + ) + + if _m: + _key_id = _m.group(1) + cmd.run("git config commit.gpgsign true") + cmd.run(f"git config user.signingkey {_key_id}") + + yield tmp_commitizen_project + + @pytest.fixture() def config(): _config = BaseConfig()