commitizen 4.14.0__tar.gz → 4.15.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. {commitizen-4.14.0 → commitizen-4.15.1}/PKG-INFO +1 -1
  2. commitizen-4.15.1/commitizen/__version__.py +1 -0
  3. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/bump.py +3 -3
  4. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/cli.py +37 -3
  5. commitizen-4.15.1/commitizen/cmd.py +105 -0
  6. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/commands/bump.py +2 -2
  7. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/commands/changelog.py +1 -1
  8. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/commands/commit.py +3 -3
  9. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/commands/init.py +12 -7
  10. commitizen-4.15.1/commitizen/commands/version.py +141 -0
  11. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/git.py +53 -36
  12. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/hooks.py +1 -1
  13. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/out.py +11 -11
  14. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/tags.py +6 -8
  15. commitizen-4.15.1/commitizen/version_increment.py +33 -0
  16. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/version_schemes.py +13 -4
  17. {commitizen-4.14.0 → commitizen-4.15.1}/pyproject.toml +1 -1
  18. commitizen-4.14.0/commitizen/__version__.py +0 -1
  19. commitizen-4.14.0/commitizen/cmd.py +0 -57
  20. commitizen-4.14.0/commitizen/commands/version.py +0 -86
  21. {commitizen-4.14.0 → commitizen-4.15.1}/LICENSE +0 -0
  22. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/__init__.py +0 -0
  23. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/__main__.py +0 -0
  24. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/changelog.py +0 -0
  25. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/changelog_formats/__init__.py +0 -0
  26. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/changelog_formats/asciidoc.py +0 -0
  27. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/changelog_formats/base.py +0 -0
  28. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/changelog_formats/markdown.py +0 -0
  29. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/changelog_formats/restructuredtext.py +0 -0
  30. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/changelog_formats/textile.py +0 -0
  31. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/commands/__init__.py +0 -0
  32. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/commands/check.py +0 -0
  33. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/commands/example.py +0 -0
  34. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/commands/info.py +0 -0
  35. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/commands/list_cz.py +0 -0
  36. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/commands/schema.py +0 -0
  37. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/config/__init__.py +0 -0
  38. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/config/base_config.py +0 -0
  39. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/config/factory.py +0 -0
  40. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/config/json_config.py +0 -0
  41. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/config/toml_config.py +0 -0
  42. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/config/yaml_config.py +0 -0
  43. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/cz/__init__.py +0 -0
  44. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/cz/base.py +0 -0
  45. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/cz/conventional_commits/__init__.py +0 -0
  46. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/cz/conventional_commits/conventional_commits.py +0 -0
  47. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/cz/conventional_commits/conventional_commits_info.txt +0 -0
  48. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/cz/customize/__init__.py +0 -0
  49. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/cz/customize/customize.py +0 -0
  50. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/cz/customize/customize_info.txt +0 -0
  51. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/cz/exceptions.py +0 -0
  52. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/cz/jira/__init__.py +0 -0
  53. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/cz/jira/jira.py +0 -0
  54. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/cz/jira/jira_info.txt +0 -0
  55. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/cz/utils.py +0 -0
  56. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/defaults.py +0 -0
  57. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/exceptions.py +0 -0
  58. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/factory.py +0 -0
  59. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/project_info.py +0 -0
  60. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/providers/__init__.py +0 -0
  61. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/providers/base_provider.py +0 -0
  62. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/providers/cargo_provider.py +0 -0
  63. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/providers/commitizen_provider.py +0 -0
  64. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/providers/composer_provider.py +0 -0
  65. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/providers/npm_provider.py +0 -0
  66. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/providers/pep621_provider.py +0 -0
  67. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/providers/poetry_provider.py +0 -0
  68. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/providers/scm_provider.py +0 -0
  69. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/providers/uv_provider.py +0 -0
  70. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/py.typed +0 -0
  71. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/question.py +0 -0
  72. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/templates/CHANGELOG.adoc.j2 +0 -0
  73. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/templates/CHANGELOG.md.j2 +0 -0
  74. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/templates/CHANGELOG.rst.j2 +0 -0
  75. {commitizen-4.14.0 → commitizen-4.15.1}/commitizen/templates/CHANGELOG.textile.j2 +0 -0
  76. {commitizen-4.14.0 → commitizen-4.15.1}/docs/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: commitizen
3
- Version: 4.14.0
3
+ Version: 4.15.1
4
4
  Summary: Python commitizen client tool
5
5
  Keywords: commitizen,conventional,commits,git
6
6
  Author: Santiago Fraire
@@ -0,0 +1 @@
1
+ __version__ = "4.15.1"
@@ -15,7 +15,7 @@ from commitizen.git import GitCommit, smart_open
15
15
  if TYPE_CHECKING:
16
16
  from collections.abc import Generator, Iterable
17
17
 
18
- from commitizen.version_schemes import Increment, Version
18
+ from commitizen.version_schemes import Increment, VersionProtocol
19
19
 
20
20
  VERSION_TYPES = [None, PATCH, MINOR, MAJOR]
21
21
 
@@ -131,8 +131,8 @@ def _resolve_files_and_regexes(
131
131
 
132
132
 
133
133
  def create_commit_message(
134
- current_version: Version | str,
135
- new_version: Version | str,
134
+ current_version: VersionProtocol | str,
135
+ new_version: VersionProtocol | str,
136
136
  message_template: str | None = None,
137
137
  ) -> str:
138
138
  if message_template is None:
@@ -21,6 +21,7 @@ from commitizen.exceptions import (
21
21
  InvalidCommandArgumentError,
22
22
  NoCommandFoundError,
23
23
  )
24
+ from commitizen.version_increment import VersionIncrement
24
25
 
25
26
  logger = logging.getLogger(__name__)
26
27
 
@@ -542,13 +543,19 @@ data = {
542
543
  },
543
544
  {
544
545
  "name": ["--major"],
545
- "help": "Output just the major version. Must be used with --project or --verbose.",
546
+ "help": (
547
+ "Output just the major version. Must be used with MANUAL_VERSION, "
548
+ "--project, or --verbose."
549
+ ),
546
550
  "action": "store_true",
547
551
  "exclusive_group": "group2",
548
552
  },
549
553
  {
550
554
  "name": ["--minor"],
551
- "help": "Output just the minor version. Must be used with --project or --verbose.",
555
+ "help": (
556
+ "Output just the minor version. Must be used with MANUAL_VERSION, "
557
+ "--project, or --verbose."
558
+ ),
552
559
  "action": "store_true",
553
560
  "exclusive_group": "group2",
554
561
  },
@@ -558,6 +565,33 @@ data = {
558
565
  "action": "store_true",
559
566
  "exclusive_group": "group2",
560
567
  },
568
+ {
569
+ "name": ["--patch"],
570
+ "help": (
571
+ "Output the patch version only. Must be used with MANUAL_VERSION, "
572
+ "--project, or --verbose."
573
+ ),
574
+ "action": "store_true",
575
+ "exclusive_group": "group2",
576
+ },
577
+ {
578
+ "name": ["--next"],
579
+ "help": "Output the next version.",
580
+ "type": str,
581
+ "nargs": "?",
582
+ "default": None,
583
+ "const": "USE_GIT_COMMITS",
584
+ "choices": ["USE_GIT_COMMITS"]
585
+ + [str(increment) for increment in VersionIncrement],
586
+ "exclusive_group": "group2",
587
+ },
588
+ {
589
+ "name": "manual_version",
590
+ "type": str,
591
+ "nargs": "?",
592
+ "help": "Use the version provided instead of the version from the project. Can be used to test the selected version scheme.",
593
+ "metavar": "MANUAL_VERSION",
594
+ },
561
595
  ],
562
596
  },
563
597
  ],
@@ -670,7 +704,7 @@ def main() -> None:
670
704
  logger.warning(
671
705
  "\nWARN: Incomplete commit command: received -- separator without any following git arguments\n"
672
706
  )
673
- extra_args = " ".join(unknown_args[1:])
707
+ extra_args = unknown_args[1:]
674
708
  arguments["extra_cli_args"] = extra_args
675
709
 
676
710
  conf = config.read_cfg(args.config)
@@ -0,0 +1,105 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import subprocess
5
+ import warnings
6
+ from typing import TYPE_CHECKING, NamedTuple, overload
7
+
8
+ from charset_normalizer import from_bytes
9
+
10
+ from commitizen.exceptions import CharacterSetDecodeError
11
+
12
+ if TYPE_CHECKING:
13
+ from collections.abc import Mapping, Sequence
14
+
15
+
16
+ class Command(NamedTuple):
17
+ out: str
18
+ err: str
19
+ stdout: bytes
20
+ stderr: bytes
21
+ return_code: int
22
+
23
+
24
+ def _try_decode(bytes_: bytes) -> str:
25
+ try:
26
+ return bytes_.decode("utf-8")
27
+ except UnicodeDecodeError:
28
+ pass
29
+
30
+ charset_match = from_bytes(bytes_).best()
31
+ if charset_match is None:
32
+ raise CharacterSetDecodeError()
33
+ try:
34
+ return bytes_.decode(charset_match.encoding)
35
+ except UnicodeDecodeError as e:
36
+ raise CharacterSetDecodeError() from e
37
+
38
+
39
+ def _popen(
40
+ cmd: str | Sequence[str],
41
+ *,
42
+ shell: bool,
43
+ env: Mapping[str, str] | None = None,
44
+ ) -> Command:
45
+ if env is not None:
46
+ env = {**os.environ, **env}
47
+
48
+ process = subprocess.Popen(
49
+ cmd,
50
+ shell=shell,
51
+ stdout=subprocess.PIPE,
52
+ stderr=subprocess.PIPE,
53
+ stdin=subprocess.PIPE,
54
+ env=env,
55
+ )
56
+ stdout, stderr = process.communicate()
57
+ return_code = process.returncode
58
+ return Command(
59
+ _try_decode(stdout),
60
+ _try_decode(stderr),
61
+ stdout,
62
+ stderr,
63
+ return_code,
64
+ )
65
+
66
+
67
+ @overload
68
+ def run(cmd: str, env: Mapping[str, str] | None = None) -> Command: ...
69
+
70
+
71
+ @overload
72
+ def run(cmd: Sequence[str], env: Mapping[str, str] | None = None) -> Command: ...
73
+
74
+
75
+ def run(cmd: str | Sequence[str], env: Mapping[str, str] | None = None) -> Command:
76
+ """Run a command safely without shell interpretation (shell=False).
77
+
78
+ Arguments are passed directly to the OS, preventing shell-injection
79
+ vulnerabilities (CWE-78).
80
+
81
+ Passing a string is deprecated and will be removed in a future version.
82
+ Use a list of arguments instead, or use run_shell() for shell features.
83
+ """
84
+ if isinstance(cmd, str):
85
+ warnings.warn(
86
+ "Passing a string to cmd.run() is deprecated and will be removed in v5. "
87
+ "Use a list of arguments instead, or use cmd.run_shell() explicitly.",
88
+ DeprecationWarning,
89
+ stacklevel=2,
90
+ )
91
+ return _popen(cmd, shell=True, env=env)
92
+ return _popen(cmd, shell=False, env=env)
93
+
94
+
95
+ def run_shell(cmd: str, env: Mapping[str, str] | None = None) -> Command:
96
+ """Run a command string via the system shell (shell=True).
97
+
98
+ Only use this for cases that intentionally require shell features
99
+ (e.g., user-defined hooks with pipes/redirects). Never pass
100
+ untrusted/user-controlled values into *cmd*.
101
+
102
+ Related: CWE-78 (OS Command Injection),
103
+ https://github.com/commitizen-tools/commitizen/issues/1918
104
+ """
105
+ return _popen(cmd, shell=True, env=env)
@@ -439,8 +439,8 @@ class Bump:
439
439
  else:
440
440
  out.success("Done!")
441
441
 
442
- def _get_commit_args(self) -> str:
442
+ def _get_commit_args(self) -> list[str]:
443
443
  commit_args = ["-a"]
444
444
  if self.no_verify:
445
445
  commit_args.append("--no-verify")
446
- return " ".join(commit_args)
446
+ return commit_args
@@ -257,7 +257,7 @@ class Changelog:
257
257
  changelog_meta.latest_version_position = None
258
258
  changelog_meta.unreleased_end = latest_full_release_info.index + 1
259
259
 
260
- commits = git.get_commits(start=start_rev, end=end_rev, args="--topo-order")
260
+ commits = git.get_commits(start=start_rev, end=end_rev, args=["--topo-order"])
261
261
  if (
262
262
  not self.allow_no_commit
263
263
  and not commits
@@ -34,7 +34,7 @@ class CommitArgs(TypedDict, total=False):
34
34
  all: bool
35
35
  dry_run: bool
36
36
  edit: bool
37
- extra_cli_args: str
37
+ extra_cli_args: list[str]
38
38
  message_length_limit: int
39
39
  no_retry: bool
40
40
  signoff: bool
@@ -132,7 +132,7 @@ class Commit:
132
132
  return self._get_message_by_prompt_commit_questions()
133
133
 
134
134
  def __call__(self) -> None:
135
- extra_args = self.arguments.get("extra_cli_args", "")
135
+ extra_args: list[str] = self.arguments.get("extra_cli_args", [])
136
136
  dry_run = bool(self.arguments.get("dry_run"))
137
137
  write_message_to_file = self.arguments.get("write_message_to_file")
138
138
  signoff = bool(self.arguments.get("signoff"))
@@ -167,7 +167,7 @@ class Commit:
167
167
  raise DryRunExit()
168
168
 
169
169
  if self.config.settings["always_signoff"] or signoff:
170
- extra_args = f"{extra_args} -s".strip()
170
+ extra_args = [*extra_args, "-s"]
171
171
 
172
172
  c = git.commit(commit_message, args=extra_args)
173
173
  if c.return_code != 0:
@@ -17,7 +17,11 @@ from commitizen.exceptions import (
17
17
  NoAnswersError,
18
18
  )
19
19
  from commitizen.git import get_latest_tag_name, get_tag_names, smart_open
20
- from commitizen.version_schemes import KNOWN_SCHEMES, Version, get_version_scheme
20
+ from commitizen.version_schemes import (
21
+ KNOWN_SCHEMES,
22
+ VersionProtocol,
23
+ get_version_scheme,
24
+ )
21
25
 
22
26
  if TYPE_CHECKING:
23
27
  from commitizen.config import (
@@ -130,11 +134,12 @@ class Init:
130
134
  "pre-commit is not installed in current environment."
131
135
  )
132
136
 
133
- cmd_str = "pre-commit install " + " ".join(
134
- f"--hook-type {ty}" for ty in hook_types
135
- )
136
- c = cmd.run(cmd_str)
137
+ cmd_args = ["pre-commit", "install"]
138
+ for ty in hook_types:
139
+ cmd_args.extend(["--hook-type", ty])
140
+ c = cmd.run(cmd_args)
137
141
  if c.return_code != 0:
142
+ cmd_str = " ".join(cmd_args)
138
143
  raise InitFailedError(
139
144
  "Failed to install pre-commit hook.\n"
140
145
  f"Error running {cmd_str}."
@@ -265,7 +270,7 @@ class Init:
265
270
  ).unsafe_ask()
266
271
  return scheme
267
272
 
268
- def _ask_major_version_zero(self, version: Version) -> bool:
273
+ def _ask_major_version_zero(self, version: VersionProtocol) -> bool:
269
274
  """Ask for setting: major_version_zero"""
270
275
  if version.major > 0:
271
276
  return False
@@ -323,7 +328,7 @@ def _write_config_to_file(
323
328
  cz_name: str,
324
329
  version_provider: str,
325
330
  version_scheme: str,
326
- version: Version,
331
+ version: VersionProtocol,
327
332
  tag_format: str,
328
333
  update_changelog_on_bump: bool,
329
334
  major_version_zero: bool,
@@ -0,0 +1,141 @@
1
+ import platform
2
+ import sys
3
+ from typing import TypedDict
4
+
5
+ from packaging.version import InvalidVersion
6
+
7
+ from commitizen import out
8
+ from commitizen.__version__ import __version__
9
+ from commitizen.config import BaseConfig
10
+ from commitizen.exceptions import NoVersionSpecifiedError, VersionSchemeUnknown
11
+ from commitizen.providers import get_provider
12
+ from commitizen.tags import TagRules
13
+ from commitizen.version_increment import VersionIncrement
14
+ from commitizen.version_schemes import Increment, get_version_scheme
15
+
16
+
17
+ class VersionArgs(TypedDict, total=False):
18
+ manual_version: str | None
19
+ next: str | None
20
+
21
+ # Exclusive groups 1
22
+ commitizen: bool
23
+ report: bool
24
+ project: bool
25
+ verbose: bool
26
+
27
+ # Exclusive groups 2
28
+ major: bool
29
+ minor: bool
30
+ patch: bool
31
+ tag: bool
32
+
33
+
34
+ class Version:
35
+ """Get the version of the installed commitizen or the current project.
36
+ Precedence:
37
+ 1. report
38
+ 2. commitizen
39
+ 3. verbose, project
40
+ """
41
+
42
+ def __init__(self, config: BaseConfig, arguments: VersionArgs) -> None:
43
+ self.config: BaseConfig = config
44
+ self.arguments = arguments
45
+
46
+ def __call__(self) -> None:
47
+ if self.arguments.get("report"):
48
+ out.write(f"Commitizen Version: {__version__}")
49
+ out.write(f"Python Version: {sys.version}")
50
+ out.write(f"Operating System: {platform.system()}")
51
+ return
52
+
53
+ if self.arguments.get("verbose"):
54
+ out.write(f"Installed Commitizen Version: {__version__}")
55
+
56
+ if self.arguments.get("commitizen"):
57
+ out.write(__version__)
58
+ return
59
+
60
+ if (
61
+ self.arguments.get("project")
62
+ or self.arguments.get("verbose")
63
+ or self.arguments.get("next")
64
+ or self.arguments.get("manual_version")
65
+ ):
66
+ version_str = self.arguments.get("manual_version")
67
+ if version_str is None:
68
+ try:
69
+ version_str = get_provider(self.config).get_version()
70
+ except NoVersionSpecifiedError:
71
+ out.error("No project information in this project.")
72
+ return
73
+ try:
74
+ scheme_factory = get_version_scheme(self.config.settings)
75
+ except VersionSchemeUnknown:
76
+ out.error("Unknown version scheme.")
77
+ return
78
+
79
+ try:
80
+ version = scheme_factory(version_str)
81
+ except InvalidVersion:
82
+ out.error(f"Invalid version: '{version_str}'")
83
+ return
84
+
85
+ if next_increment_str := self.arguments.get("next"):
86
+ if next_increment_str == "USE_GIT_COMMITS":
87
+ # TODO: implement USE_GIT_COMMITS by deriving the increment from
88
+ # git history. This requires refactoring the bump logic out of
89
+ # `commitizen/commands/bump.py` so it can be reused here. See #1678.
90
+ out.error("--next USE_GIT_COMMITS is not implemented yet.")
91
+ return
92
+
93
+ next_increment = VersionIncrement.from_value(next_increment_str)
94
+ increment: Increment | None
95
+ if next_increment == VersionIncrement.NONE:
96
+ increment = None
97
+ elif next_increment == VersionIncrement.PATCH:
98
+ increment = "PATCH"
99
+ elif next_increment == VersionIncrement.MINOR:
100
+ increment = "MINOR"
101
+ else:
102
+ increment = "MAJOR"
103
+ version = version.bump(increment=increment)
104
+
105
+ if self.arguments.get("major"):
106
+ out.write(version.major)
107
+ return
108
+ if self.arguments.get("minor"):
109
+ out.write(version.minor)
110
+ return
111
+ if self.arguments.get("patch"):
112
+ out.write(version.micro)
113
+ return
114
+
115
+ display_version: str
116
+ if self.arguments.get("tag"):
117
+ tag_rules = TagRules.from_settings(self.config.settings)
118
+ display_version = tag_rules.normalize_tag(version)
119
+ else:
120
+ display_version = str(version)
121
+
122
+ out.write(
123
+ f"Project Version: {display_version}"
124
+ if self.arguments.get("verbose")
125
+ else display_version
126
+ )
127
+ return
128
+
129
+ for argument in ("major", "minor", "patch"):
130
+ if self.arguments.get(argument):
131
+ out.error(
132
+ f"{argument} can only be used with MANUAL_VERSION, --project or --verbose."
133
+ )
134
+ return
135
+
136
+ if self.arguments.get("tag"):
137
+ out.error("Tag can only be used with --project or --verbose.")
138
+ return
139
+
140
+ # If no arguments are provided, just show the installed commitizen version
141
+ out.write(__version__)
@@ -5,10 +5,14 @@ from enum import Enum
5
5
  from functools import lru_cache
6
6
  from pathlib import Path
7
7
  from tempfile import NamedTemporaryFile
8
+ from typing import TYPE_CHECKING
8
9
 
9
10
  from commitizen import cmd, out
10
11
  from commitizen.exceptions import GitCommandError
11
12
 
13
+ if TYPE_CHECKING:
14
+ from collections.abc import Sequence
15
+
12
16
 
13
17
  class EOLType(Enum):
14
18
  """The EOL type from `git config core.eol`."""
@@ -19,7 +23,7 @@ class EOLType(Enum):
19
23
 
20
24
  @classmethod
21
25
  def for_open(cls) -> str:
22
- c = cmd.run("git config core.eol")
26
+ c = cmd.run(["git", "config", "core.eol"])
23
27
  eol = c.out.strip().upper()
24
28
  return cls._char_for_open()[cls._safe_cast(eol)]
25
29
 
@@ -164,49 +168,47 @@ def tag(
164
168
  tag: str, annotated: bool = False, signed: bool = False, msg: str | None = None
165
169
  ) -> cmd.Command:
166
170
  if not annotated and not signed:
167
- return cmd.run(f"git tag {tag}")
171
+ return cmd.run(["git", "tag", tag])
168
172
 
169
173
  # according to https://git-scm.com/book/en/v2/Git-Basics-Tagging,
170
174
  # we're not able to create lightweight tag with message.
171
175
  # by adding message, we make it a annotated tags
172
176
  option = "-s" if signed else "-a" # The else case is for annotated tags
173
- return cmd.run(f'git tag {option} {tag} -m "{msg or tag}"')
177
+ return cmd.run(["git", "tag", option, tag, "-m", msg or tag])
174
178
 
175
179
 
176
180
  def add(*args: str) -> cmd.Command:
177
- return cmd.run(f"git add {' '.join(args)}")
181
+ return cmd.run(["git", "add", *args])
178
182
 
179
183
 
180
184
  def commit(
181
185
  message: str,
182
- args: str = "",
186
+ args: Sequence[str] = (),
183
187
  committer_date: str | None = None,
184
188
  ) -> cmd.Command:
185
189
  f = NamedTemporaryFile("wb", delete=False)
186
190
  f.write(message.encode("utf-8"))
187
191
  f.close()
188
192
 
189
- command = _create_commit_cmd_string(args, committer_date, f.name)
190
- c = cmd.run(command)
191
- os.unlink(f.name)
192
- return c
193
+ cmd_args = ["git", "commit"]
194
+ if args:
195
+ cmd_args.extend(args)
196
+ cmd_args.extend(["-F", f.name])
193
197
 
198
+ env: dict[str, str] | None = None
199
+ if committer_date:
200
+ env = {"GIT_COMMITTER_DATE": committer_date}
194
201
 
195
- def _create_commit_cmd_string(args: str, committer_date: str | None, name: str) -> str:
196
- command = f'git commit {args} -F "{name}"'
197
- if not committer_date:
198
- return command
199
- if os.name != "nt":
200
- return f"GIT_COMMITTER_DATE={committer_date} {command}"
201
- # Using `cmd /v /c "{command}"` sets environment variables only for that command
202
- return f'cmd /v /c "set GIT_COMMITTER_DATE={committer_date}&& {command}"'
202
+ c = cmd.run(cmd_args, env=env)
203
+ os.unlink(f.name)
204
+ return c
203
205
 
204
206
 
205
207
  def get_commits(
206
208
  start: str | None = None,
207
209
  end: str | None = None,
208
210
  *,
209
- args: str = "",
211
+ args: Sequence[str] = (),
210
212
  ) -> list[GitCommit]:
211
213
  """Get the commits between start and end."""
212
214
  if end is None:
@@ -226,7 +228,10 @@ def get_filenames_in_commit(git_reference: str = "") -> list[str]:
226
228
 
227
229
  :returns: file names committed in the last commit by default or inside the passed git reference
228
230
  """
229
- c = cmd.run(f"git show --name-only --pretty=format: {git_reference}")
231
+ cmd_args = ["git", "show", "--name-only", "--pretty=format:"]
232
+ if git_reference:
233
+ cmd_args.append(git_reference)
234
+ c = cmd.run(cmd_args)
230
235
  if c.return_code == 0:
231
236
  return c.out.strip().split("\n")
232
237
  raise GitCommandError(c.err)
@@ -237,15 +242,17 @@ def get_tags(
237
242
  ) -> list[GitTag]:
238
243
  inner_delimiter = "---inner_delimiter---"
239
244
  formatter = (
240
- f'"%(refname:strip=2){inner_delimiter}'
245
+ f"%(refname:strip=2){inner_delimiter}"
241
246
  f"%(objectname){inner_delimiter}"
242
247
  f"%(creatordate:format:{dateformat}){inner_delimiter}"
243
- f'%(object)"'
248
+ f"%(object)"
244
249
  )
245
- extra = "--merged" if reachable_only else ""
250
+ cmd_args = ["git", "tag", f"--format={formatter}", "--sort=-creatordate"]
251
+ if reachable_only:
252
+ cmd_args.append("--merged")
246
253
  # Force the default language for parsing
247
254
  env = {"LC_ALL": "C", "LANG": "C", "LANGUAGE": "C"}
248
- c = cmd.run(f"git tag --format={formatter} --sort=-creatordate {extra}", env=env)
255
+ c = cmd.run(cmd_args, env=env)
249
256
  if c.return_code != 0:
250
257
  if reachable_only and c.err == "fatal: malformed object name HEAD\n":
251
258
  # this can happen if there are no commits in the repo yet
@@ -262,37 +269,37 @@ def get_tags(
262
269
 
263
270
 
264
271
  def tag_exist(tag: str) -> bool:
265
- c = cmd.run(f"git tag --list {tag}")
272
+ c = cmd.run(["git", "tag", "--list", tag])
266
273
  return tag in c.out
267
274
 
268
275
 
269
276
  def is_signed_tag(tag: str) -> bool:
270
- return cmd.run(f"git tag -v {tag}").return_code == 0
277
+ return cmd.run(["git", "tag", "-v", tag]).return_code == 0
271
278
 
272
279
 
273
280
  def get_latest_tag_name() -> str | None:
274
- c = cmd.run("git describe --abbrev=0 --tags")
281
+ c = cmd.run(["git", "describe", "--abbrev=0", "--tags"])
275
282
  if c.err:
276
283
  return None
277
284
  return c.out.strip()
278
285
 
279
286
 
280
287
  def get_tag_message(tag: str) -> str | None:
281
- c = cmd.run(f"git tag -l --format='%(contents:subject)' {tag}")
288
+ c = cmd.run(["git", "tag", "-l", "--format=%(contents:subject)", tag])
282
289
  if c.err:
283
290
  return None
284
291
  return c.out.strip()
285
292
 
286
293
 
287
294
  def get_tag_names() -> list[str]:
288
- c = cmd.run("git tag --list")
295
+ c = cmd.run(["git", "tag", "--list"])
289
296
  if c.err:
290
297
  return []
291
298
  return [tag for raw in c.out.split("\n") if (tag := raw.strip())]
292
299
 
293
300
 
294
301
  def find_git_project_root() -> Path | None:
295
- c = cmd.run("git rev-parse --show-toplevel")
302
+ c = cmd.run(["git", "rev-parse", "--show-toplevel"])
296
303
  if c.err:
297
304
  return None
298
305
  return Path(c.out.strip())
@@ -300,17 +307,17 @@ def find_git_project_root() -> Path | None:
300
307
 
301
308
  def is_staging_clean() -> bool:
302
309
  """Check if staging is clean."""
303
- c = cmd.run("git diff --no-ext-diff --cached --name-only")
310
+ c = cmd.run(["git", "diff", "--no-ext-diff", "--cached", "--name-only"])
304
311
  return not bool(c.out)
305
312
 
306
313
 
307
314
  def is_git_project() -> bool:
308
- c = cmd.run("git rev-parse --is-inside-work-tree")
315
+ c = cmd.run(["git", "rev-parse", "--is-inside-work-tree"])
309
316
  return c.out.strip() == "true"
310
317
 
311
318
 
312
319
  def get_core_editor() -> str | None:
313
- c = cmd.run("git var GIT_EDITOR")
320
+ c = cmd.run(["git", "var", "GIT_EDITOR"])
314
321
  if c.out:
315
322
  return c.out.strip()
316
323
  return None
@@ -321,21 +328,31 @@ def smart_open(*args, **kwargs): # type: ignore[no-untyped-def,unused-ignore] #
321
328
  return open(*args, newline=EOLType.for_open(), **kwargs)
322
329
 
323
330
 
324
- def _get_log_as_str_list(start: str | None, end: str, args: str) -> list[str]:
331
+ def _get_log_as_str_list(start: str | None, end: str, args: Sequence[str]) -> list[str]:
325
332
  """Get string representation of each log entry"""
326
333
  delimiter = "----------commit-delimiter----------"
327
334
  log_format: str = "%H%n%P%n%s%n%an%n%ae%n%b"
328
335
  command_range = f"{start}..{end}" if start else end
329
- command = f"git -c log.showSignature=False log --pretty={log_format}{delimiter} {args} {command_range}"
336
+ cmd_args = [
337
+ "git",
338
+ "-c",
339
+ "log.showSignature=False",
340
+ "log",
341
+ f"--pretty={log_format}{delimiter}",
342
+ ]
343
+ if args:
344
+ cmd_args.extend(args)
345
+ if command_range:
346
+ cmd_args.append(command_range)
330
347
 
331
- c = cmd.run(command)
348
+ c = cmd.run(cmd_args)
332
349
  if c.return_code != 0:
333
350
  raise GitCommandError(c.err)
334
351
  return c.out.split(f"{delimiter}\n")
335
352
 
336
353
 
337
354
  def get_default_branch() -> str:
338
- c = cmd.run("git symbolic-ref refs/remotes/origin/HEAD")
355
+ c = cmd.run(["git", "symbolic-ref", "refs/remotes/origin/HEAD"])
339
356
  if c.return_code != 0:
340
357
  raise GitCommandError(c.err)
341
358
  return c.out.strip()
@@ -17,7 +17,7 @@ def run(hooks: str | list[str], _env_prefix: str = "CZ_", **env: object) -> None
17
17
  for hook in hooks:
18
18
  out.info(f"Running hook '{hook}'")
19
19
 
20
- c = cmd.run(hook, env=_format_env(_env_prefix, env))
20
+ c = cmd.run_shell(hook, env=_format_env(_env_prefix, env))
21
21
 
22
22
  if c.out:
23
23
  out.write(c.out)
@@ -9,35 +9,35 @@ if sys.platform == "win32":
9
9
  sys.stdout.reconfigure(encoding="utf-8")
10
10
 
11
11
 
12
- def write(value: str, *args: object) -> None:
12
+ def write(value: object, *args: object) -> None:
13
13
  """Intended to be used when value is multiline."""
14
14
  print(value, *args)
15
15
 
16
16
 
17
- def line(value: str, *args: object, **kwargs: Any) -> None:
17
+ def line(value: object, *args: object, **kwargs: Any) -> None:
18
18
  """Wrapper in case I want to do something different later."""
19
19
  print(value, *args, **kwargs)
20
20
 
21
21
 
22
- def error(value: str) -> None:
23
- message = colored(value, "red")
22
+ def error(value: object) -> None:
23
+ message = colored(str(value), "red")
24
24
  line(message, file=sys.stderr)
25
25
 
26
26
 
27
- def success(value: str) -> None:
28
- message = colored(value, "green")
27
+ def success(value: object) -> None:
28
+ message = colored(str(value), "green")
29
29
  line(message)
30
30
 
31
31
 
32
- def info(value: str) -> None:
33
- message = colored(value, "blue")
32
+ def info(value: object) -> None:
33
+ message = colored(str(value), "blue")
34
34
  line(message)
35
35
 
36
36
 
37
- def diagnostic(value: str) -> None:
37
+ def diagnostic(value: object) -> None:
38
38
  line(value, file=sys.stderr)
39
39
 
40
40
 
41
- def warn(value: str) -> None:
42
- message = colored(value, "magenta")
41
+ def warn(value: object) -> None:
42
+ message = colored(str(value), "magenta")
43
43
  line(message, file=sys.stderr)
@@ -14,7 +14,7 @@ from commitizen.git import GitTag
14
14
  from commitizen.version_schemes import (
15
15
  DEFAULT_SCHEME,
16
16
  InvalidVersion,
17
- Version,
17
+ VersionProtocol,
18
18
  VersionScheme,
19
19
  get_version_scheme,
20
20
  )
@@ -23,8 +23,6 @@ if TYPE_CHECKING:
23
23
  import sys
24
24
  from collections.abc import Iterable, Sequence
25
25
 
26
- from commitizen.version_schemes import VersionScheme
27
-
28
26
  # Self is Python 3.11+ but backported in typing-extensions
29
27
  if sys.version_info < (3, 11):
30
28
  from typing_extensions import Self
@@ -75,7 +73,7 @@ class TagRules:
75
73
  assert not rules.is_version_tag("warn1.0.0", warn=True) # Does warn
76
74
 
77
75
  assert rules.search_version("# My v1.0.0 version").version == "1.0.0"
78
- assert rules.extract_version("v1.0.0") == Version("1.0.0")
76
+ assert rules.extract_version("v1.0.0") == rules.scheme("1.0.0")
79
77
  try:
80
78
  assert rules.extract_version("not-a-v1.0.0")
81
79
  except InvalidVersion:
@@ -145,7 +143,7 @@ class TagRules:
145
143
  """Filter in version tags and warn on unexpected tags"""
146
144
  return [tag for tag in tags if self.is_version_tag(tag, warn)]
147
145
 
148
- def extract_version(self, tag: GitTag) -> Version:
146
+ def extract_version(self, tag: GitTag) -> VersionProtocol:
149
147
  """
150
148
  Extract a version from the tag as defined in tag formats.
151
149
 
@@ -195,7 +193,7 @@ class TagRules:
195
193
  return VersionTag(version, match.group(0))
196
194
 
197
195
  def normalize_tag(
198
- self, version: Version | str, tag_format: str | None = None
196
+ self, version: VersionProtocol | str, tag_format: str | None = None
199
197
  ) -> str:
200
198
  """
201
199
  The tag and the software version might be different.
@@ -225,7 +223,7 @@ class TagRules:
225
223
  )
226
224
 
227
225
  def find_tag_for(
228
- self, tags: Iterable[GitTag], version: Version | str
226
+ self, tags: Iterable[GitTag], version: VersionProtocol | str
229
227
  ) -> GitTag | None:
230
228
  """Find the first matching tag for a given version."""
231
229
  version = self.scheme(version) if isinstance(version, str) else version
@@ -234,7 +232,7 @@ class TagRules:
234
232
  # If the requested version is incomplete (e.g., "1.2"), try to find the latest
235
233
  # matching tag that shares the provided prefix.
236
234
  if len(release) < 3:
237
- matching_versions: list[tuple[Version, GitTag]] = []
235
+ matching_versions: list[tuple[VersionProtocol, GitTag]] = []
238
236
  for tag in tags:
239
237
  try:
240
238
  tag_version = self.extract_version(tag)
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import IntEnum
4
+
5
+
6
+ class VersionIncrement(IntEnum):
7
+ """Semantic versioning bump increments.
8
+
9
+ IntEnum keeps a total order compatible with NONE < PATCH < MINOR < MAJOR
10
+ for comparisons across the codebase.
11
+
12
+ - NONE: no bump (docs-only / style commits, etc.)
13
+ - PATCH: backwards-compatible bug fixes
14
+ - MINOR: backwards-compatible features
15
+ - MAJOR: incompatible API changes
16
+ """
17
+
18
+ NONE = 0
19
+ PATCH = 1
20
+ MINOR = 2
21
+ MAJOR = 3
22
+
23
+ def __str__(self) -> str:
24
+ return self.name
25
+
26
+ @classmethod
27
+ def from_value(cls, value: object) -> VersionIncrement:
28
+ if not isinstance(value, str):
29
+ return VersionIncrement.NONE
30
+ try:
31
+ return cls[value]
32
+ except KeyError:
33
+ return VersionIncrement.NONE
@@ -10,6 +10,7 @@ from typing import (
10
10
  ClassVar,
11
11
  Literal,
12
12
  Protocol,
13
+ TypeAlias,
13
14
  cast,
14
15
  runtime_checkable,
15
16
  )
@@ -22,7 +23,6 @@ from commitizen.exceptions import VersionSchemeUnknown
22
23
 
23
24
  if TYPE_CHECKING:
24
25
  import sys
25
- from typing import TypeAlias
26
26
 
27
27
  # Self is Python 3.11+ but backported in typing-extensions
28
28
  if sys.version_info < (3, 11):
@@ -31,7 +31,7 @@ if TYPE_CHECKING:
31
31
  from typing import Self
32
32
 
33
33
 
34
- Increment: TypeAlias = Literal["MAJOR", "MINOR", "PATCH"]
34
+ Increment: TypeAlias = Literal["MAJOR", "MINOR", "PATCH"] # TODO: deprecate
35
35
  Prerelease: TypeAlias = Literal["alpha", "beta", "rc"]
36
36
  _DEFAULT_VERSION_PARSER = re.compile(
37
37
  r"v?(?P<version>([0-9]+)\.([0-9]+)(?:\.([0-9]+))?(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z.]+)?(\w+)?)"
@@ -140,7 +140,7 @@ class VersionProtocol(Protocol):
140
140
  """
141
141
 
142
142
 
143
- # With PEP 440 and SemVer semantic, Scheme is the type, Version is an instance
143
+ # With PEP 440 and SemVer semantics, a scheme is the class; a version is an instance.
144
144
  Version: TypeAlias = VersionProtocol
145
145
  VersionScheme: TypeAlias = type[VersionProtocol]
146
146
 
@@ -422,7 +422,16 @@ def get_version_scheme(settings: Settings, name: str | None = None) -> VersionSc
422
422
  raise VersionSchemeUnknown(f'Version scheme "{name}" unknown.')
423
423
  scheme = cast("VersionScheme", ep.load())
424
424
 
425
- if not isinstance(scheme, VersionProtocol):
425
+ # `VersionProtocol` is a `@runtime_checkable` Protocol, but `issubclass()` is not
426
+ # supported for Protocols with non-method members. We check an instance instead by
427
+ # verifying the loaded object is a class with the expected interface.
428
+ if isinstance(scheme, type):
429
+ # Check for a key method/attribute that VersionProtocol requires
430
+ if not hasattr(scheme, "bump"):
431
+ warnings.warn(
432
+ f"Version scheme {name} does not implement the VersionProtocol"
433
+ )
434
+ else:
426
435
  warnings.warn(f"Version scheme {name} does not implement the VersionProtocol")
427
436
 
428
437
  return scheme
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "commitizen"
3
- version = "4.14.0"
3
+ version = "4.15.1"
4
4
  description = "Python commitizen client tool"
5
5
  authors = [{ name = "Santiago Fraire", email = "santiwilly@gmail.com" }]
6
6
  maintainers = [
@@ -1 +0,0 @@
1
- __version__ = "4.14.0"
@@ -1,57 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import os
4
- import subprocess
5
- from typing import TYPE_CHECKING, NamedTuple
6
-
7
- from charset_normalizer import from_bytes
8
-
9
- from commitizen.exceptions import CharacterSetDecodeError
10
-
11
- if TYPE_CHECKING:
12
- from collections.abc import Mapping
13
-
14
-
15
- class Command(NamedTuple):
16
- out: str
17
- err: str
18
- stdout: bytes
19
- stderr: bytes
20
- return_code: int
21
-
22
-
23
- def _try_decode(bytes_: bytes) -> str:
24
- try:
25
- return bytes_.decode("utf-8")
26
- except UnicodeDecodeError:
27
- pass
28
-
29
- charset_match = from_bytes(bytes_).best()
30
- if charset_match is None:
31
- raise CharacterSetDecodeError()
32
- try:
33
- return bytes_.decode(charset_match.encoding)
34
- except UnicodeDecodeError as e:
35
- raise CharacterSetDecodeError() from e
36
-
37
-
38
- def run(cmd: str, env: Mapping[str, str] | None = None) -> Command:
39
- if env is not None:
40
- env = {**os.environ, **env}
41
- process = subprocess.Popen(
42
- cmd,
43
- shell=True,
44
- stdout=subprocess.PIPE,
45
- stderr=subprocess.PIPE,
46
- stdin=subprocess.PIPE,
47
- env=env,
48
- )
49
- stdout, stderr = process.communicate()
50
- return_code = process.returncode
51
- return Command(
52
- _try_decode(stdout),
53
- _try_decode(stderr),
54
- stdout,
55
- stderr,
56
- return_code,
57
- )
@@ -1,86 +0,0 @@
1
- import platform
2
- import sys
3
- from typing import TypedDict
4
-
5
- from commitizen import out
6
- from commitizen.__version__ import __version__
7
- from commitizen.config import BaseConfig
8
- from commitizen.exceptions import NoVersionSpecifiedError, VersionSchemeUnknown
9
- from commitizen.providers import get_provider
10
- from commitizen.tags import TagRules
11
- from commitizen.version_schemes import get_version_scheme
12
-
13
-
14
- class VersionArgs(TypedDict, total=False):
15
- commitizen: bool
16
- report: bool
17
- project: bool
18
- verbose: bool
19
- major: bool
20
- minor: bool
21
- tag: bool
22
-
23
-
24
- class Version:
25
- """Get the version of the installed commitizen or the current project.
26
- Precedence:
27
- 1. report
28
- 2. commitizen
29
- 3. verbose, project
30
- """
31
-
32
- def __init__(self, config: BaseConfig, arguments: VersionArgs) -> None:
33
- self.config: BaseConfig = config
34
- self.arguments = arguments
35
-
36
- def __call__(self) -> None:
37
- if self.arguments.get("report"):
38
- out.write(f"Commitizen Version: {__version__}")
39
- out.write(f"Python Version: {sys.version}")
40
- out.write(f"Operating System: {platform.system()}")
41
- return
42
-
43
- if self.arguments.get("verbose"):
44
- out.write(f"Installed Commitizen Version: {__version__}")
45
-
46
- if not self.arguments.get("commitizen") and (
47
- self.arguments.get("project") or self.arguments.get("verbose")
48
- ):
49
- try:
50
- version = get_provider(self.config).get_version()
51
- except NoVersionSpecifiedError:
52
- out.error("No project information in this project.")
53
- return
54
- try:
55
- version_scheme = get_version_scheme(self.config.settings)(version)
56
- except VersionSchemeUnknown:
57
- out.error("Unknown version scheme.")
58
- return
59
-
60
- if self.arguments.get("major"):
61
- version = f"{version_scheme.major}"
62
- elif self.arguments.get("minor"):
63
- version = f"{version_scheme.minor}"
64
- elif self.arguments.get("tag"):
65
- tag_rules = TagRules.from_settings(self.config.settings)
66
- version = tag_rules.normalize_tag(version_scheme)
67
-
68
- out.write(
69
- f"Project Version: {version}"
70
- if self.arguments.get("verbose")
71
- else version
72
- )
73
- return
74
-
75
- if self.arguments.get("major") or self.arguments.get("minor"):
76
- out.error(
77
- "Major or minor version can only be used with --project or --verbose."
78
- )
79
- return
80
-
81
- if self.arguments.get("tag"):
82
- out.error("Tag can only be used with --project or --verbose.")
83
- return
84
-
85
- # If no arguments are provided, just show the installed commitizen version
86
- out.write(__version__)
File without changes
File without changes