bitool 0.1.2__py3-none-any.whl
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.
- bitool/__init__.py +27 -0
- bitool/cmd/__init__.py +65 -0
- bitool/cmd/_base.py +105 -0
- bitool/cmd/_condition.py +60 -0
- bitool/cmd/_scheduler.py +548 -0
- bitool/cmd/env.py +454 -0
- bitool/cmd/git.py +123 -0
- bitool/cmd/io.py +248 -0
- bitool/cmd/pdf.py +385 -0
- bitool/cmd/run.py +300 -0
- bitool/cmd/toml.py +237 -0
- bitool/cmd/version.py +630 -0
- bitool/consts.py +14 -0
- bitool/core/__init__.py +7 -0
- bitool/core/app.py +142 -0
- bitool/core/commands.py +194 -0
- bitool/core/config.py +647 -0
- bitool/core/env.py +18 -0
- bitool/core/logger.py +237 -0
- bitool/core/plugin.py +117 -0
- bitool/core/workspace.py +76 -0
- bitool/models/__init__.py +3 -0
- bitool/models/version.py +173 -0
- bitool/scripts/__init__.py +1 -0
- bitool/scripts/bumpversion.py +189 -0
- bitool/scripts/clearscreen.py +37 -0
- bitool/scripts/envpy.py +161 -0
- bitool/scripts/envrs.py +119 -0
- bitool/scripts/filedate.py +246 -0
- bitool/scripts/filelevel.py +191 -0
- bitool/scripts/gittool.py +178 -0
- bitool/scripts/img2pdf.py +151 -0
- bitool/scripts/pdf2img.py +139 -0
- bitool/scripts/piptool.py +130 -0
- bitool/scripts/pymake.py +345 -0
- bitool/scripts/sshcopyid.py +491 -0
- bitool/scripts/taskkill.py +366 -0
- bitool/scripts/which.py +227 -0
- bitool/types.py +7 -0
- bitool/utils/__init__.py +9 -0
- bitool/utils/cli_parser.py +412 -0
- bitool/utils/executor.py +881 -0
- bitool/utils/profiler.py +369 -0
- bitool/utils/task.py +133 -0
- bitool/utils/task_group.py +668 -0
- bitool/utils/tests/__init__.py +0 -0
- bitool/utils/tests/test_profiler.py +487 -0
- bitool-0.1.2.dist-info/METADATA +154 -0
- bitool-0.1.2.dist-info/RECORD +51 -0
- bitool-0.1.2.dist-info/WHEEL +4 -0
- bitool-0.1.2.dist-info/entry_points.txt +15 -0
bitool/cmd/version.py
ADDED
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
"""版本号管理相关命令.
|
|
2
|
+
|
|
3
|
+
提供文件和 TOML 文档的版本号读取、更新和递增功能.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import ClassVar
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
import tomli
|
|
15
|
+
import tomli_w
|
|
16
|
+
except ImportError:
|
|
17
|
+
try:
|
|
18
|
+
import tomli_w
|
|
19
|
+
import tomllib as tomli # ty: ignore[unresolved-import]
|
|
20
|
+
except ImportError:
|
|
21
|
+
tomli = None # ty: ignore
|
|
22
|
+
tomli_w = None # ty: ignore
|
|
23
|
+
|
|
24
|
+
from bitool.cmd._base import BaseCommand
|
|
25
|
+
from bitool.core import logger
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class ReadTextVersionCommand(BaseCommand):
|
|
30
|
+
"""从文本文件读取版本号命令.
|
|
31
|
+
|
|
32
|
+
使用正则表达式从文本文件中提取版本号.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
filepath: 文件路径
|
|
36
|
+
pattern: 正则表达式模式
|
|
37
|
+
version: 读取到的版本号
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
name: str = "read-text-version"
|
|
41
|
+
description: str = "从文本文件读取版本号"
|
|
42
|
+
filepath: Path = Path()
|
|
43
|
+
pattern: str = ""
|
|
44
|
+
version: str = ""
|
|
45
|
+
|
|
46
|
+
def run(self) -> bool:
|
|
47
|
+
"""执行文本文件版本号读取.
|
|
48
|
+
|
|
49
|
+
Returns
|
|
50
|
+
-------
|
|
51
|
+
bool
|
|
52
|
+
读取成功返回True, 否则返回False
|
|
53
|
+
"""
|
|
54
|
+
if not self.filepath.exists():
|
|
55
|
+
logger.warning(f"文件不存在: {self.filepath}")
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
if not self.pattern:
|
|
59
|
+
logger.error("未提供正则表达式模式")
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
content = self.filepath.read_text(encoding="utf-8")
|
|
64
|
+
match = re.search(self.pattern, content, re.MULTILINE)
|
|
65
|
+
if not match:
|
|
66
|
+
logger.warning(f"在 {self.filepath} 中未找到版本号")
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
self.version = match.group(1)
|
|
70
|
+
logger.info(f"读取到版本号: {self.filepath} -> {self.version}")
|
|
71
|
+
except Exception as e: # noqa: BLE001
|
|
72
|
+
logger.error(f"读取文本版本号失败: {e}")
|
|
73
|
+
return False
|
|
74
|
+
else:
|
|
75
|
+
return True
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class UpdateTextVersionCommand(BaseCommand):
|
|
80
|
+
"""更新文本文件版本号命令.
|
|
81
|
+
|
|
82
|
+
使用正则表达式替换文本文件中的版本号.
|
|
83
|
+
|
|
84
|
+
Attributes:
|
|
85
|
+
filepath: 文件路径
|
|
86
|
+
pattern: 查找正则表达式模式
|
|
87
|
+
template: 替换模板字符串 (使用 {} 占位新版本号)
|
|
88
|
+
version: 新版本号
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
name: str = "update-text-version"
|
|
92
|
+
description: str = "更新文本文件中的版本号"
|
|
93
|
+
filepath: Path = Path()
|
|
94
|
+
pattern: str = ""
|
|
95
|
+
template: str = ""
|
|
96
|
+
version: str = ""
|
|
97
|
+
|
|
98
|
+
def run(self) -> bool:
|
|
99
|
+
"""执行文本文件版本号更新.
|
|
100
|
+
|
|
101
|
+
Returns
|
|
102
|
+
-------
|
|
103
|
+
bool
|
|
104
|
+
更新成功返回True, 否则返回False
|
|
105
|
+
"""
|
|
106
|
+
if not self.filepath.parent.exists():
|
|
107
|
+
logger.warning(f"目录不存在: {self.filepath.parent}")
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
if not self.pattern or not self.template:
|
|
111
|
+
logger.error("未提供正则表达式模式或替换模板")
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
content = self.filepath.read_text(encoding="utf-8")
|
|
116
|
+
new_content = re.sub(
|
|
117
|
+
self.pattern,
|
|
118
|
+
self.template.format(self.version),
|
|
119
|
+
content,
|
|
120
|
+
flags=re.MULTILINE,
|
|
121
|
+
)
|
|
122
|
+
self.filepath.write_text(new_content, encoding="utf-8")
|
|
123
|
+
logger.info(f"已更新版本号: {self.filepath} -> {self.version}")
|
|
124
|
+
except Exception as e: # noqa: BLE001
|
|
125
|
+
logger.error(f"更新文本版本号失败: {e}")
|
|
126
|
+
return False
|
|
127
|
+
else:
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@dataclass
|
|
132
|
+
class BumpFileVersionCommand(BaseCommand):
|
|
133
|
+
"""文件版本号递增命令.
|
|
134
|
+
|
|
135
|
+
读取文件当前版本号, 递增指定部分, 然后更新文件.
|
|
136
|
+
|
|
137
|
+
Attributes:
|
|
138
|
+
filepath: 文件路径
|
|
139
|
+
version_part: 版本号部分 (major, minor, patch)
|
|
140
|
+
prerelease: 可选的预发布标识
|
|
141
|
+
read_pattern: 读取版本号的正则表达式 (文本文件用)
|
|
142
|
+
update_template: 更新版本号的模板 (文本文件用)
|
|
143
|
+
version_path: 版本号在TOML中的路径 (TOML文件用)
|
|
144
|
+
old_version: 原版本号
|
|
145
|
+
new_version: 新版本号
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
name: str = "bump-file-version"
|
|
149
|
+
description: str = "递增文件中的版本号"
|
|
150
|
+
filepath: Path = Path()
|
|
151
|
+
version_part: str = "patch"
|
|
152
|
+
prerelease: str | None = None
|
|
153
|
+
read_pattern: str = ""
|
|
154
|
+
update_template: str = ""
|
|
155
|
+
version_path: list[str] = field(default_factory=lambda: ["project", "version"])
|
|
156
|
+
old_version: str = ""
|
|
157
|
+
new_version: str = ""
|
|
158
|
+
|
|
159
|
+
def run(self) -> bool:
|
|
160
|
+
"""执行文件版本号递增.
|
|
161
|
+
|
|
162
|
+
Returns
|
|
163
|
+
-------
|
|
164
|
+
bool
|
|
165
|
+
递增成功返回True, 否则返回False
|
|
166
|
+
"""
|
|
167
|
+
if not self.filepath.exists():
|
|
168
|
+
logger.warning(f"文件不存在: {self.filepath}")
|
|
169
|
+
return False
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
# 读取当前版本号
|
|
173
|
+
current_version = self._read_version()
|
|
174
|
+
if not current_version:
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
self.old_version = current_version
|
|
178
|
+
|
|
179
|
+
# 递增版本号
|
|
180
|
+
bumped_version = self._bump_version(current_version)
|
|
181
|
+
if not bumped_version:
|
|
182
|
+
return False
|
|
183
|
+
|
|
184
|
+
self.new_version = bumped_version
|
|
185
|
+
|
|
186
|
+
# 更新文件
|
|
187
|
+
if not self._update_version(bumped_version):
|
|
188
|
+
return False
|
|
189
|
+
|
|
190
|
+
logger.info(
|
|
191
|
+
f"已更新版本号: {self.filepath} ({self.old_version} -> {self.new_version})"
|
|
192
|
+
)
|
|
193
|
+
except Exception as e: # noqa: BLE001
|
|
194
|
+
logger.error(f"递增文件版本号失败: {e}")
|
|
195
|
+
return False
|
|
196
|
+
else:
|
|
197
|
+
return True
|
|
198
|
+
|
|
199
|
+
def _read_version(self) -> str | None:
|
|
200
|
+
"""读取当前版本号.
|
|
201
|
+
|
|
202
|
+
Returns
|
|
203
|
+
-------
|
|
204
|
+
str | None
|
|
205
|
+
当前版本号, 失败时返回None
|
|
206
|
+
"""
|
|
207
|
+
# 判断文件类型 (Cargo.toml 使用文本方式处理)
|
|
208
|
+
if self.filepath.suffix == ".toml" and self.filepath.name != "Cargo.toml":
|
|
209
|
+
return self._read_toml_version()
|
|
210
|
+
else:
|
|
211
|
+
return self._read_text_version()
|
|
212
|
+
|
|
213
|
+
def _read_toml_version(self) -> str | None:
|
|
214
|
+
"""从TOML文件读取版本号.
|
|
215
|
+
|
|
216
|
+
Returns
|
|
217
|
+
-------
|
|
218
|
+
str | None
|
|
219
|
+
版本号, 失败时返回None
|
|
220
|
+
"""
|
|
221
|
+
if tomli is None:
|
|
222
|
+
logger.error("未安装tomli或tomllib库")
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
with self.filepath.open("rb") as f:
|
|
227
|
+
data = tomli.load(f)
|
|
228
|
+
|
|
229
|
+
current = data
|
|
230
|
+
for key in self.version_path:
|
|
231
|
+
if key not in current:
|
|
232
|
+
logger.warning(
|
|
233
|
+
f"在 {self.filepath} 中未找到版本号路径: {'.'.join(self.version_path)}"
|
|
234
|
+
)
|
|
235
|
+
return None
|
|
236
|
+
current = current[key]
|
|
237
|
+
|
|
238
|
+
return str(current)
|
|
239
|
+
except Exception as e: # noqa: BLE001
|
|
240
|
+
logger.error(f"读取TOML版本号失败: {e}")
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
def _read_text_version(self) -> str | None:
|
|
244
|
+
"""从文本文件读取版本号.
|
|
245
|
+
|
|
246
|
+
Returns
|
|
247
|
+
-------
|
|
248
|
+
str | None
|
|
249
|
+
版本号, 失败时返回None
|
|
250
|
+
"""
|
|
251
|
+
if not self.read_pattern:
|
|
252
|
+
logger.error("未提供读取版本号的正则表达式")
|
|
253
|
+
return None
|
|
254
|
+
|
|
255
|
+
try:
|
|
256
|
+
content = self.filepath.read_text(encoding="utf-8")
|
|
257
|
+
match = re.search(self.read_pattern, content, re.MULTILINE)
|
|
258
|
+
if not match:
|
|
259
|
+
logger.warning(f"在 {self.filepath} 中未找到版本号")
|
|
260
|
+
return None
|
|
261
|
+
|
|
262
|
+
return match.group(1)
|
|
263
|
+
except Exception as e: # noqa: BLE001
|
|
264
|
+
logger.error(f"读取文本版本号失败: {e}")
|
|
265
|
+
return None
|
|
266
|
+
|
|
267
|
+
def _bump_version(self, version_str: str) -> str | None:
|
|
268
|
+
"""递增版本号.
|
|
269
|
+
|
|
270
|
+
Parameters
|
|
271
|
+
----------
|
|
272
|
+
version_str : str
|
|
273
|
+
当前版本号字符串
|
|
274
|
+
|
|
275
|
+
Returns
|
|
276
|
+
-------
|
|
277
|
+
str | None
|
|
278
|
+
递增后的版本号, 失败时返回None
|
|
279
|
+
"""
|
|
280
|
+
try:
|
|
281
|
+
# 解析版本号
|
|
282
|
+
version_pattern = re.compile(
|
|
283
|
+
r"(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)"
|
|
284
|
+
r"(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?"
|
|
285
|
+
r"(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?"
|
|
286
|
+
)
|
|
287
|
+
match = version_pattern.match(version_str)
|
|
288
|
+
if not match:
|
|
289
|
+
logger.error(f"无效的版本号格式: {version_str}")
|
|
290
|
+
return None
|
|
291
|
+
|
|
292
|
+
major = int(match.group("major"))
|
|
293
|
+
minor = int(match.group("minor"))
|
|
294
|
+
patch = int(match.group("patch"))
|
|
295
|
+
|
|
296
|
+
# 递增指定部分
|
|
297
|
+
if self.version_part == "major":
|
|
298
|
+
major += 1
|
|
299
|
+
minor = 0
|
|
300
|
+
patch = 0
|
|
301
|
+
prerelease = None
|
|
302
|
+
elif self.version_part == "minor":
|
|
303
|
+
minor += 1
|
|
304
|
+
patch = 0
|
|
305
|
+
prerelease = None
|
|
306
|
+
elif self.version_part == "patch":
|
|
307
|
+
patch += 1
|
|
308
|
+
prerelease = None
|
|
309
|
+
else:
|
|
310
|
+
logger.error(f"不支持的版本号部分: {self.version_part}")
|
|
311
|
+
return None
|
|
312
|
+
|
|
313
|
+
# 设置预发布标识
|
|
314
|
+
if self.prerelease:
|
|
315
|
+
prerelease: str = self.prerelease
|
|
316
|
+
|
|
317
|
+
# 构建新版本号
|
|
318
|
+
new_version = f"{major}.{minor}.{patch}"
|
|
319
|
+
if prerelease:
|
|
320
|
+
new_version += f"-{prerelease}"
|
|
321
|
+
except Exception as e: # noqa: BLE001
|
|
322
|
+
logger.error(f"递增版本号失败: {e}")
|
|
323
|
+
return None
|
|
324
|
+
else:
|
|
325
|
+
return new_version
|
|
326
|
+
|
|
327
|
+
def _update_version(self, new_version: str) -> bool:
|
|
328
|
+
"""更新文件中的版本号.
|
|
329
|
+
|
|
330
|
+
Parameters
|
|
331
|
+
----------
|
|
332
|
+
new_version : str
|
|
333
|
+
新版本号
|
|
334
|
+
|
|
335
|
+
Returns
|
|
336
|
+
-------
|
|
337
|
+
bool
|
|
338
|
+
更新成功返回True, 否则返回False
|
|
339
|
+
"""
|
|
340
|
+
# 判断文件类型 (Cargo.toml 使用文本方式处理)
|
|
341
|
+
if self.filepath.suffix == ".toml" and self.filepath.name != "Cargo.toml":
|
|
342
|
+
return self._update_toml_version(new_version)
|
|
343
|
+
else:
|
|
344
|
+
return self._update_text_version(new_version)
|
|
345
|
+
|
|
346
|
+
def _update_toml_version(self, new_version: str) -> bool:
|
|
347
|
+
"""更新TOML文件中的版本号.
|
|
348
|
+
|
|
349
|
+
使用文本替换方式, 只更新版本号所在行, 保持其他内容格式不变.
|
|
350
|
+
|
|
351
|
+
Parameters
|
|
352
|
+
----------
|
|
353
|
+
new_version : str
|
|
354
|
+
新版本号
|
|
355
|
+
|
|
356
|
+
Returns
|
|
357
|
+
-------
|
|
358
|
+
bool
|
|
359
|
+
更新成功返回True, 否则返回False
|
|
360
|
+
"""
|
|
361
|
+
if tomli is None:
|
|
362
|
+
logger.error("未安装tomli或tomllib库")
|
|
363
|
+
return False
|
|
364
|
+
|
|
365
|
+
try:
|
|
366
|
+
# 先读取并验证版本号路径
|
|
367
|
+
with self.filepath.open("rb") as f:
|
|
368
|
+
data = tomli.load(f)
|
|
369
|
+
|
|
370
|
+
current = data
|
|
371
|
+
for key in self.version_path:
|
|
372
|
+
if key not in current:
|
|
373
|
+
logger.warning(
|
|
374
|
+
f"在 {self.filepath} 中未找到版本号路径: {'.'.join(self.version_path)}"
|
|
375
|
+
)
|
|
376
|
+
return False
|
|
377
|
+
current = current[key]
|
|
378
|
+
|
|
379
|
+
# 使用文本替换方式更新版本号, 保持其他格式不变
|
|
380
|
+
content = self.filepath.read_text(encoding="utf-8")
|
|
381
|
+
|
|
382
|
+
# 构建版本号键名 (例如: project.version -> version)
|
|
383
|
+
version_key = self.version_path[-1]
|
|
384
|
+
|
|
385
|
+
# 匹配版本号行: version = "1.0.0" 或 version = '1.0.0'
|
|
386
|
+
# 支持行首缩进、等号对齐、不同的引号格式
|
|
387
|
+
# (\s*) 捕获行首缩进
|
|
388
|
+
# ({version_key}) 捕获键名
|
|
389
|
+
# (\s*=\s*) 捕获等号及其前后的空格(保持对齐)
|
|
390
|
+
pattern = rf'^(\s*)({version_key})(\s*=\s*)(["\'])([^"\']+)(\4)'
|
|
391
|
+
|
|
392
|
+
def replace_version(match: re.Match) -> str:
|
|
393
|
+
indent = match.group(1) # 行首缩进
|
|
394
|
+
key = match.group(2) # 键名 (version)
|
|
395
|
+
equals_spacing = match.group(3) # 等号及前后空格 ( = )
|
|
396
|
+
quote = match.group(4) # " 或 '
|
|
397
|
+
return f"{indent}{key}{equals_spacing}{quote}{new_version}{quote}"
|
|
398
|
+
|
|
399
|
+
new_content = re.sub(pattern, replace_version, content, flags=re.MULTILINE)
|
|
400
|
+
|
|
401
|
+
if new_content == content:
|
|
402
|
+
logger.warning(f"在 {self.filepath} 中未找到版本号行")
|
|
403
|
+
return False
|
|
404
|
+
|
|
405
|
+
self.filepath.write_text(new_content, encoding="utf-8")
|
|
406
|
+
except Exception as e: # noqa: BLE001
|
|
407
|
+
logger.error(f"更新TOML版本号失败: {e}")
|
|
408
|
+
return False
|
|
409
|
+
else:
|
|
410
|
+
return True
|
|
411
|
+
|
|
412
|
+
def _update_text_version(self, new_version: str) -> bool:
|
|
413
|
+
"""更新文本文件中的版本号.
|
|
414
|
+
|
|
415
|
+
Parameters
|
|
416
|
+
----------
|
|
417
|
+
new_version : str
|
|
418
|
+
新版本号
|
|
419
|
+
|
|
420
|
+
Returns
|
|
421
|
+
-------
|
|
422
|
+
bool
|
|
423
|
+
更新成功返回True, 否则返回False
|
|
424
|
+
"""
|
|
425
|
+
if not self.read_pattern:
|
|
426
|
+
logger.error("未提供读取模式")
|
|
427
|
+
return False
|
|
428
|
+
|
|
429
|
+
try:
|
|
430
|
+
content = self.filepath.read_text(encoding="utf-8")
|
|
431
|
+
|
|
432
|
+
# 如果提供了 update_template, 使用原有逻辑(向后兼容)
|
|
433
|
+
if self.update_template:
|
|
434
|
+
new_content = re.sub(
|
|
435
|
+
self.read_pattern,
|
|
436
|
+
self.update_template.format(new_version),
|
|
437
|
+
content,
|
|
438
|
+
flags=re.MULTILINE,
|
|
439
|
+
)
|
|
440
|
+
else:
|
|
441
|
+
# 如果没有提供 update_template, 智能替换以保持格式对齐
|
|
442
|
+
# 从 read_pattern 中提取键名(假设是 version)
|
|
443
|
+
# 使用与 _update_toml_version 相同的逻辑保持空格对齐
|
|
444
|
+
# read_pattern 应该是像 ^version\s*=\s*"([^"]+)" 这样的格式
|
|
445
|
+
# 我们需要提取键名
|
|
446
|
+
key_match = re.match(r"^\^?\s*(\w+)", self.read_pattern)
|
|
447
|
+
if not key_match:
|
|
448
|
+
logger.error("无法从读取模式中提取键名")
|
|
449
|
+
return False
|
|
450
|
+
|
|
451
|
+
version_key = key_match.group(1)
|
|
452
|
+
pattern = rf'^(\s*)({version_key})(\s*=\s*)(["\'])([^"\']+)(\4)'
|
|
453
|
+
|
|
454
|
+
def replace_version(match: re.Match) -> str:
|
|
455
|
+
indent = match.group(1) # 行首缩进
|
|
456
|
+
key = match.group(2) # 键名 (version)
|
|
457
|
+
equals_spacing = match.group(3) # 等号及前后空格 ( = )
|
|
458
|
+
quote = match.group(4) # " 或 '
|
|
459
|
+
return f"{indent}{key}{equals_spacing}{quote}{new_version}{quote}"
|
|
460
|
+
|
|
461
|
+
new_content = re.sub(
|
|
462
|
+
pattern, replace_version, content, flags=re.MULTILINE
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
self.filepath.write_text(new_content, encoding="utf-8")
|
|
466
|
+
except Exception as e: # noqa: BLE001
|
|
467
|
+
logger.error(f"更新文本版本号失败: {e}")
|
|
468
|
+
return False
|
|
469
|
+
else:
|
|
470
|
+
return True
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
@dataclass
|
|
474
|
+
class BumpProjectVersionCommand(BaseCommand):
|
|
475
|
+
"""递增项目版本号命令.
|
|
476
|
+
|
|
477
|
+
查找项目中的所有版本文件 (pyproject.toml, __init__.py 等),
|
|
478
|
+
并递增它们的版本号.
|
|
479
|
+
|
|
480
|
+
Attributes:
|
|
481
|
+
root_path: 项目根目录路径
|
|
482
|
+
part: 版本号部分 (major, minor, patch)
|
|
483
|
+
prerelease: 可选的预发布标识
|
|
484
|
+
excluded_dirs: 排除的目录名称集合
|
|
485
|
+
exclude_root: 是否排除根目录的 pyproject.toml
|
|
486
|
+
updated_files: 已更新的文件列表
|
|
487
|
+
"""
|
|
488
|
+
|
|
489
|
+
name: str = "bump-project-version"
|
|
490
|
+
description: str = "递增项目中所有文件的版本号"
|
|
491
|
+
root_path: Path = field(default_factory=Path.cwd)
|
|
492
|
+
part: str = "patch"
|
|
493
|
+
prerelease: str | None = None
|
|
494
|
+
excluded_dirs: set[str] = field(
|
|
495
|
+
default_factory=lambda: {
|
|
496
|
+
"__pycache__",
|
|
497
|
+
".venv",
|
|
498
|
+
"venv",
|
|
499
|
+
"env",
|
|
500
|
+
".git",
|
|
501
|
+
"dist",
|
|
502
|
+
"build",
|
|
503
|
+
".tox",
|
|
504
|
+
".pytest_cache",
|
|
505
|
+
}
|
|
506
|
+
)
|
|
507
|
+
exclude_root: bool = False
|
|
508
|
+
updated_files: list[Path] = field(default_factory=list)
|
|
509
|
+
|
|
510
|
+
# 文件类型配置: (文件名, 读取正则, 更新模板)
|
|
511
|
+
FILE_PATTERNS: ClassVar[dict[str, tuple[str, str | None]]] = {
|
|
512
|
+
"__init__.py": (
|
|
513
|
+
r'__version__\s*=\s*["\']([^"\']+)["\']',
|
|
514
|
+
r'__version__ = "{}"',
|
|
515
|
+
),
|
|
516
|
+
"setup.py": (r'version\s*=\s*["\']([^"\']+)["\']', r'version = "{}"'),
|
|
517
|
+
"package.json": (r'"version"\s*:\s*"([^"]+)"', r'"version": "{}"'),
|
|
518
|
+
# Cargo.toml 不提供 update_template, 使用智能替换保持等号对齐
|
|
519
|
+
"Cargo.toml": (r'^version\s*=\s*"([^"]+)"', None),
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
def run(self) -> bool:
|
|
523
|
+
"""执行项目版本号递增.
|
|
524
|
+
|
|
525
|
+
Returns
|
|
526
|
+
-------
|
|
527
|
+
bool
|
|
528
|
+
递增成功返回True, 否则返回False
|
|
529
|
+
"""
|
|
530
|
+
if not self.root_path.exists():
|
|
531
|
+
logger.warning(f"项目目录不存在: {self.root_path}")
|
|
532
|
+
return False
|
|
533
|
+
|
|
534
|
+
try:
|
|
535
|
+
# 查找所有需要更新的版本文件
|
|
536
|
+
version_files = self._find_version_files()
|
|
537
|
+
|
|
538
|
+
if not version_files:
|
|
539
|
+
logger.warning("未找到任何包含版本号的文件")
|
|
540
|
+
return False
|
|
541
|
+
|
|
542
|
+
logger.info(f"找到 {len(version_files)} 个版本文件")
|
|
543
|
+
|
|
544
|
+
# 对每个文件执行版本号递增
|
|
545
|
+
success_count = 0
|
|
546
|
+
for filepath in version_files:
|
|
547
|
+
cmd = self._create_bump_command(filepath)
|
|
548
|
+
if cmd.run():
|
|
549
|
+
self.updated_files.append(filepath)
|
|
550
|
+
success_count += 1
|
|
551
|
+
|
|
552
|
+
logger.info(f"成功更新 {success_count}/{len(version_files)} 个文件")
|
|
553
|
+
except Exception as e: # noqa: BLE001
|
|
554
|
+
logger.error(f"递增项目版本号失败: {e}")
|
|
555
|
+
return False
|
|
556
|
+
else:
|
|
557
|
+
return success_count > 0
|
|
558
|
+
|
|
559
|
+
def _find_version_files(self) -> list[Path]:
|
|
560
|
+
"""查找项目中所有包含版本号的文件.
|
|
561
|
+
|
|
562
|
+
Returns
|
|
563
|
+
-------
|
|
564
|
+
list[Path]
|
|
565
|
+
版本文件路径列表
|
|
566
|
+
"""
|
|
567
|
+
version_files: list[Path] = []
|
|
568
|
+
|
|
569
|
+
# 递归遍历目录
|
|
570
|
+
for filepath in self.root_path.rglob("*"):
|
|
571
|
+
# 跳过排除的目录
|
|
572
|
+
if any(excluded in filepath.parts for excluded in self.excluded_dirs):
|
|
573
|
+
continue
|
|
574
|
+
|
|
575
|
+
# 跳过非文件
|
|
576
|
+
if not filepath.is_file():
|
|
577
|
+
continue
|
|
578
|
+
|
|
579
|
+
# 检查是否是 pyproject.toml
|
|
580
|
+
if filepath.name == "pyproject.toml":
|
|
581
|
+
# 如果排除根目录, 跳过根目录的 pyproject.toml
|
|
582
|
+
if self.exclude_root and filepath.parent == self.root_path:
|
|
583
|
+
continue
|
|
584
|
+
version_files.append(filepath)
|
|
585
|
+
|
|
586
|
+
# 检查其他文件类型
|
|
587
|
+
elif filepath.name in self.FILE_PATTERNS:
|
|
588
|
+
version_files.append(filepath)
|
|
589
|
+
|
|
590
|
+
return version_files
|
|
591
|
+
|
|
592
|
+
def _create_bump_command(self, filepath: Path) -> BumpFileVersionCommand:
|
|
593
|
+
"""为指定文件创建版本号递增命令.
|
|
594
|
+
|
|
595
|
+
Parameters
|
|
596
|
+
----------
|
|
597
|
+
filepath : Path
|
|
598
|
+
文件路径
|
|
599
|
+
|
|
600
|
+
Returns
|
|
601
|
+
-------
|
|
602
|
+
BumpFileVersionCommand
|
|
603
|
+
配置好的版本号递增命令对象
|
|
604
|
+
"""
|
|
605
|
+
# TOML 文件使用 version_path (Cargo.toml 除外)
|
|
606
|
+
if filepath.suffix == ".toml" and filepath.name != "Cargo.toml":
|
|
607
|
+
return BumpFileVersionCommand(
|
|
608
|
+
filepath=filepath,
|
|
609
|
+
version_part=self.part,
|
|
610
|
+
prerelease=self.prerelease,
|
|
611
|
+
version_path=["project", "version"],
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
# 文本文件使用正则表达式
|
|
615
|
+
read_pattern = ""
|
|
616
|
+
update_template: str | None = ""
|
|
617
|
+
|
|
618
|
+
for filename, (pattern, template) in self.FILE_PATTERNS.items():
|
|
619
|
+
if filepath.name == filename:
|
|
620
|
+
read_pattern = pattern
|
|
621
|
+
update_template = template
|
|
622
|
+
break
|
|
623
|
+
|
|
624
|
+
return BumpFileVersionCommand(
|
|
625
|
+
filepath=filepath,
|
|
626
|
+
version_part=self.part,
|
|
627
|
+
prerelease=self.prerelease,
|
|
628
|
+
read_pattern=read_pattern,
|
|
629
|
+
update_template=update_template or "",
|
|
630
|
+
)
|
bitool/consts.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import platform
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass(frozen=True)
|
|
6
|
+
class Constants:
|
|
7
|
+
IS_WINDOWS = platform.system() == "Windows"
|
|
8
|
+
IS_UNIX = platform.system() == "Linux" or platform.system() == "Darwin"
|
|
9
|
+
IS_CYGWIN = platform.system() == "CYGWIN"
|
|
10
|
+
IS_MINGW = platform.system() == "MINGW"
|
|
11
|
+
IS_WSL = platform.system() == "Linux" and "microsoft" in platform.release().lower()
|
|
12
|
+
IS_MSYS = platform.system() == "MSYS"
|
|
13
|
+
|
|
14
|
+
EXTENSION = ".exe" if IS_WINDOWS else ""
|