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
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""移除文件日期前缀并替换为创建/修改时间."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import concurrent.futures
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import platform
|
|
10
|
+
import re
|
|
11
|
+
import time
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from functools import cached_property, lru_cache
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from re import Pattern
|
|
16
|
+
from typing import Final
|
|
17
|
+
|
|
18
|
+
from bitool.core import logger
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class FileDateConfig:
|
|
22
|
+
"""filedate 工具配置类."""
|
|
23
|
+
|
|
24
|
+
NAME: str = "filedate"
|
|
25
|
+
DETECT_SEPARATORS: str = "-_#.~"
|
|
26
|
+
SEP: str = "_"
|
|
27
|
+
MAX_RETRY: int = 100
|
|
28
|
+
WINDOWS_RESERVED_NAME_LEN: int = 4
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
conf = FileDateConfig()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _is_windows_reserved_name(filename: str) -> bool:
|
|
35
|
+
"""检查给定文件名是否为 Windows 保留名称.
|
|
36
|
+
|
|
37
|
+
Windows 保留名称包括 CON, PRN, AUX, NUL, 以及 COM1-COM9, LPT1-LPT9.
|
|
38
|
+
这些名称在 Windows 系统上不能用作文件或目录名.
|
|
39
|
+
|
|
40
|
+
Returns
|
|
41
|
+
-------
|
|
42
|
+
bool: 如果文件名是 Windows 保留名称则返回 True, 否则返回 False.
|
|
43
|
+
"""
|
|
44
|
+
if platform.system().lower() != "windows":
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
# 规范化文件名为大写并去除扩展名
|
|
48
|
+
name = Path(filename).stem.upper()
|
|
49
|
+
|
|
50
|
+
# 检查基本保留名称
|
|
51
|
+
if name in {"CON", "PRN", "AUX", "NUL"}:
|
|
52
|
+
return True
|
|
53
|
+
|
|
54
|
+
# 检查 COM 和 LPT 设备名称 (COM1-COM9, LPT1-LPT9)
|
|
55
|
+
if (name.startswith(("COM", "LPT"))) and len(
|
|
56
|
+
name
|
|
57
|
+
) == conf.WINDOWS_RESERVED_NAME_LEN:
|
|
58
|
+
try:
|
|
59
|
+
num = int(name[3])
|
|
60
|
+
if 1 <= num <= 9:
|
|
61
|
+
return True
|
|
62
|
+
except ValueError:
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# 日期检测模式
|
|
69
|
+
DATE_PATTERN: Final[Pattern[str]] = re.compile(
|
|
70
|
+
r"(20|19)\d{2}((0[1-9])|(1[012]))((0[1-9])|([12]\d)|(3[01]))"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True)
|
|
75
|
+
class SingleFileRenamer:
|
|
76
|
+
"""单个文件重命名器."""
|
|
77
|
+
|
|
78
|
+
path: Path
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def rename(path: Path) -> None:
|
|
82
|
+
"""将带日期前缀的文件重命名为创建/修改时间."""
|
|
83
|
+
renamer = SingleFileRenamer(path)
|
|
84
|
+
|
|
85
|
+
dest_path = renamer.filepath_renamed
|
|
86
|
+
sequence = 1
|
|
87
|
+
while dest_path.exists() and sequence <= conf.MAX_RETRY:
|
|
88
|
+
logger.warning(f"{dest_path} 已存在, 添加唯一后缀.")
|
|
89
|
+
dest_path = renamer.filepath_renamed.with_name(
|
|
90
|
+
f"{renamer.filestem_renamed}({sequence}){renamer.file_suffix}"
|
|
91
|
+
)
|
|
92
|
+
sequence += 1
|
|
93
|
+
|
|
94
|
+
# 如果达到最大重试次数路径仍存在, 则放弃
|
|
95
|
+
if dest_path.exists() and sequence > conf.MAX_RETRY:
|
|
96
|
+
logger.error(f"已达到最大重试次数, 放弃处理 {renamer.path}.")
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
renamer.path.rename(dest_path)
|
|
101
|
+
except (OSError, PermissionError):
|
|
102
|
+
logger.exception("重命名失败: %s -> %s", renamer.path, dest_path)
|
|
103
|
+
raise
|
|
104
|
+
else:
|
|
105
|
+
logger.info("重命名: %s -> %s", renamer.path, dest_path)
|
|
106
|
+
|
|
107
|
+
@cached_property
|
|
108
|
+
def file_suffix(self) -> str:
|
|
109
|
+
"""获取文件后缀."""
|
|
110
|
+
return self.path.suffix
|
|
111
|
+
|
|
112
|
+
@cached_property
|
|
113
|
+
def filepath_renamed(self) -> Path:
|
|
114
|
+
"""获取重命名后的文件路径."""
|
|
115
|
+
return self.path.with_name(self.filename_renamed)
|
|
116
|
+
|
|
117
|
+
@cached_property
|
|
118
|
+
def filestem_renamed(self) -> str:
|
|
119
|
+
"""获取重命名后的文件名主干."""
|
|
120
|
+
# 从文件名字符串中提取主干
|
|
121
|
+
suffix = self.path.suffix
|
|
122
|
+
return (
|
|
123
|
+
self.filename_renamed[: -len(suffix)]
|
|
124
|
+
if suffix and self.filename_renamed.endswith(suffix)
|
|
125
|
+
else self.filename_renamed
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
@cached_property
|
|
129
|
+
def filename_renamed(self) -> str:
|
|
130
|
+
"""获取重命名后的文件名."""
|
|
131
|
+
return (
|
|
132
|
+
f"{self.time_mark}{conf.SEP}{self.filestem_without_date}{self.path.suffix}"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
@cached_property
|
|
136
|
+
def filestem_without_date(self) -> str:
|
|
137
|
+
"""获取去除日期前缀的文件名主干."""
|
|
138
|
+
|
|
139
|
+
@lru_cache(maxsize=1024)
|
|
140
|
+
def remove_date_prefix(filestem: str) -> str:
|
|
141
|
+
"""从文件名中移除日期前缀.
|
|
142
|
+
|
|
143
|
+
Returns
|
|
144
|
+
-------
|
|
145
|
+
str: 移除了日期前缀的文件名.
|
|
146
|
+
"""
|
|
147
|
+
if (
|
|
148
|
+
filestem.startswith(".")
|
|
149
|
+
and not filestem[1:].isdigit()
|
|
150
|
+
and len(filestem) > 1
|
|
151
|
+
) and not re.search(DATE_PATTERN, filestem):
|
|
152
|
+
return ""
|
|
153
|
+
|
|
154
|
+
match = re.search(DATE_PATTERN, filestem)
|
|
155
|
+
if not match:
|
|
156
|
+
logger.debug("未检测到日期前缀: %s", filestem)
|
|
157
|
+
return filestem
|
|
158
|
+
b, e = match.start(), match.end()
|
|
159
|
+
if b >= 1 and filestem[b - 1] in conf.DETECT_SEPARATORS:
|
|
160
|
+
filestem = filestem[: b - 1] + filestem[e:]
|
|
161
|
+
elif e < len(filestem) and filestem[e] in conf.DETECT_SEPARATORS:
|
|
162
|
+
filestem = filestem[:b] + filestem[e + 1 :]
|
|
163
|
+
return remove_date_prefix(filestem)
|
|
164
|
+
|
|
165
|
+
return remove_date_prefix(self.filestem)
|
|
166
|
+
|
|
167
|
+
@cached_property
|
|
168
|
+
def filestem(self) -> str:
|
|
169
|
+
"""获取当前文件名主干."""
|
|
170
|
+
return self.path.stem
|
|
171
|
+
|
|
172
|
+
@cached_property
|
|
173
|
+
def filestat(self) -> os.stat_result:
|
|
174
|
+
"""获取文件 stat 信息."""
|
|
175
|
+
return self.path.stat()
|
|
176
|
+
|
|
177
|
+
@cached_property
|
|
178
|
+
def modified_time(self) -> float:
|
|
179
|
+
"""获取修改时间."""
|
|
180
|
+
return self.filestat.st_mtime
|
|
181
|
+
|
|
182
|
+
@cached_property
|
|
183
|
+
def created_time(self) -> float:
|
|
184
|
+
"""获取创建时间."""
|
|
185
|
+
return self.filestat.st_ctime
|
|
186
|
+
|
|
187
|
+
@cached_property
|
|
188
|
+
def time_mark(self) -> str:
|
|
189
|
+
"""获取时间标记."""
|
|
190
|
+
return time.strftime(
|
|
191
|
+
"%Y%m%d", time.localtime(max((self.modified_time, self.created_time)))
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@dataclass(frozen=True)
|
|
196
|
+
class MultiFileRenamer:
|
|
197
|
+
"""多文件重命名器."""
|
|
198
|
+
|
|
199
|
+
paths: list[str]
|
|
200
|
+
|
|
201
|
+
@staticmethod
|
|
202
|
+
def rename_files(paths: list[str]) -> None:
|
|
203
|
+
"""将带日期前缀的文件重命名为创建/修改时间."""
|
|
204
|
+
renamer = MultiFileRenamer(paths)
|
|
205
|
+
if not renamer.filtered_paths:
|
|
206
|
+
logger.error("没有有效的文件可处理.")
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
t0 = time.perf_counter()
|
|
210
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor:
|
|
211
|
+
executor.map(SingleFileRenamer.rename, renamer.filtered_paths)
|
|
212
|
+
logger.info("处理完成, 耗时: %.4fs", time.perf_counter() - t0)
|
|
213
|
+
|
|
214
|
+
@cached_property
|
|
215
|
+
def converted_paths(self) -> list[Path]:
|
|
216
|
+
"""获取转换后的路径列表."""
|
|
217
|
+
return [Path(p) for p in self.paths]
|
|
218
|
+
|
|
219
|
+
@cached_property
|
|
220
|
+
def filtered_paths(self) -> list[Path]:
|
|
221
|
+
"""获取过滤后的有效路径列表."""
|
|
222
|
+
return [
|
|
223
|
+
p
|
|
224
|
+
for p in self.converted_paths
|
|
225
|
+
if p.exists() and p.is_file() and not _is_windows_reserved_name(p.name)
|
|
226
|
+
]
|
|
227
|
+
|
|
228
|
+
@cached_property
|
|
229
|
+
def missing_paths(self) -> list[Path]:
|
|
230
|
+
"""获取缺失的路径列表."""
|
|
231
|
+
return list(set(self.converted_paths) - set(self.filtered_paths))
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def main() -> None:
|
|
235
|
+
"""运行命令行界面入口点."""
|
|
236
|
+
parser = argparse.ArgumentParser(
|
|
237
|
+
prog="filedate", description="移除文件日期前缀并替换为创建/修改时间."
|
|
238
|
+
)
|
|
239
|
+
parser.add_argument("targets", type=str, nargs="+", help="输入文件列表")
|
|
240
|
+
parser.add_argument("--debug", "-d", action="store_true", help="启用调试模式")
|
|
241
|
+
args = parser.parse_args()
|
|
242
|
+
|
|
243
|
+
if args.debug:
|
|
244
|
+
logger.setLevel(logging.DEBUG)
|
|
245
|
+
|
|
246
|
+
MultiFileRenamer.rename_files(args.targets)
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""重命名文件等级后缀."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import concurrent.futures
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Final
|
|
11
|
+
|
|
12
|
+
from bitool.core import logger
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FilelevelConfig:
|
|
16
|
+
"""文件等级配置."""
|
|
17
|
+
|
|
18
|
+
LEVELS: Final[dict[str, str]] = {"0": "", "1": "PUB,NOR", "2": "INT", "3": "CON", "4": "CLA"}
|
|
19
|
+
BRACKETS: Final[tuple[str, str]] = (" ([_(【-", " )]_)】")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
conf = FilelevelConfig()
|
|
23
|
+
|
|
24
|
+
_MAX_LEVELS = len(conf.LEVELS)
|
|
25
|
+
_MIN_FILES_FOR_CONCURRENT = 5 # 触发并行处理的最小文件数
|
|
26
|
+
_LEVEL_VALUES = list(conf.LEVELS.values())
|
|
27
|
+
_LEFT_BRACKETS, _RIGHT_BRACKETS = conf.BRACKETS
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def remove_marks(stem: str, marks: list[str]) -> str:
|
|
31
|
+
"""从文件名主干中移除所有标记.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
stem: 文件名主干
|
|
35
|
+
marks: 要移除的标记列表
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
str: 移除了标记的文件名主干
|
|
39
|
+
"""
|
|
40
|
+
for mark in marks:
|
|
41
|
+
pos = 0
|
|
42
|
+
while True:
|
|
43
|
+
pos = stem.find(mark, pos)
|
|
44
|
+
if pos == -1:
|
|
45
|
+
break
|
|
46
|
+
b, e = pos - 1, pos + len(mark)
|
|
47
|
+
if b >= 0 and e < len(stem) and stem[b] in _LEFT_BRACKETS and stem[e] in _RIGHT_BRACKETS:
|
|
48
|
+
stem = stem[:b] + stem[e + 1 :]
|
|
49
|
+
else:
|
|
50
|
+
pos = e
|
|
51
|
+
return stem
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def process_file(target: Path, level: int = 0) -> bool:
|
|
55
|
+
"""处理单个文件,使用指定等级.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
target: 目标文件路径
|
|
59
|
+
level: 文件等级 (0-4), 0 用于清除等级
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
bool: 处理成功返回 True,失败返回 False
|
|
63
|
+
"""
|
|
64
|
+
try:
|
|
65
|
+
if not (0 <= level < _MAX_LEVELS):
|
|
66
|
+
logger.error("无效的等级 %d, 必须在 0 和 %d 之间", level, _MAX_LEVELS - 1)
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
if not target.exists() or not os.access(target, os.W_OK):
|
|
70
|
+
logger.error("文件不存在或不可写:%s", target)
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
# 处理文件名
|
|
74
|
+
filestem = target.stem
|
|
75
|
+
original_stem = filestem
|
|
76
|
+
|
|
77
|
+
# 移除所有等级标记
|
|
78
|
+
for level_names in _LEVEL_VALUES:
|
|
79
|
+
if level_names:
|
|
80
|
+
filestem = remove_marks(filestem, level_names.split(","))
|
|
81
|
+
|
|
82
|
+
# 移除数字标记
|
|
83
|
+
for digit in map(str, range(1, 10)):
|
|
84
|
+
filestem = remove_marks(filestem, [digit])
|
|
85
|
+
|
|
86
|
+
# 仅在文件名改变或需要添加标记时进行处理
|
|
87
|
+
if filestem != original_stem or level > 0:
|
|
88
|
+
# 添加等级标记
|
|
89
|
+
if level > 0:
|
|
90
|
+
levelstr = conf.LEVELS.get(str(level), "").split(",")[0]
|
|
91
|
+
if levelstr:
|
|
92
|
+
filestem = f"{filestem}({levelstr})"
|
|
93
|
+
|
|
94
|
+
# 重命名文件
|
|
95
|
+
target_path = target.with_name(filestem + target.suffix)
|
|
96
|
+
if target_path != target:
|
|
97
|
+
logger.info("重命名:%s -> %s", target, target_path)
|
|
98
|
+
target.rename(target_path)
|
|
99
|
+
except (OSError, ValueError, RuntimeError):
|
|
100
|
+
logger.exception("处理文件 %s 时出错", target)
|
|
101
|
+
return False
|
|
102
|
+
else:
|
|
103
|
+
return True
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _handle_future_exception(future: concurrent.futures.Future) -> None:
|
|
107
|
+
"""安全地获取 Future 结果,捕获并记录异常.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
future: 要检查的 Future 对象
|
|
111
|
+
"""
|
|
112
|
+
try:
|
|
113
|
+
future.result()
|
|
114
|
+
except (OSError, ValueError, RuntimeError):
|
|
115
|
+
logger.exception("处理文件时出错")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _process_files_concurrent(targets: list[Path], level: int, max_workers: int) -> None:
|
|
119
|
+
"""使用线程池并行处理多个文件.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
targets: 目标文件路径列表
|
|
123
|
+
level: 文件等级 (0-4)
|
|
124
|
+
max_workers: 最大工作线程数
|
|
125
|
+
"""
|
|
126
|
+
logger.info("对大量文件使用并行处理")
|
|
127
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
128
|
+
futures = [executor.submit(process_file, target, level) for target in targets]
|
|
129
|
+
for future in concurrent.futures.as_completed(futures):
|
|
130
|
+
_handle_future_exception(future)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def parse_args(args: list[str] | None = None) -> argparse.Namespace:
|
|
134
|
+
"""解析命令行参数.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
args: 命令行参数列表,None 时使用 sys.argv
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
argparse.Namespace: 包含以下内容的已解析命名空间:
|
|
141
|
+
- targets (list[Path]): 要处理的文件
|
|
142
|
+
- level (int): 文件等级 0-4
|
|
143
|
+
- max_workers (int): 线程池大小
|
|
144
|
+
- debug (bool): 启用调试日志
|
|
145
|
+
"""
|
|
146
|
+
parser = argparse.ArgumentParser(
|
|
147
|
+
description="重命名文件等级后缀",
|
|
148
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
149
|
+
epilog="""
|
|
150
|
+
示例:
|
|
151
|
+
filelevel file1.txt file2.txt --level 1
|
|
152
|
+
filelevel *.txt -l 2
|
|
153
|
+
filelevel document.pdf --level 0 # 清除等级
|
|
154
|
+
""",
|
|
155
|
+
)
|
|
156
|
+
parser.add_argument("targets", nargs="+", type=Path, help="要处理的目标文件")
|
|
157
|
+
parser.add_argument(
|
|
158
|
+
"--level",
|
|
159
|
+
"-l",
|
|
160
|
+
type=int,
|
|
161
|
+
default=0,
|
|
162
|
+
choices=range(_MAX_LEVELS),
|
|
163
|
+
help=f"文件等级 (0-{_MAX_LEVELS - 1}), 0 用于清除等级 (默认:0)",
|
|
164
|
+
)
|
|
165
|
+
parser.add_argument("--max-workers", "-w", type=int, default=4, help="并行处理的最大工作线程数 (默认:4)")
|
|
166
|
+
parser.add_argument("--debug", "-d", action="store_true", help="启用调试模式")
|
|
167
|
+
|
|
168
|
+
return parser.parse_args(args)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def main() -> None:
|
|
172
|
+
"""运行命令行界面入口点."""
|
|
173
|
+
args = parse_args()
|
|
174
|
+
|
|
175
|
+
if args.debug:
|
|
176
|
+
logger.setLevel(logging.DEBUG)
|
|
177
|
+
|
|
178
|
+
if not any(Path(t).exists() for t in args.targets):
|
|
179
|
+
logger.error("在以下文件中未找到有效文件:%s", args.targets)
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
if len(args.targets) >= _MIN_FILES_FOR_CONCURRENT:
|
|
183
|
+
_process_files_concurrent(args.targets, args.level, args.max_workers)
|
|
184
|
+
else:
|
|
185
|
+
logger.info("对小量文件使用串行处理")
|
|
186
|
+
for target in args.targets:
|
|
187
|
+
process_file(target, args.level)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
if __name__ == "__main__":
|
|
191
|
+
main()
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Git工具模块.
|
|
2
|
+
|
|
3
|
+
提供Git仓库管理的常用操作封装, 支持初始化、提交、清理、推送等功能.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from bitool.cmd import CommandScheduler, RunCommand
|
|
13
|
+
from bitool.core import ConfigMixin
|
|
14
|
+
from bitool.utils import execute
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class GitToolConfig(ConfigMixin):
|
|
18
|
+
"""Git工具配置类."""
|
|
19
|
+
|
|
20
|
+
CACHE_DIRS_FOR_CLEAN: list[str] = [ # noqa: RUF012
|
|
21
|
+
".coverage",
|
|
22
|
+
".benchmarks",
|
|
23
|
+
".ruff_cache",
|
|
24
|
+
".pytest_cache",
|
|
25
|
+
".mypy_cache",
|
|
26
|
+
"__pycache__",
|
|
27
|
+
".hypothesis",
|
|
28
|
+
"htmlcov",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
EXCLUDE_DIRS_FOR_CLEAN: list[str] = [ # noqa: RUF012
|
|
32
|
+
".venv",
|
|
33
|
+
"node_modules",
|
|
34
|
+
".git",
|
|
35
|
+
".idea",
|
|
36
|
+
".vscode",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
conf = GitToolConfig()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def init_subdirs_scheduler() -> CommandScheduler:
|
|
44
|
+
"""初始化子目录的Git仓库."""
|
|
45
|
+
|
|
46
|
+
def func() -> None:
|
|
47
|
+
"""初始化子目录的Git仓库."""
|
|
48
|
+
origin = Path.cwd()
|
|
49
|
+
subdirs = [d for d in Path.cwd().iterdir() if d.is_dir()]
|
|
50
|
+
for subdir in subdirs:
|
|
51
|
+
os.chdir(str(subdir))
|
|
52
|
+
execute(
|
|
53
|
+
[
|
|
54
|
+
["git", "init"],
|
|
55
|
+
["git", "add", "."],
|
|
56
|
+
["git", "commit", "-m", "init commit"],
|
|
57
|
+
]
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
os.chdir(str(origin))
|
|
61
|
+
|
|
62
|
+
return CommandScheduler(commands=[RunCommand(cmd=func)])
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def create_scheduler_map() -> dict[str, CommandScheduler]:
|
|
66
|
+
"""创建Git工具命令调度器映射."""
|
|
67
|
+
return {
|
|
68
|
+
"a": CommandScheduler(
|
|
69
|
+
commands=[
|
|
70
|
+
RunCommand(
|
|
71
|
+
name="add",
|
|
72
|
+
cmd=["git", "add", "."],
|
|
73
|
+
description="将所有更改添加到暂存区",
|
|
74
|
+
),
|
|
75
|
+
RunCommand(
|
|
76
|
+
name="commit",
|
|
77
|
+
cmd=["git", "commit", "-m", "chore: update"],
|
|
78
|
+
description="提交所有更改",
|
|
79
|
+
),
|
|
80
|
+
]
|
|
81
|
+
),
|
|
82
|
+
"c": CommandScheduler(
|
|
83
|
+
commands=[
|
|
84
|
+
RunCommand(
|
|
85
|
+
name="clean",
|
|
86
|
+
cmd=["git", "clean", "-xfd"]
|
|
87
|
+
+ [arg for d in conf.EXCLUDE_DIRS_FOR_CLEAN for arg in ["-e", d]],
|
|
88
|
+
description="清理工作区",
|
|
89
|
+
),
|
|
90
|
+
RunCommand(
|
|
91
|
+
name="status",
|
|
92
|
+
cmd=["git", "status", "--porcelain"],
|
|
93
|
+
description="显示状态",
|
|
94
|
+
),
|
|
95
|
+
]
|
|
96
|
+
),
|
|
97
|
+
"i": CommandScheduler(
|
|
98
|
+
commands=[
|
|
99
|
+
RunCommand(
|
|
100
|
+
name="init",
|
|
101
|
+
cmd=["git", "init"],
|
|
102
|
+
description="初始化Git仓库",
|
|
103
|
+
forbid_conditions=[
|
|
104
|
+
(
|
|
105
|
+
lambda: (
|
|
106
|
+
".git"
|
|
107
|
+
in [d.name for d in Path.cwd().iterdir() if d.is_dir()]
|
|
108
|
+
),
|
|
109
|
+
"Git仓库已初始化",
|
|
110
|
+
)
|
|
111
|
+
],
|
|
112
|
+
),
|
|
113
|
+
RunCommand(
|
|
114
|
+
name="add",
|
|
115
|
+
cmd=["git", "add", "."],
|
|
116
|
+
description="将所有更改添加到暂存区",
|
|
117
|
+
forbid_conditions=[
|
|
118
|
+
(lambda: not Path.cwd().glob("*"), "没有文件可添加")
|
|
119
|
+
],
|
|
120
|
+
),
|
|
121
|
+
RunCommand(
|
|
122
|
+
name="commit",
|
|
123
|
+
cmd=["git", "commit", "-m", "chore: update"],
|
|
124
|
+
description="提交所有更改",
|
|
125
|
+
forbid_conditions=[
|
|
126
|
+
(lambda: not Path.cwd().glob("*"), "没有文件可提交")
|
|
127
|
+
],
|
|
128
|
+
),
|
|
129
|
+
]
|
|
130
|
+
),
|
|
131
|
+
"isub": init_subdirs_scheduler(),
|
|
132
|
+
"p": CommandScheduler(
|
|
133
|
+
commands=[
|
|
134
|
+
RunCommand(
|
|
135
|
+
name="push", cmd=["git", "push"], description="推送更改到远程仓库"
|
|
136
|
+
)
|
|
137
|
+
]
|
|
138
|
+
),
|
|
139
|
+
"pl": CommandScheduler(
|
|
140
|
+
commands=[
|
|
141
|
+
RunCommand(
|
|
142
|
+
name="pull",
|
|
143
|
+
cmd=["git", "pull"],
|
|
144
|
+
description="从远程仓库拉取最新更改",
|
|
145
|
+
)
|
|
146
|
+
]
|
|
147
|
+
),
|
|
148
|
+
"re": CommandScheduler(
|
|
149
|
+
commands=[
|
|
150
|
+
RunCommand(
|
|
151
|
+
name="重启tgitcache",
|
|
152
|
+
cmd=["taskkill", "/f", "/t", "/im", "tgitcache.exe"],
|
|
153
|
+
description="重启tgitcache服务(如果正在运行)",
|
|
154
|
+
)
|
|
155
|
+
]
|
|
156
|
+
),
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
ACTIONS = list(create_scheduler_map().keys())
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def parse_args() -> argparse.Namespace:
|
|
164
|
+
"""解析命令行参数."""
|
|
165
|
+
parser = argparse.ArgumentParser(description="Git工具")
|
|
166
|
+
parser.add_argument("action", choices=ACTIONS, help="要执行的操作")
|
|
167
|
+
return parser.parse_args()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def main() -> None:
|
|
171
|
+
"""Git工具主函数."""
|
|
172
|
+
args = parse_args()
|
|
173
|
+
scheduler_map = create_scheduler_map()
|
|
174
|
+
scheduler = scheduler_map.get(args.action, None)
|
|
175
|
+
if not scheduler:
|
|
176
|
+
raise ValueError(f"无效的操作: {args.action}")
|
|
177
|
+
|
|
178
|
+
scheduler.run()
|