-
-
Notifications
You must be signed in to change notification settings - Fork 334
Expand file tree
/
Copy pathinit.py
More file actions
364 lines (310 loc) · 13.2 KB
/
init.py
File metadata and controls
364 lines (310 loc) · 13.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
from __future__ import annotations
import os
import shutil
from typing import Any
import questionary
import yaml
from commitizen import cmd, factory, out
from commitizen.__version__ import __version__
from commitizen.config import BaseConfig, JsonConfig, TomlConfig, YAMLConfig
from commitizen.cz import registry
from commitizen.defaults import DEFAULT_SETTINGS, config_files
from commitizen.exceptions import InitFailedError, NoAnswersError
from commitizen.git import get_latest_tag_name, get_tag_names, smart_open
from commitizen.version_schemes import KNOWN_SCHEMES, Version, get_version_scheme
class ProjectInfo:
"""Discover information about the current folder."""
@property
def has_pyproject(self) -> bool:
return os.path.isfile("pyproject.toml")
@property
def has_setup(self) -> bool:
return os.path.isfile("setup.py")
@property
def has_pre_commit_config(self) -> bool:
return os.path.isfile(".pre-commit-config.yaml")
@property
def is_python_poetry(self) -> bool:
if not self.has_pyproject:
return False
with open("pyproject.toml") as f:
return "[tool.poetry]" in f.read()
@property
def is_python(self) -> bool:
return self.has_pyproject or self.has_setup
@property
def is_rust_cargo(self) -> bool:
return os.path.isfile("Cargo.toml")
@property
def is_npm_package(self) -> bool:
return os.path.isfile("package.json")
@property
def is_php_composer(self) -> bool:
return os.path.isfile("composer.json")
@property
def latest_tag(self) -> str | None:
return get_latest_tag_name()
def tags(self) -> list | None:
"""Not a property, only use if necessary"""
if self.latest_tag is None:
return None
return get_tag_names()
@property
def is_pre_commit_installed(self) -> bool:
return bool(shutil.which("pre-commit"))
class Init:
def __init__(self, config: BaseConfig, *args):
self.config: BaseConfig = config
self.encoding = config.settings["encoding"]
self.cz = factory.commiter_factory(self.config)
self.project_info = ProjectInfo()
def __call__(self):
if self.config.path:
out.line(f"Config file {self.config.path} already exists")
return
out.info("Welcome to commitizen!\n")
out.line(
"Answer the questions to configure your project.\n"
"For further configuration visit:\n"
"\n"
"https://commitizen-tools.github.io/commitizen/config/"
"\n"
)
# Collect information
try:
config_path = self._ask_config_path() # select
cz_name = self._ask_name() # select
version_provider = self._ask_version_provider() # select
tag = self._ask_tag() # confirm & select
version_scheme = self._ask_version_scheme() # select
version = get_version_scheme(self.config.settings, version_scheme)(tag)
tag_format = self._ask_tag_format(tag) # confirm & text
update_changelog_on_bump = self._ask_update_changelog_on_bump() # confirm
major_version_zero = self._ask_major_version_zero(version) # confirm
except KeyboardInterrupt:
raise InitFailedError("Stopped by user")
# Initialize configuration
if "toml" in config_path:
self.config = TomlConfig(data="", path=config_path)
elif "json" in config_path:
self.config = JsonConfig(data="{}", path=config_path)
elif "yaml" in config_path:
self.config = YAMLConfig(data="", path=config_path)
values_to_add = {}
values_to_add["name"] = cz_name
values_to_add["tag_format"] = tag_format
values_to_add["version_scheme"] = version_scheme
if version_provider == "commitizen":
values_to_add["version"] = version.public
else:
values_to_add["version_provider"] = version_provider
if update_changelog_on_bump:
values_to_add["update_changelog_on_bump"] = update_changelog_on_bump
if major_version_zero:
values_to_add["major_version_zero"] = major_version_zero
# Collect hook data
hook_types = questionary.checkbox(
"What types of pre-commit hook you want to install? (Leave blank if you don't want to install)",
choices=[
questionary.Choice("commit-msg", checked=False),
questionary.Choice("pre-push", checked=False),
],
).unsafe_ask()
if hook_types:
try:
self._install_pre_commit_hook(hook_types)
except InitFailedError as e:
raise InitFailedError(f"Failed to install pre-commit hook.\n{e}")
# Create and initialize config
self.config.init_empty_config_content()
self._update_config_file(values_to_add)
out.write("\nYou can bump the version running:\n")
out.info("\tcz bump\n")
out.success("Configuration complete 🚀")
def _ask_config_path(self) -> str:
default_path = ".cz.toml"
if self.project_info.has_pyproject:
default_path = "pyproject.toml"
name: str = questionary.select(
"Please choose a supported config file: ",
choices=config_files,
default=default_path,
style=self.cz.style,
).unsafe_ask()
return name
def _ask_name(self) -> str:
name: str = questionary.select(
"Please choose a cz (commit rule): (default: cz_conventional_commits)",
choices=list(registry.keys()),
default="cz_conventional_commits",
style=self.cz.style,
).unsafe_ask()
return name
def _ask_tag(self) -> str:
latest_tag = self.project_info.latest_tag
if not latest_tag:
out.error("No Existing Tag. Set tag to v0.0.1")
return "0.0.1"
is_correct_tag = questionary.confirm(
f"Is {latest_tag} the latest tag?", style=self.cz.style, default=False
).unsafe_ask()
if not is_correct_tag:
tags = self.project_info.tags()
if not tags:
out.error("No Existing Tag. Set tag to v0.0.1")
return "0.0.1"
# the latest tag is most likely with the largest number. Thus list the tags in reverse order makes more sense
sorted_tags = sorted(tags, reverse=True)
latest_tag = questionary.select(
"Please choose the latest tag: ",
choices=sorted_tags,
style=self.cz.style,
).unsafe_ask()
if not latest_tag:
raise NoAnswersError("Tag is required!")
return latest_tag
def _ask_tag_format(self, latest_tag) -> str:
is_correct_format = False
if latest_tag.startswith("v"):
tag_format = r"v$version"
is_correct_format = questionary.confirm(
f'Is "{tag_format}" the correct tag format?', style=self.cz.style
).unsafe_ask()
default_format = DEFAULT_SETTINGS["tag_format"]
if not is_correct_format:
tag_format = questionary.text(
f'Please enter the correct version format: (default: "{default_format}")',
style=self.cz.style,
).unsafe_ask()
if not tag_format:
tag_format = default_format
return tag_format
def _ask_version_provider(self) -> str:
"""Ask for setting: version_provider"""
OPTS = {
"commitizen": "commitizen: Fetch and set version in commitizen config (default)",
"cargo": "cargo: Get and set version from Cargo.toml:project.version field",
"composer": "composer: Get and set version from composer.json:project.version field",
"npm": "npm: Get and set version from package.json:project.version field",
"pep621": "pep621: Get and set version from pyproject.toml:project.version field",
"poetry": "poetry: Get and set version from pyproject.toml:tool.poetry.version field",
"uv": "uv: Get and Get and set version from pyproject.toml and uv.lock",
"scm": "scm: Fetch the version from git and does not need to set it back",
}
default_val = "commitizen"
if self.project_info.is_python:
if self.project_info.is_python_poetry:
default_val = "poetry"
else:
default_val = "pep621"
elif self.project_info.is_rust_cargo:
default_val = "cargo"
elif self.project_info.is_npm_package:
default_val = "npm"
elif self.project_info.is_php_composer:
default_val = "composer"
choices = [
questionary.Choice(title=title, value=value)
for value, title in OPTS.items()
]
default = next(filter(lambda x: x.value == default_val, choices))
version_provider: str = questionary.select(
"Choose the source of the version:",
choices=choices,
style=self.cz.style,
default=default,
).unsafe_ask()
return version_provider
def _ask_version_scheme(self) -> str:
"""Ask for setting: version_scheme"""
default = "semver"
if self.project_info.is_python:
default = "pep440"
scheme: str = questionary.select(
"Choose version scheme: ",
choices=list(KNOWN_SCHEMES),
style=self.cz.style,
default=default,
).unsafe_ask()
return scheme
def _ask_major_version_zero(self, version: Version) -> bool:
"""Ask for setting: major_version_zero"""
if version.major > 0:
return False
major_version_zero: bool = questionary.confirm(
"Keep major version zero (0.x) during breaking changes",
default=True,
auto_enter=True,
).unsafe_ask()
return major_version_zero
def _ask_update_changelog_on_bump(self) -> bool:
"Ask for setting: update_changelog_on_bump"
update_changelog_on_bump: bool = questionary.confirm(
"Create changelog automatically on bump",
default=True,
auto_enter=True,
).unsafe_ask()
return update_changelog_on_bump
def _exec_install_pre_commit_hook(self, hook_types: list[str]):
cmd_str = self._gen_pre_commit_cmd(hook_types)
c = cmd.run(cmd_str)
if c.return_code != 0:
err_msg = (
f"Error running {cmd_str}."
"Outputs are attached below:\n"
f"stdout: {c.out}\n"
f"stderr: {c.err}"
)
raise InitFailedError(err_msg)
def _gen_pre_commit_cmd(self, hook_types: list[str]) -> str:
"""Generate pre-commit command according to given hook types"""
if not hook_types:
raise ValueError("At least 1 hook type should be provided.")
cmd_str = "pre-commit install " + " ".join(
f"--hook-type {ty}" for ty in hook_types
)
return cmd_str
def _install_pre_commit_hook(self, hook_types: list[str] | None = None):
pre_commit_config_filename = ".pre-commit-config.yaml"
cz_hook_config = {
"repo": "https://github.com/commitizen-tools/commitizen",
"rev": f"v{__version__}",
"hooks": [
{"id": "commitizen"},
{"id": "commitizen-branch", "stages": ["push"]},
],
}
config_data = {}
if not self.project_info.has_pre_commit_config:
# .pre-commit-config.yaml does not exist
config_data["repos"] = [cz_hook_config]
else:
with open(
pre_commit_config_filename, encoding=self.encoding
) as config_file:
yaml_data = yaml.safe_load(config_file)
if yaml_data:
config_data = yaml_data
if "repos" in config_data:
for pre_commit_hook in config_data["repos"]:
if "commitizen" in pre_commit_hook["repo"]:
out.write("commitizen already in pre-commit config")
break
else:
config_data["repos"].append(cz_hook_config)
else:
# .pre-commit-config.yaml exists but there's no "repos" key
config_data["repos"] = [cz_hook_config]
with smart_open(
pre_commit_config_filename, "w", encoding=self.encoding
) as config_file:
yaml.safe_dump(config_data, stream=config_file)
if not self.project_info.is_pre_commit_installed:
raise InitFailedError("pre-commit is not installed in current environment.")
if hook_types is None:
hook_types = ["commit-msg", "pre-push"]
self._exec_install_pre_commit_hook(hook_types)
out.write("commitizen pre-commit hook is now installed in your '.git'\n")
def _update_config_file(self, values: dict[str, Any]):
for key, value in values.items():
self.config.set_key(key, value)