@adonis0123/weekly-report 1.0.6
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/LICENSE +21 -0
- package/README.md +453 -0
- package/SKILL.md +174 -0
- package/install-skill.js +207 -0
- package/package.json +45 -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 +118 -0
- package/utils.js +94 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""日期处理工具模块
|
|
2
|
+
|
|
3
|
+
提供周报日期范围计算和验证功能。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from datetime import date, datetime, timedelta, timezone
|
|
7
|
+
from typing import Optional, Tuple
|
|
8
|
+
|
|
9
|
+
from dateutil.relativedelta import relativedelta
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# 中国时区(东八区)
|
|
13
|
+
CHINA_TZ = timezone(timedelta(hours=8))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_today_china() -> date:
|
|
17
|
+
"""获取中国时区(东八区)的今天日期
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
中国时区的今天日期
|
|
21
|
+
"""
|
|
22
|
+
return datetime.now(CHINA_TZ).date()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_week_range(offset: int = 0) -> Tuple[date, date]:
|
|
26
|
+
"""获取指定周的日期范围(周一到周日)
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
offset: 周偏移量,0 表示本周,-1 表示上周,以此类推
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
(start_date, end_date): 周一和周日的日期元组
|
|
33
|
+
如果周日在未来,则结束日期为今天
|
|
34
|
+
"""
|
|
35
|
+
today = get_today_china()
|
|
36
|
+
|
|
37
|
+
# 计算本周一
|
|
38
|
+
days_since_monday = today.weekday()
|
|
39
|
+
current_monday = today - timedelta(days=days_since_monday)
|
|
40
|
+
|
|
41
|
+
# 应用偏移量
|
|
42
|
+
target_monday = current_monday + timedelta(weeks=offset)
|
|
43
|
+
target_sunday = target_monday + timedelta(days=6)
|
|
44
|
+
|
|
45
|
+
# 结束日期不能超过今天
|
|
46
|
+
end_date = min(target_sunday, today)
|
|
47
|
+
|
|
48
|
+
return target_monday, end_date
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def validate_date_range(
|
|
52
|
+
start: date, end: date
|
|
53
|
+
) -> Tuple[bool, Optional[str]]:
|
|
54
|
+
"""验证日期范围是否有效
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
start: 开始日期
|
|
58
|
+
end: 结束日期
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
(is_valid, error_message): 验证结果和错误信息
|
|
62
|
+
"""
|
|
63
|
+
today = get_today_china()
|
|
64
|
+
|
|
65
|
+
# 检查开始日期是否晚于结束日期
|
|
66
|
+
if start > end:
|
|
67
|
+
return False, "开始日期不能晚于结束日期"
|
|
68
|
+
|
|
69
|
+
# 检查是否选择了未来日期
|
|
70
|
+
if end > today:
|
|
71
|
+
return False, "不能选择未来日期"
|
|
72
|
+
|
|
73
|
+
# 检查开始日期是否是周一
|
|
74
|
+
if start.weekday() != 0:
|
|
75
|
+
return False, "开始日期必须是周一"
|
|
76
|
+
|
|
77
|
+
return True, None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def is_valid_week(start: date, end: date) -> bool:
|
|
81
|
+
"""检查是否为有效的周一到周日
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
start: 开始日期
|
|
85
|
+
end: 结束日期
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
是否为有效的完整周
|
|
89
|
+
"""
|
|
90
|
+
# 开始日期必须是周一
|
|
91
|
+
if start.weekday() != 0:
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
# 结束日期必须是周日
|
|
95
|
+
if end.weekday() != 6:
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
# 间隔必须是 6 天(同一周)
|
|
99
|
+
if (end - start).days != 6:
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
return True
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def get_week_number(d: date) -> int:
|
|
106
|
+
"""获取日期所在的周数
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
d: 日期
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
ISO 周数 (1-53)
|
|
113
|
+
"""
|
|
114
|
+
return d.isocalendar()[1]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def format_date_range(start: date, end: date) -> str:
|
|
118
|
+
"""格式化日期范围为字符串
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
start: 开始日期
|
|
122
|
+
end: 结束日期
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
格式化的日期范围字符串
|
|
126
|
+
"""
|
|
127
|
+
return f"{start.isoformat()} ~ {end.isoformat()}"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def get_available_weeks(count: int = 5) -> list:
|
|
131
|
+
"""获取可选择的周列表
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
count: 返回的周数量
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
周列表,每项包含 (offset, start_date, end_date, label)
|
|
138
|
+
"""
|
|
139
|
+
weeks = []
|
|
140
|
+
for i in range(count):
|
|
141
|
+
offset = -i
|
|
142
|
+
start, end = get_week_range(offset)
|
|
143
|
+
|
|
144
|
+
if i == 0:
|
|
145
|
+
label = "本周"
|
|
146
|
+
elif i == 1:
|
|
147
|
+
label = "上周"
|
|
148
|
+
else:
|
|
149
|
+
label = f"{i} 周前"
|
|
150
|
+
|
|
151
|
+
weeks.append({
|
|
152
|
+
"offset": offset,
|
|
153
|
+
"start": start,
|
|
154
|
+
"end": end,
|
|
155
|
+
"label": label,
|
|
156
|
+
"display": f"{label} ({format_date_range(start, end)})",
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
return weeks
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# ==================== 时间段报告相关函数 ====================
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def get_half_year_range() -> Tuple[date, date]:
|
|
166
|
+
"""获取前半年的日期范围
|
|
167
|
+
|
|
168
|
+
从当前时间往前推 6 个自然月。
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
(start_date, end_date): 从 6 个月前到今天的日期元组
|
|
172
|
+
"""
|
|
173
|
+
today = get_today_china()
|
|
174
|
+
# 使用 relativedelta 计算 6 个月前的日期
|
|
175
|
+
start_date = today - relativedelta(months=6)
|
|
176
|
+
return start_date, today
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def validate_custom_date_range(start: date, end: date) -> Tuple[bool, Optional[str]]:
|
|
180
|
+
"""验证自定义日期范围(无周一限制)
|
|
181
|
+
|
|
182
|
+
用于时间段报告的日期验证,允许任意日期作为起始日。
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
start: 开始日期
|
|
186
|
+
end: 结束日期
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
(is_valid, error_message): 验证结果和错误信息
|
|
190
|
+
"""
|
|
191
|
+
today = get_today_china()
|
|
192
|
+
|
|
193
|
+
# 检查开始日期是否晚于结束日期
|
|
194
|
+
if start > end:
|
|
195
|
+
return False, "开始日期不能晚于结束日期"
|
|
196
|
+
|
|
197
|
+
# 检查是否选择了未来日期
|
|
198
|
+
if end > today:
|
|
199
|
+
return False, "不能选择未来日期"
|
|
200
|
+
|
|
201
|
+
return True, None
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def format_date_for_filename(start: date, end: date) -> str:
|
|
205
|
+
"""生成用于文件名的日期范围字符串
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
start: 开始日期
|
|
209
|
+
end: 结束日期
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
格式为 "YYYY-MM-DD_to_YYYY-MM-DD"
|
|
213
|
+
"""
|
|
214
|
+
return f"{start.isoformat()}_to_{end.isoformat()}"
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def format_period_title(start: date, end: date) -> str:
|
|
218
|
+
"""生成时间段报告的标题
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
start: 开始日期
|
|
222
|
+
end: 结束日期
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
格式为 "YYYY-MM-DD ~ YYYY-MM-DD"
|
|
226
|
+
"""
|
|
227
|
+
return format_date_range(start, end)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def get_available_time_ranges() -> list:
|
|
231
|
+
"""获取可选择的时间范围列表(包含周报和时间段报告选项)
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
时间范围列表,每项包含 type, start, end, label, display
|
|
235
|
+
type 可选值: "week" 或 "period"
|
|
236
|
+
"""
|
|
237
|
+
ranges = []
|
|
238
|
+
|
|
239
|
+
# 添加本周和上周(保持原有逻辑)
|
|
240
|
+
weeks = get_available_weeks(count=2)
|
|
241
|
+
for week in weeks:
|
|
242
|
+
ranges.append({
|
|
243
|
+
"type": "week",
|
|
244
|
+
"offset": week["offset"],
|
|
245
|
+
"start": week["start"],
|
|
246
|
+
"end": week["end"],
|
|
247
|
+
"label": week["label"],
|
|
248
|
+
"display": week["display"],
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
# 添加前半年选项
|
|
252
|
+
half_year_start, half_year_end = get_half_year_range()
|
|
253
|
+
ranges.append({
|
|
254
|
+
"type": "period",
|
|
255
|
+
"period_name": "前半年",
|
|
256
|
+
"start": half_year_start,
|
|
257
|
+
"end": half_year_end,
|
|
258
|
+
"label": "前半年",
|
|
259
|
+
"display": f"前半年 ({format_date_range(half_year_start, half_year_end)})",
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
# 添加自定义时间段选项
|
|
263
|
+
ranges.append({
|
|
264
|
+
"type": "period",
|
|
265
|
+
"period_name": "custom",
|
|
266
|
+
"start": None, # 用户输入
|
|
267
|
+
"end": None, # 用户输入(今天)
|
|
268
|
+
"label": "自定义时间段",
|
|
269
|
+
"display": "自定义时间段(输入起始日期,截止到今天)",
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
return ranges
|
|
@@ -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
|