@adonis0123/weekly-report 1.0.5
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.
- package/.claude-skill.json +46 -0
- package/README.md +63 -0
- package/SKILL.md +174 -0
- package/install-skill.js +315 -0
- package/package.json +35 -0
- package/references/WEEKLY_REPORT_FORMAT.md +116 -0
- package/src/__init__.py +3 -0
- package/src/config_manager.py +171 -0
- package/src/date_utils.py +272 -0
- package/src/git_analyzer.py +342 -0
- package/src/report_generator.py +257 -0
- package/src/storage.py +491 -0
- package/uninstall-skill.js +191 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"""Git 分析器模块
|
|
2
|
+
|
|
3
|
+
提供 Git 仓库提交记录分析功能。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
import subprocess
|
|
8
|
+
from datetime import date, timedelta
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# 琐碎提交的关键词
|
|
14
|
+
TRIVIAL_PATTERNS = [
|
|
15
|
+
r"^fix\s*typo",
|
|
16
|
+
r"^typo",
|
|
17
|
+
r"^update\s*(readme|changelog)",
|
|
18
|
+
r"^merge\s+branch",
|
|
19
|
+
r"^merge\s+pull\s+request",
|
|
20
|
+
r"^wip$",
|
|
21
|
+
r"^wip:",
|
|
22
|
+
r"^format",
|
|
23
|
+
r"^lint",
|
|
24
|
+
r"^style:",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_git_user(repo_path: Path) -> Optional[str]:
|
|
29
|
+
"""获取 Git 用户名
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
repo_path: 仓库路径
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
用户名,未配置时返回 None
|
|
36
|
+
"""
|
|
37
|
+
try:
|
|
38
|
+
result = subprocess.run(
|
|
39
|
+
["git", "config", "user.name"],
|
|
40
|
+
cwd=repo_path,
|
|
41
|
+
capture_output=True,
|
|
42
|
+
text=True,
|
|
43
|
+
)
|
|
44
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
45
|
+
return result.stdout.strip()
|
|
46
|
+
return None
|
|
47
|
+
except Exception:
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_git_user_email(repo_path: Path) -> Optional[str]:
|
|
52
|
+
"""获取 Git 用户邮箱
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
repo_path: 仓库路径
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
邮箱,未配置时返回 None
|
|
59
|
+
"""
|
|
60
|
+
try:
|
|
61
|
+
result = subprocess.run(
|
|
62
|
+
["git", "config", "user.email"],
|
|
63
|
+
cwd=repo_path,
|
|
64
|
+
capture_output=True,
|
|
65
|
+
text=True,
|
|
66
|
+
)
|
|
67
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
68
|
+
return result.stdout.strip()
|
|
69
|
+
return None
|
|
70
|
+
except Exception:
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _escape_git_author_pattern(value: str) -> str:
|
|
75
|
+
# git log --author 使用正则匹配;这里做最小转义,避免邮箱/括号等字符影响匹配。
|
|
76
|
+
return re.sub(r"([\\.^$|?*+()[\]{}])", r"\\\1", value)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def build_author_pattern(
|
|
80
|
+
user_name: Optional[str],
|
|
81
|
+
user_email: Optional[str],
|
|
82
|
+
) -> Optional[str]:
|
|
83
|
+
"""构建 git log --author 的匹配模式(name/email 任一匹配即视为本人)"""
|
|
84
|
+
parts: List[str] = []
|
|
85
|
+
if user_name and user_name.strip():
|
|
86
|
+
parts.append(_escape_git_author_pattern(user_name.strip()))
|
|
87
|
+
if user_email and user_email.strip():
|
|
88
|
+
parts.append(_escape_git_author_pattern(user_email.strip()))
|
|
89
|
+
|
|
90
|
+
if not parts:
|
|
91
|
+
return None
|
|
92
|
+
if len(parts) == 1:
|
|
93
|
+
return parts[0]
|
|
94
|
+
return "(" + "|".join(parts) + ")"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def get_commits(
|
|
98
|
+
repo_path: Path,
|
|
99
|
+
start_date: date,
|
|
100
|
+
end_date: date,
|
|
101
|
+
author: Optional[str] = None,
|
|
102
|
+
) -> List[Dict[str, Any]]:
|
|
103
|
+
"""获取指定日期范围内的提交记录
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
repo_path: 仓库路径
|
|
107
|
+
start_date: 开始日期
|
|
108
|
+
end_date: 结束日期
|
|
109
|
+
author: 作者名(可选)
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
提交记录列表
|
|
113
|
+
"""
|
|
114
|
+
# git log 的 --until=YYYY-MM-DD 会被解析为当天 00:00:00,
|
|
115
|
+
# 可能导致“结束日当天”的提交被排除;这里将截止时间调整到下一天 00:00。
|
|
116
|
+
end_date_exclusive = end_date + timedelta(days=1)
|
|
117
|
+
|
|
118
|
+
# 构建 git log 命令
|
|
119
|
+
cmd = [
|
|
120
|
+
"git",
|
|
121
|
+
"log",
|
|
122
|
+
"--all",
|
|
123
|
+
f"--since={start_date.isoformat()}",
|
|
124
|
+
f"--until={end_date_exclusive.isoformat()}",
|
|
125
|
+
"--pretty=format:%H|%s|%an|%ad",
|
|
126
|
+
"--date=short",
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
if author:
|
|
130
|
+
cmd.append(f"--author={author}")
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
result = subprocess.run(
|
|
134
|
+
cmd,
|
|
135
|
+
cwd=repo_path,
|
|
136
|
+
capture_output=True,
|
|
137
|
+
text=True,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
if result.returncode != 0 or not result.stdout.strip():
|
|
141
|
+
return []
|
|
142
|
+
|
|
143
|
+
commits = []
|
|
144
|
+
for line in result.stdout.strip().split("\n"):
|
|
145
|
+
if not line:
|
|
146
|
+
continue
|
|
147
|
+
|
|
148
|
+
parts = line.split("|")
|
|
149
|
+
if len(parts) >= 4:
|
|
150
|
+
parsed = parse_commit_message(parts[1])
|
|
151
|
+
commits.append({
|
|
152
|
+
"hash": parts[0],
|
|
153
|
+
"message": parts[1],
|
|
154
|
+
"author": parts[2],
|
|
155
|
+
"date": parts[3],
|
|
156
|
+
"type": parsed["type"],
|
|
157
|
+
"is_trivial": parsed["is_trivial"],
|
|
158
|
+
"project": get_repo_name(repo_path),
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
return commits
|
|
162
|
+
except Exception:
|
|
163
|
+
return []
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def group_commits_by_project(
|
|
167
|
+
commits: List[Dict[str, Any]]
|
|
168
|
+
) -> Dict[str, List[Dict[str, Any]]]:
|
|
169
|
+
"""按项目分组提交记录
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
commits: 提交记录列表
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
按项目分组的提交记录字典
|
|
176
|
+
"""
|
|
177
|
+
grouped: Dict[str, List[Dict[str, Any]]] = {}
|
|
178
|
+
|
|
179
|
+
for commit in commits:
|
|
180
|
+
project = commit.get("project", "unknown")
|
|
181
|
+
if project not in grouped:
|
|
182
|
+
grouped[project] = []
|
|
183
|
+
grouped[project].append(commit)
|
|
184
|
+
|
|
185
|
+
return grouped
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def parse_commit_message(message: str) -> Dict[str, Any]:
|
|
189
|
+
"""解析提交信息
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
message: 提交信息
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
解析后的提交信息字典
|
|
196
|
+
"""
|
|
197
|
+
result = {
|
|
198
|
+
"type": "other",
|
|
199
|
+
"scope": None,
|
|
200
|
+
"description": message,
|
|
201
|
+
"is_trivial": False,
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
# 检查是否为琐碎提交
|
|
205
|
+
message_lower = message.lower().strip()
|
|
206
|
+
for pattern in TRIVIAL_PATTERNS:
|
|
207
|
+
if re.match(pattern, message_lower, re.IGNORECASE):
|
|
208
|
+
result["is_trivial"] = True
|
|
209
|
+
break
|
|
210
|
+
|
|
211
|
+
# 解析常规提交格式: type(scope): description
|
|
212
|
+
conventional_pattern = r"^(\w+)(?:\(([^)]+)\))?\s*:\s*(.+)$"
|
|
213
|
+
match = re.match(conventional_pattern, message)
|
|
214
|
+
|
|
215
|
+
if match:
|
|
216
|
+
result["type"] = match.group(1).lower()
|
|
217
|
+
result["scope"] = match.group(2)
|
|
218
|
+
result["description"] = match.group(3)
|
|
219
|
+
else:
|
|
220
|
+
result["description"] = message
|
|
221
|
+
|
|
222
|
+
return result
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def is_git_repo(path: Path) -> bool:
|
|
226
|
+
"""检查路径是否为 Git 仓库
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
path: 路径
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
是否为 Git 仓库
|
|
233
|
+
"""
|
|
234
|
+
git_dir = path / ".git"
|
|
235
|
+
return git_dir.exists() and git_dir.is_dir()
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def get_repo_name(repo_path: Path) -> str:
|
|
239
|
+
"""获取仓库名称
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
repo_path: 仓库路径
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
仓库名称
|
|
246
|
+
"""
|
|
247
|
+
return repo_path.name
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def scan_repos(
|
|
251
|
+
repo_paths: List[Path],
|
|
252
|
+
) -> List[Dict[str, Any]]:
|
|
253
|
+
"""扫描多个仓库
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
repo_paths: 仓库路径列表
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
有效仓库信息列表
|
|
260
|
+
"""
|
|
261
|
+
repos = []
|
|
262
|
+
|
|
263
|
+
for path in repo_paths:
|
|
264
|
+
if isinstance(path, str):
|
|
265
|
+
path = Path(path)
|
|
266
|
+
|
|
267
|
+
if is_git_repo(path):
|
|
268
|
+
repos.append({
|
|
269
|
+
"path": path,
|
|
270
|
+
"name": get_repo_name(path),
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
return repos
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def merge_commits_from_repos(
|
|
277
|
+
commits_by_repo: Dict[str, List[Dict[str, Any]]]
|
|
278
|
+
) -> List[Dict[str, Any]]:
|
|
279
|
+
"""合并多仓库提交记录
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
commits_by_repo: 按仓库分组的提交记录
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
合并后的提交记录列表
|
|
286
|
+
"""
|
|
287
|
+
merged = []
|
|
288
|
+
|
|
289
|
+
for repo_name, commits in commits_by_repo.items():
|
|
290
|
+
for commit in commits:
|
|
291
|
+
# 确保每个提交都有 project 字段
|
|
292
|
+
if "project" not in commit:
|
|
293
|
+
commit["project"] = repo_name
|
|
294
|
+
merged.append(commit)
|
|
295
|
+
|
|
296
|
+
# 按日期排序
|
|
297
|
+
merged.sort(key=lambda x: x.get("date", ""), reverse=True)
|
|
298
|
+
|
|
299
|
+
return merged
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def get_all_commits_from_repos(
|
|
303
|
+
repo_paths: List[Path],
|
|
304
|
+
start_date: date,
|
|
305
|
+
end_date: date,
|
|
306
|
+
author: Optional[str] = None,
|
|
307
|
+
) -> Dict[str, List[Dict[str, Any]]]:
|
|
308
|
+
"""从多个仓库获取提交记录
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
repo_paths: 仓库路径列表
|
|
312
|
+
start_date: 开始日期
|
|
313
|
+
end_date: 结束日期
|
|
314
|
+
author: 作者名(可选,None 表示自动获取)
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
按仓库分组的提交记录
|
|
318
|
+
"""
|
|
319
|
+
commits_by_repo: Dict[str, List[Dict[str, Any]]] = {}
|
|
320
|
+
|
|
321
|
+
for path in repo_paths:
|
|
322
|
+
if isinstance(path, str):
|
|
323
|
+
path = Path(path)
|
|
324
|
+
|
|
325
|
+
if not is_git_repo(path):
|
|
326
|
+
continue
|
|
327
|
+
|
|
328
|
+
# 如果没有指定作者,自动获取
|
|
329
|
+
current_author = author
|
|
330
|
+
if current_author is None:
|
|
331
|
+
current_author = build_author_pattern(
|
|
332
|
+
user_name=get_git_user(path),
|
|
333
|
+
user_email=get_git_user_email(path),
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
repo_name = get_repo_name(path)
|
|
337
|
+
commits = get_commits(path, start_date, end_date, current_author)
|
|
338
|
+
|
|
339
|
+
if commits:
|
|
340
|
+
commits_by_repo[repo_name] = commits
|
|
341
|
+
|
|
342
|
+
return commits_by_repo
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""周报生成器模块
|
|
2
|
+
|
|
3
|
+
根据 Git 提交记录生成结构化周报。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
from src.git_analyzer import group_commits_by_project
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def generate_report(
|
|
13
|
+
commits: List[Dict[str, Any]],
|
|
14
|
+
supplements: Optional[List[str]] = None,
|
|
15
|
+
) -> str:
|
|
16
|
+
"""生成周报
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
commits: 提交记录列表
|
|
20
|
+
supplements: 补充内容列表
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Markdown 格式的周报内容
|
|
24
|
+
"""
|
|
25
|
+
if not commits and not supplements:
|
|
26
|
+
return ""
|
|
27
|
+
|
|
28
|
+
# 过滤琐碎提交
|
|
29
|
+
filtered_commits = filter_trivial_commits(commits)
|
|
30
|
+
|
|
31
|
+
# 按项目分组
|
|
32
|
+
grouped = group_commits_by_project(filtered_commits)
|
|
33
|
+
|
|
34
|
+
# 生成周报内容
|
|
35
|
+
sections = []
|
|
36
|
+
|
|
37
|
+
# 按项目生成各部分
|
|
38
|
+
for project, project_commits in sorted(grouped.items()):
|
|
39
|
+
# 合并相关提交
|
|
40
|
+
merged = merge_related_commits(project_commits)
|
|
41
|
+
section = format_project_section(project, merged)
|
|
42
|
+
sections.append(section)
|
|
43
|
+
|
|
44
|
+
# 添加"其他"部分(补充内容)
|
|
45
|
+
if supplements:
|
|
46
|
+
other_section = format_other_section(supplements)
|
|
47
|
+
sections.append(other_section)
|
|
48
|
+
|
|
49
|
+
return "\n\n".join(sections)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def filter_trivial_commits(
|
|
53
|
+
commits: List[Dict[str, Any]]
|
|
54
|
+
) -> List[Dict[str, Any]]:
|
|
55
|
+
"""过滤琐碎提交
|
|
56
|
+
|
|
57
|
+
过滤规则:
|
|
58
|
+
- typo 修复
|
|
59
|
+
- 纯格式化/lint 调整
|
|
60
|
+
- merge 提交
|
|
61
|
+
- WIP 提交
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
commits: 提交记录列表
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
过滤后的提交列表
|
|
68
|
+
"""
|
|
69
|
+
return [c for c in commits if not c.get("is_trivial", False)]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def merge_related_commits(
|
|
73
|
+
commits: List[Dict[str, Any]]
|
|
74
|
+
) -> List[Dict[str, Any]]:
|
|
75
|
+
"""合并相关提交
|
|
76
|
+
|
|
77
|
+
合并规则:
|
|
78
|
+
- 同一功能的多次迭代合并为一条
|
|
79
|
+
- 问题排查和解决归为一条
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
commits: 提交记录列表
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
合并后的提交列表
|
|
86
|
+
"""
|
|
87
|
+
if not commits:
|
|
88
|
+
return []
|
|
89
|
+
if len(commits) <= 1:
|
|
90
|
+
single = commits[0].copy()
|
|
91
|
+
single.setdefault("details", [])
|
|
92
|
+
return [single]
|
|
93
|
+
|
|
94
|
+
# 按关键词分组
|
|
95
|
+
groups: Dict[str, List[Dict[str, Any]]] = {}
|
|
96
|
+
|
|
97
|
+
for commit in commits:
|
|
98
|
+
# 提取关键词
|
|
99
|
+
keywords = extract_keywords(commit["message"])
|
|
100
|
+
key = frozenset(keywords) if keywords else commit["message"]
|
|
101
|
+
|
|
102
|
+
# 转换为字符串 key
|
|
103
|
+
str_key = str(sorted(keywords)) if keywords else commit["message"]
|
|
104
|
+
|
|
105
|
+
if str_key not in groups:
|
|
106
|
+
groups[str_key] = []
|
|
107
|
+
groups[str_key].append(commit)
|
|
108
|
+
|
|
109
|
+
# 合并同组提交(保留主条目 + 子条目细节,避免信息丢失)
|
|
110
|
+
merged: List[Dict[str, Any]] = []
|
|
111
|
+
for group_commits in groups.values():
|
|
112
|
+
main_commit = group_commits[0].copy()
|
|
113
|
+
for c in group_commits:
|
|
114
|
+
if c.get("type") == "feat":
|
|
115
|
+
main_commit = c.copy()
|
|
116
|
+
break
|
|
117
|
+
|
|
118
|
+
details = []
|
|
119
|
+
for c in group_commits:
|
|
120
|
+
details.append(clean_commit_message(c.get("message", "")))
|
|
121
|
+
|
|
122
|
+
# 去重并保持顺序
|
|
123
|
+
seen = set()
|
|
124
|
+
uniq_details = []
|
|
125
|
+
for d in details:
|
|
126
|
+
key = d.strip()
|
|
127
|
+
if not key or key in seen:
|
|
128
|
+
continue
|
|
129
|
+
seen.add(key)
|
|
130
|
+
uniq_details.append(d.strip())
|
|
131
|
+
|
|
132
|
+
main_commit["details"] = uniq_details if len(uniq_details) > 1 else []
|
|
133
|
+
merged.append(main_commit)
|
|
134
|
+
|
|
135
|
+
return merged
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def extract_keywords(message: str) -> List[str]:
|
|
139
|
+
"""从提交信息中提取关键词
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
message: 提交信息
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
关键词列表
|
|
146
|
+
"""
|
|
147
|
+
# 去除前缀
|
|
148
|
+
cleaned = re.sub(r"^(\w+)(\([^)]+\))?\s*:\s*", "", message)
|
|
149
|
+
|
|
150
|
+
# 提取中文词语和英文单词
|
|
151
|
+
chinese_words = re.findall(r"[\u4e00-\u9fff]+", cleaned)
|
|
152
|
+
english_words = re.findall(r"[a-zA-Z]{3,}", cleaned)
|
|
153
|
+
|
|
154
|
+
keywords = chinese_words + [w.lower() for w in english_words]
|
|
155
|
+
|
|
156
|
+
# 过滤常见无意义词
|
|
157
|
+
stop_words = {"the", "and", "for", "with", "this", "that", "from", "into"}
|
|
158
|
+
keywords = [k for k in keywords if k.lower() not in stop_words]
|
|
159
|
+
|
|
160
|
+
return keywords[:3] # 只保留前3个关键词
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def clean_commit_message(message: str) -> str:
|
|
164
|
+
"""清理提交信息为可读描述(去除 conventional 前缀)"""
|
|
165
|
+
return re.sub(r"^(\w+)(\([^)]+\))?\s*:\s*", "", message).strip()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def format_project_section(
|
|
169
|
+
project: str,
|
|
170
|
+
commits: List[Dict[str, Any]],
|
|
171
|
+
) -> str:
|
|
172
|
+
"""格式化项目部分
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
project: 项目名称
|
|
176
|
+
commits: 提交记录列表
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
格式化的 Markdown 内容
|
|
180
|
+
"""
|
|
181
|
+
lines = [project]
|
|
182
|
+
|
|
183
|
+
for commit in commits:
|
|
184
|
+
summary = summarize_commit(commit["message"])
|
|
185
|
+
lines.append(f" - {summary}")
|
|
186
|
+
details = commit.get("details") or []
|
|
187
|
+
for detail in details:
|
|
188
|
+
lines.append(f" - {detail}")
|
|
189
|
+
|
|
190
|
+
return "\n".join(lines)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def format_other_section(supplements: List[str]) -> str:
|
|
194
|
+
"""格式化"其他"部分
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
supplements: 补充内容列表
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
格式化的 Markdown 内容
|
|
201
|
+
"""
|
|
202
|
+
lines = ["其他"]
|
|
203
|
+
|
|
204
|
+
for item in supplements:
|
|
205
|
+
lines.append(f" - {item}")
|
|
206
|
+
|
|
207
|
+
return "\n".join(lines)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def summarize_commit(message: str, max_length: int = 20) -> str:
|
|
211
|
+
"""生成提交摘要
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
message: 提交信息
|
|
215
|
+
max_length: 最大长度
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
摘要文本
|
|
219
|
+
"""
|
|
220
|
+
cleaned = clean_commit_message(message)
|
|
221
|
+
|
|
222
|
+
# 截断过长的文本
|
|
223
|
+
if len(cleaned) > max_length:
|
|
224
|
+
cleaned = cleaned[:max_length - 3] + "..."
|
|
225
|
+
|
|
226
|
+
return cleaned.strip()
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def generate_full_report(
|
|
230
|
+
commits_by_project: Dict[str, List[Dict[str, Any]]],
|
|
231
|
+
supplements: Optional[List[str]] = None,
|
|
232
|
+
date_range: Optional[str] = None,
|
|
233
|
+
) -> str:
|
|
234
|
+
"""生成完整周报
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
commits_by_project: 按项目分组的提交记录
|
|
238
|
+
supplements: 补充内容列表
|
|
239
|
+
date_range: 日期范围描述
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
完整的 Markdown 周报
|
|
243
|
+
"""
|
|
244
|
+
# 合并所有提交
|
|
245
|
+
all_commits = []
|
|
246
|
+
for commits in commits_by_project.values():
|
|
247
|
+
all_commits.extend(commits)
|
|
248
|
+
|
|
249
|
+
# 生成报告内容
|
|
250
|
+
content = generate_report(all_commits, supplements)
|
|
251
|
+
|
|
252
|
+
# 添加标题(如果有日期范围)
|
|
253
|
+
if date_range:
|
|
254
|
+
header = f"# 周报 ({date_range})\n\n"
|
|
255
|
+
content = header + content
|
|
256
|
+
|
|
257
|
+
return content
|