git-weekly 0.2.0__tar.gz
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.
- git_weekly-0.2.0/LICENSE +21 -0
- git_weekly-0.2.0/PKG-INFO +181 -0
- git_weekly-0.2.0/README.md +151 -0
- git_weekly-0.2.0/git_weekly/__init__.py +3 -0
- git_weekly-0.2.0/git_weekly/__main__.py +4 -0
- git_weekly-0.2.0/git_weekly/analyzer.py +218 -0
- git_weekly-0.2.0/git_weekly/cli.py +129 -0
- git_weekly-0.2.0/git_weekly/llm.py +153 -0
- git_weekly-0.2.0/git_weekly/report.py +147 -0
- git_weekly-0.2.0/git_weekly.egg-info/PKG-INFO +181 -0
- git_weekly-0.2.0/git_weekly.egg-info/SOURCES.txt +16 -0
- git_weekly-0.2.0/git_weekly.egg-info/dependency_links.txt +1 -0
- git_weekly-0.2.0/git_weekly.egg-info/entry_points.txt +2 -0
- git_weekly-0.2.0/git_weekly.egg-info/requires.txt +7 -0
- git_weekly-0.2.0/git_weekly.egg-info/top_level.txt +1 -0
- git_weekly-0.2.0/pyproject.toml +51 -0
- git_weekly-0.2.0/setup.cfg +4 -0
- git_weekly-0.2.0/setup.py +5 -0
git_weekly-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 woaichilajiaochaorou
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: git-weekly
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Generate weekly reports from Git commit history, with optional AI summary
|
|
5
|
+
Author: woaichilajiaochaorou
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/woaichilajiaochaorou/git-weekly
|
|
8
|
+
Project-URL: Repository, https://github.com/woaichilajiaochaorou/git-weekly
|
|
9
|
+
Project-URL: Issues, https://github.com/woaichilajiaochaorou/git-weekly/issues
|
|
10
|
+
Keywords: git,weekly,report,changelog,productivity,ai,llm
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Provides-Extra: ai
|
|
25
|
+
Requires-Dist: openai>=1.0; extra == "ai"
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
28
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# git-weekly
|
|
32
|
+
|
|
33
|
+
从 Git 提交记录自动生成周报,支持 AI 智能总结。告别"我这周干了啥"的灵魂拷问。
|
|
34
|
+
|
|
35
|
+
## 功能
|
|
36
|
+
|
|
37
|
+
- 扫描指定时间范围内的所有 Git 提交
|
|
38
|
+
- 自动分析 diff,归类为:新功能、Bug 修复、重构、文档、测试等
|
|
39
|
+
- 生成结构化周报(Markdown / 终端输出)
|
|
40
|
+
- 支持多仓库聚合
|
|
41
|
+
- 代码统计(新增 / 删除 / 修改的文件数)
|
|
42
|
+
- **AI 智能总结**:接入 OpenAI 兼容 API,自动生成连贯的工作总结
|
|
43
|
+
|
|
44
|
+
## 安装
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install git-weekly
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
如需 AI 总结功能:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install "git-weekly[ai]"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
或从源码安装:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
git clone https://github.com/woaichilajiaochaorou/git-weekly.git
|
|
60
|
+
cd git-weekly
|
|
61
|
+
pip install -e ".[ai]"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## 使用
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# 生成本周周报(默认当前仓库)
|
|
68
|
+
git-weekly
|
|
69
|
+
|
|
70
|
+
# 指定时间范围
|
|
71
|
+
git-weekly --since "2025-02-24" --until "2025-02-28"
|
|
72
|
+
|
|
73
|
+
# 指定仓库路径
|
|
74
|
+
git-weekly --repo /path/to/your/project
|
|
75
|
+
|
|
76
|
+
# 多仓库聚合
|
|
77
|
+
git-weekly --repo ./project-a --repo ./project-b
|
|
78
|
+
|
|
79
|
+
# 输出为 Markdown 文件
|
|
80
|
+
git-weekly -o weekly-report.md
|
|
81
|
+
|
|
82
|
+
# 指定作者(默认为 git config 中的用户)
|
|
83
|
+
git-weekly --author "your-name"
|
|
84
|
+
|
|
85
|
+
# AI 智能总结(需要安装 git-weekly[ai])
|
|
86
|
+
git-weekly --ai
|
|
87
|
+
git-weekly --ai --api-key sk-xxx
|
|
88
|
+
git-weekly --ai --base-url https://api.deepseek.com/v1 --model deepseek-chat
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## 输出示例
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
──────────────────────────────────────────────────
|
|
95
|
+
📋 周报 (02/24 - 02/28)
|
|
96
|
+
──────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
本周工作
|
|
99
|
+
|
|
100
|
+
🚀 新功能
|
|
101
|
+
• 用户登录模块:实现 JWT 鉴权
|
|
102
|
+
• 添加文件上传接口,支持图片和 PDF
|
|
103
|
+
|
|
104
|
+
🐛 Bug 修复
|
|
105
|
+
• 修复分页查询在最后一页返回空数据的问题
|
|
106
|
+
• 修复并发请求下 session 丢失
|
|
107
|
+
|
|
108
|
+
♻️ 重构
|
|
109
|
+
• 将数据库操作抽取为 Repository 层
|
|
110
|
+
|
|
111
|
+
📝 文档
|
|
112
|
+
• 更新 API 接口文档
|
|
113
|
+
|
|
114
|
+
代码统计
|
|
115
|
+
|
|
116
|
+
提交次数 12 次
|
|
117
|
+
文件变更 23 个文件
|
|
118
|
+
新增行数 +847
|
|
119
|
+
删除行数 -312
|
|
120
|
+
|
|
121
|
+
🤖 AI 总结
|
|
122
|
+
|
|
123
|
+
本周主要完成了用户登录模块的 JWT 鉴权实现和文件上传接口开发,
|
|
124
|
+
同时修复了分页查询和并发 session 相关的两个稳定性问题。
|
|
125
|
+
技术层面对数据库操作进行了 Repository 层抽取,提升了代码可维护性。
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## AI 配置
|
|
129
|
+
|
|
130
|
+
API Key 支持三种配置方式(优先级从高到低):
|
|
131
|
+
|
|
132
|
+
**1. 命令行参数**
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
git-weekly --ai --api-key sk-xxx
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**2. 环境变量**
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
export GIT_WEEKLY_API_KEY=sk-xxx
|
|
142
|
+
export GIT_WEEKLY_BASE_URL=https://api.deepseek.com/v1 # 可选
|
|
143
|
+
export GIT_WEEKLY_MODEL=deepseek-chat # 可选
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**3. 配置文件** `~/.config/git-weekly/config.toml`
|
|
147
|
+
|
|
148
|
+
```toml
|
|
149
|
+
[ai]
|
|
150
|
+
api_key = "sk-xxx"
|
|
151
|
+
base_url = "https://api.openai.com/v1"
|
|
152
|
+
model = "gpt-4o-mini"
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### 支持的 LLM 服务
|
|
156
|
+
|
|
157
|
+
任何兼容 OpenAI API 的服务都可以使用,例如:
|
|
158
|
+
|
|
159
|
+
| 服务 | base_url | 推荐模型 |
|
|
160
|
+
|------|----------|----------|
|
|
161
|
+
| OpenAI | `https://api.openai.com/v1` (默认) | `gpt-4o-mini` |
|
|
162
|
+
| DeepSeek | `https://api.deepseek.com/v1` | `deepseek-chat` |
|
|
163
|
+
| 通义千问 | `https://dashscope.aliyuncs.com/compatible-mode/v1` | `qwen-plus` |
|
|
164
|
+
| Ollama (本地) | `http://localhost:11434/v1` | `llama3` |
|
|
165
|
+
|
|
166
|
+
## 开发
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
# 安装开发依赖
|
|
170
|
+
pip install -e ".[dev]"
|
|
171
|
+
|
|
172
|
+
# 运行测试
|
|
173
|
+
pytest
|
|
174
|
+
|
|
175
|
+
# 代码检查
|
|
176
|
+
ruff check .
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## License
|
|
180
|
+
|
|
181
|
+
MIT
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# git-weekly
|
|
2
|
+
|
|
3
|
+
从 Git 提交记录自动生成周报,支持 AI 智能总结。告别"我这周干了啥"的灵魂拷问。
|
|
4
|
+
|
|
5
|
+
## 功能
|
|
6
|
+
|
|
7
|
+
- 扫描指定时间范围内的所有 Git 提交
|
|
8
|
+
- 自动分析 diff,归类为:新功能、Bug 修复、重构、文档、测试等
|
|
9
|
+
- 生成结构化周报(Markdown / 终端输出)
|
|
10
|
+
- 支持多仓库聚合
|
|
11
|
+
- 代码统计(新增 / 删除 / 修改的文件数)
|
|
12
|
+
- **AI 智能总结**:接入 OpenAI 兼容 API,自动生成连贯的工作总结
|
|
13
|
+
|
|
14
|
+
## 安装
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install git-weekly
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
如需 AI 总结功能:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install "git-weekly[ai]"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
或从源码安装:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
git clone https://github.com/woaichilajiaochaorou/git-weekly.git
|
|
30
|
+
cd git-weekly
|
|
31
|
+
pip install -e ".[ai]"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## 使用
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# 生成本周周报(默认当前仓库)
|
|
38
|
+
git-weekly
|
|
39
|
+
|
|
40
|
+
# 指定时间范围
|
|
41
|
+
git-weekly --since "2025-02-24" --until "2025-02-28"
|
|
42
|
+
|
|
43
|
+
# 指定仓库路径
|
|
44
|
+
git-weekly --repo /path/to/your/project
|
|
45
|
+
|
|
46
|
+
# 多仓库聚合
|
|
47
|
+
git-weekly --repo ./project-a --repo ./project-b
|
|
48
|
+
|
|
49
|
+
# 输出为 Markdown 文件
|
|
50
|
+
git-weekly -o weekly-report.md
|
|
51
|
+
|
|
52
|
+
# 指定作者(默认为 git config 中的用户)
|
|
53
|
+
git-weekly --author "your-name"
|
|
54
|
+
|
|
55
|
+
# AI 智能总结(需要安装 git-weekly[ai])
|
|
56
|
+
git-weekly --ai
|
|
57
|
+
git-weekly --ai --api-key sk-xxx
|
|
58
|
+
git-weekly --ai --base-url https://api.deepseek.com/v1 --model deepseek-chat
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## 输出示例
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
──────────────────────────────────────────────────
|
|
65
|
+
📋 周报 (02/24 - 02/28)
|
|
66
|
+
──────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
本周工作
|
|
69
|
+
|
|
70
|
+
🚀 新功能
|
|
71
|
+
• 用户登录模块:实现 JWT 鉴权
|
|
72
|
+
• 添加文件上传接口,支持图片和 PDF
|
|
73
|
+
|
|
74
|
+
🐛 Bug 修复
|
|
75
|
+
• 修复分页查询在最后一页返回空数据的问题
|
|
76
|
+
• 修复并发请求下 session 丢失
|
|
77
|
+
|
|
78
|
+
♻️ 重构
|
|
79
|
+
• 将数据库操作抽取为 Repository 层
|
|
80
|
+
|
|
81
|
+
📝 文档
|
|
82
|
+
• 更新 API 接口文档
|
|
83
|
+
|
|
84
|
+
代码统计
|
|
85
|
+
|
|
86
|
+
提交次数 12 次
|
|
87
|
+
文件变更 23 个文件
|
|
88
|
+
新增行数 +847
|
|
89
|
+
删除行数 -312
|
|
90
|
+
|
|
91
|
+
🤖 AI 总结
|
|
92
|
+
|
|
93
|
+
本周主要完成了用户登录模块的 JWT 鉴权实现和文件上传接口开发,
|
|
94
|
+
同时修复了分页查询和并发 session 相关的两个稳定性问题。
|
|
95
|
+
技术层面对数据库操作进行了 Repository 层抽取,提升了代码可维护性。
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## AI 配置
|
|
99
|
+
|
|
100
|
+
API Key 支持三种配置方式(优先级从高到低):
|
|
101
|
+
|
|
102
|
+
**1. 命令行参数**
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
git-weekly --ai --api-key sk-xxx
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**2. 环境变量**
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
export GIT_WEEKLY_API_KEY=sk-xxx
|
|
112
|
+
export GIT_WEEKLY_BASE_URL=https://api.deepseek.com/v1 # 可选
|
|
113
|
+
export GIT_WEEKLY_MODEL=deepseek-chat # 可选
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**3. 配置文件** `~/.config/git-weekly/config.toml`
|
|
117
|
+
|
|
118
|
+
```toml
|
|
119
|
+
[ai]
|
|
120
|
+
api_key = "sk-xxx"
|
|
121
|
+
base_url = "https://api.openai.com/v1"
|
|
122
|
+
model = "gpt-4o-mini"
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### 支持的 LLM 服务
|
|
126
|
+
|
|
127
|
+
任何兼容 OpenAI API 的服务都可以使用,例如:
|
|
128
|
+
|
|
129
|
+
| 服务 | base_url | 推荐模型 |
|
|
130
|
+
|------|----------|----------|
|
|
131
|
+
| OpenAI | `https://api.openai.com/v1` (默认) | `gpt-4o-mini` |
|
|
132
|
+
| DeepSeek | `https://api.deepseek.com/v1` | `deepseek-chat` |
|
|
133
|
+
| 通义千问 | `https://dashscope.aliyuncs.com/compatible-mode/v1` | `qwen-plus` |
|
|
134
|
+
| Ollama (本地) | `http://localhost:11434/v1` | `llama3` |
|
|
135
|
+
|
|
136
|
+
## 开发
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
# 安装开发依赖
|
|
140
|
+
pip install -e ".[dev]"
|
|
141
|
+
|
|
142
|
+
# 运行测试
|
|
143
|
+
pytest
|
|
144
|
+
|
|
145
|
+
# 代码检查
|
|
146
|
+
ruff check .
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## License
|
|
150
|
+
|
|
151
|
+
MIT
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""Git log parsing and diff analysis."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import subprocess
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from datetime import datetime, timedelta
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
COMMIT_SEPARATOR = "\x00"
|
|
12
|
+
COMMIT_FORMAT = "%H%x00%an%x00%aI%x00%s"
|
|
13
|
+
|
|
14
|
+
CATEGORY_PATTERNS: dict[str, dict[str, object]] = {
|
|
15
|
+
"feat": {
|
|
16
|
+
"keywords": ["add", "feat", "feature", "new", "implement", "support", "create"],
|
|
17
|
+
"label": "\U0001f680 新功能",
|
|
18
|
+
},
|
|
19
|
+
"fix": {
|
|
20
|
+
"keywords": ["fix", "bug", "patch", "resolve", "close", "repair", "correct"],
|
|
21
|
+
"label": "\U0001f41b Bug 修复",
|
|
22
|
+
},
|
|
23
|
+
"refactor": {
|
|
24
|
+
"keywords": ["refactor", "restructure", "reorganize", "clean", "simplify", "extract",
|
|
25
|
+
"move", "rename", "optimize"],
|
|
26
|
+
"label": "\u267b\ufe0f 重构",
|
|
27
|
+
},
|
|
28
|
+
"docs": {
|
|
29
|
+
"keywords": ["doc", "readme", "comment", "changelog", "license"],
|
|
30
|
+
"label": "\U0001f4dd 文档",
|
|
31
|
+
},
|
|
32
|
+
"test": {
|
|
33
|
+
"keywords": ["test", "spec", "coverage", "mock", "assert"],
|
|
34
|
+
"label": "\U0001f9ea 测试",
|
|
35
|
+
},
|
|
36
|
+
"chore": {
|
|
37
|
+
"keywords": ["chore", "ci", "cd", "build", "deploy", "config", "deps", "bump",
|
|
38
|
+
"upgrade", "update dep", "docker", "makefile", "lint"],
|
|
39
|
+
"label": "\U0001f527 工程化",
|
|
40
|
+
},
|
|
41
|
+
"style": {
|
|
42
|
+
"keywords": ["style", "format", "indent", "whitespace", "prettier", "eslint"],
|
|
43
|
+
"label": "\U0001f3a8 代码风格",
|
|
44
|
+
},
|
|
45
|
+
"other": {
|
|
46
|
+
"keywords": [],
|
|
47
|
+
"label": "\U0001f4e6 其他",
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
CATEGORY_ORDER: list[str] = ["feat", "fix", "refactor", "docs", "test", "chore", "style", "other"]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class CommitInfo:
|
|
56
|
+
hash: str
|
|
57
|
+
author: str
|
|
58
|
+
date: datetime
|
|
59
|
+
message: str
|
|
60
|
+
files_changed: int = 0
|
|
61
|
+
insertions: int = 0
|
|
62
|
+
deletions: int = 0
|
|
63
|
+
files: list[str] = field(default_factory=list)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class RepoStats:
|
|
68
|
+
repo_path: str
|
|
69
|
+
repo_name: str
|
|
70
|
+
commits: list[CommitInfo] = field(default_factory=list)
|
|
71
|
+
total_files_changed: int = 0
|
|
72
|
+
total_insertions: int = 0
|
|
73
|
+
total_deletions: int = 0
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def total_commits(self) -> int:
|
|
77
|
+
return len(self.commits)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _run_git(args: list[str], cwd: str) -> str:
|
|
81
|
+
result = subprocess.run(
|
|
82
|
+
["git"] + args,
|
|
83
|
+
cwd=cwd,
|
|
84
|
+
capture_output=True,
|
|
85
|
+
text=True,
|
|
86
|
+
)
|
|
87
|
+
if result.returncode != 0:
|
|
88
|
+
raise RuntimeError(f"git command failed: git {' '.join(args)}\n{result.stderr.strip()}")
|
|
89
|
+
return result.stdout.strip()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def get_git_user(repo_path: str) -> str:
|
|
93
|
+
try:
|
|
94
|
+
return _run_git(["config", "user.name"], cwd=repo_path)
|
|
95
|
+
except RuntimeError:
|
|
96
|
+
return ""
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def get_repo_name(repo_path: str) -> str:
|
|
100
|
+
path = Path(repo_path).resolve()
|
|
101
|
+
try:
|
|
102
|
+
remote = _run_git(["remote", "get-url", "origin"], cwd=repo_path)
|
|
103
|
+
name = remote.rstrip("/").split("/")[-1]
|
|
104
|
+
return name.removesuffix(".git")
|
|
105
|
+
except RuntimeError:
|
|
106
|
+
return path.name
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def parse_commits(
|
|
110
|
+
repo_path: str,
|
|
111
|
+
since: str,
|
|
112
|
+
until: str,
|
|
113
|
+
author: str | None = None,
|
|
114
|
+
) -> RepoStats:
|
|
115
|
+
"""Parse git log and return structured commit data."""
|
|
116
|
+
repo_path = str(Path(repo_path).resolve())
|
|
117
|
+
repo_name = get_repo_name(repo_path)
|
|
118
|
+
|
|
119
|
+
git_args = [
|
|
120
|
+
"log",
|
|
121
|
+
f"--since={since}",
|
|
122
|
+
f"--until={until}",
|
|
123
|
+
f"--format={COMMIT_FORMAT}",
|
|
124
|
+
"--numstat",
|
|
125
|
+
]
|
|
126
|
+
if author:
|
|
127
|
+
git_args.append(f"--author={author}")
|
|
128
|
+
|
|
129
|
+
raw = _run_git(git_args, cwd=repo_path)
|
|
130
|
+
if not raw:
|
|
131
|
+
return RepoStats(repo_path=repo_path, repo_name=repo_name)
|
|
132
|
+
|
|
133
|
+
stats = RepoStats(repo_path=repo_path, repo_name=repo_name)
|
|
134
|
+
commits: list[CommitInfo] = []
|
|
135
|
+
current_commit: CommitInfo | None = None
|
|
136
|
+
|
|
137
|
+
for line in raw.split("\n"):
|
|
138
|
+
if not line:
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
if COMMIT_SEPARATOR in line:
|
|
142
|
+
parts = line.split(COMMIT_SEPARATOR, 3)
|
|
143
|
+
if len(parts) == 4 and len(parts[0]) == 40:
|
|
144
|
+
if current_commit:
|
|
145
|
+
commits.append(current_commit)
|
|
146
|
+
current_commit = CommitInfo(
|
|
147
|
+
hash=parts[0],
|
|
148
|
+
author=parts[1],
|
|
149
|
+
date=datetime.fromisoformat(parts[2]),
|
|
150
|
+
message=parts[3],
|
|
151
|
+
)
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
if current_commit and "\t" in line:
|
|
155
|
+
parts = line.split("\t")
|
|
156
|
+
if len(parts) == 3:
|
|
157
|
+
added, deleted, filepath = parts
|
|
158
|
+
try:
|
|
159
|
+
ins = int(added) if added != "-" else 0
|
|
160
|
+
dels = int(deleted) if deleted != "-" else 0
|
|
161
|
+
current_commit.insertions += ins
|
|
162
|
+
current_commit.deletions += dels
|
|
163
|
+
current_commit.files_changed += 1
|
|
164
|
+
current_commit.files.append(filepath)
|
|
165
|
+
except ValueError:
|
|
166
|
+
pass
|
|
167
|
+
|
|
168
|
+
if current_commit:
|
|
169
|
+
commits.append(current_commit)
|
|
170
|
+
|
|
171
|
+
stats.commits = commits
|
|
172
|
+
stats.total_files_changed = len({f for c in commits for f in c.files})
|
|
173
|
+
stats.total_insertions = sum(c.insertions for c in commits)
|
|
174
|
+
stats.total_deletions = sum(c.deletions for c in commits)
|
|
175
|
+
|
|
176
|
+
return stats
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def categorize_commit(commit: CommitInfo) -> str:
|
|
180
|
+
"""Categorize a commit based on its message using keyword matching."""
|
|
181
|
+
msg = commit.message.lower()
|
|
182
|
+
|
|
183
|
+
prefix_match = re.match(r"^(\w+)[\(:]", msg)
|
|
184
|
+
if prefix_match:
|
|
185
|
+
prefix = prefix_match.group(1)
|
|
186
|
+
for cat_key in CATEGORY_PATTERNS:
|
|
187
|
+
if cat_key == "other":
|
|
188
|
+
continue
|
|
189
|
+
if prefix == cat_key or prefix in CATEGORY_PATTERNS[cat_key]["keywords"]:
|
|
190
|
+
return cat_key
|
|
191
|
+
|
|
192
|
+
extensions = {Path(f).suffix for f in commit.files}
|
|
193
|
+
if extensions & {".md", ".rst", ".txt"} and not (extensions - {".md", ".rst", ".txt"}):
|
|
194
|
+
return "docs"
|
|
195
|
+
if commit.files and all("test" in f.lower() or "spec" in f.lower() for f in commit.files):
|
|
196
|
+
return "test"
|
|
197
|
+
|
|
198
|
+
for cat_key, cat_info in CATEGORY_PATTERNS.items():
|
|
199
|
+
if cat_key == "other":
|
|
200
|
+
continue
|
|
201
|
+
for keyword in cat_info["keywords"]:
|
|
202
|
+
if keyword in msg:
|
|
203
|
+
return cat_key
|
|
204
|
+
|
|
205
|
+
return "other"
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def get_default_since() -> str:
|
|
209
|
+
"""Return the Monday of the current week as YYYY-MM-DD."""
|
|
210
|
+
today = datetime.now()
|
|
211
|
+
monday = today - timedelta(days=today.weekday())
|
|
212
|
+
return monday.strftime("%Y-%m-%d")
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def get_default_until() -> str:
|
|
216
|
+
"""Return tomorrow as YYYY-MM-DD to include today's commits."""
|
|
217
|
+
tomorrow = datetime.now() + timedelta(days=1)
|
|
218
|
+
return tomorrow.strftime("%Y-%m-%d")
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Command-line interface for git-weekly."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .analyzer import get_default_since, get_default_until, get_git_user, parse_commits
|
|
10
|
+
from .report import build_report, render_markdown, render_terminal
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main():
|
|
14
|
+
"""Generate weekly report from Git commit history."""
|
|
15
|
+
parser = argparse.ArgumentParser(
|
|
16
|
+
description="Generate weekly report from Git commit history.",
|
|
17
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
18
|
+
epilog="""\
|
|
19
|
+
examples:
|
|
20
|
+
git-weekly # 当前仓库本周周报
|
|
21
|
+
git-weekly --since 2025-02-24 --until 2025-02-28
|
|
22
|
+
git-weekly --repo /path/to/project
|
|
23
|
+
git-weekly --repo ./proj-a --repo ./proj-b # 多仓库聚合
|
|
24
|
+
git-weekly -o report.md # 输出 Markdown 文件
|
|
25
|
+
git-weekly --all-authors # 包含所有人的提交
|
|
26
|
+
git-weekly --ai # AI 智能总结
|
|
27
|
+
git-weekly --ai --base-url https://api.deepseek.com/v1 # 用 DeepSeek
|
|
28
|
+
""",
|
|
29
|
+
)
|
|
30
|
+
parser.add_argument(
|
|
31
|
+
"--repo", "-r", action="append", default=None,
|
|
32
|
+
help="Git repository path (can specify multiple times, default: current directory)",
|
|
33
|
+
)
|
|
34
|
+
parser.add_argument(
|
|
35
|
+
"--since", "-s", default=None,
|
|
36
|
+
help="Start date YYYY-MM-DD (default: Monday of current week)",
|
|
37
|
+
)
|
|
38
|
+
parser.add_argument(
|
|
39
|
+
"--until", "-u", default=None,
|
|
40
|
+
help="End date YYYY-MM-DD (default: today)",
|
|
41
|
+
)
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"--author", "-a", default=None,
|
|
44
|
+
help="Filter by author name (default: git config user.name)",
|
|
45
|
+
)
|
|
46
|
+
parser.add_argument(
|
|
47
|
+
"--output", "-o", default=None,
|
|
48
|
+
help="Output to Markdown file instead of terminal",
|
|
49
|
+
)
|
|
50
|
+
parser.add_argument(
|
|
51
|
+
"--all-authors", action="store_true", default=False,
|
|
52
|
+
help="Include commits from all authors",
|
|
53
|
+
)
|
|
54
|
+
parser.add_argument(
|
|
55
|
+
"--ai", action="store_true", default=False,
|
|
56
|
+
help="Enable AI-powered summary (requires: pip install git-weekly[ai])",
|
|
57
|
+
)
|
|
58
|
+
parser.add_argument(
|
|
59
|
+
"--api-key", default=None,
|
|
60
|
+
help="LLM API key (or set GIT_WEEKLY_API_KEY env var)",
|
|
61
|
+
)
|
|
62
|
+
parser.add_argument(
|
|
63
|
+
"--base-url", default=None,
|
|
64
|
+
help="LLM API base URL (default: OpenAI; set for DeepSeek/Ollama/etc.)",
|
|
65
|
+
)
|
|
66
|
+
parser.add_argument(
|
|
67
|
+
"--model", default=None,
|
|
68
|
+
help="LLM model name (default: gpt-4o-mini)",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
args = parser.parse_args()
|
|
72
|
+
|
|
73
|
+
repos = args.repo or ["."]
|
|
74
|
+
since = args.since or get_default_since()
|
|
75
|
+
until = args.until or get_default_until()
|
|
76
|
+
author = args.author
|
|
77
|
+
|
|
78
|
+
if not author and not args.all_authors:
|
|
79
|
+
first_repo = str(Path(repos[0]).resolve())
|
|
80
|
+
author = get_git_user(first_repo)
|
|
81
|
+
if not author:
|
|
82
|
+
print("Warning: could not detect git user, showing all authors", file=sys.stderr)
|
|
83
|
+
author = None
|
|
84
|
+
|
|
85
|
+
reports = []
|
|
86
|
+
for repo_path in repos:
|
|
87
|
+
resolved = str(Path(repo_path).resolve())
|
|
88
|
+
if not Path(resolved, ".git").exists():
|
|
89
|
+
print(f"Error: {repo_path} is not a git repository", file=sys.stderr)
|
|
90
|
+
sys.exit(1)
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
stats = parse_commits(resolved, since, until, author if not args.all_authors else None)
|
|
94
|
+
report = build_report(stats, since, until)
|
|
95
|
+
reports.append(report)
|
|
96
|
+
except RuntimeError as e:
|
|
97
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
98
|
+
sys.exit(1)
|
|
99
|
+
|
|
100
|
+
if not reports:
|
|
101
|
+
print("No repositories to analyze.", file=sys.stderr)
|
|
102
|
+
sys.exit(0)
|
|
103
|
+
|
|
104
|
+
if args.ai:
|
|
105
|
+
try:
|
|
106
|
+
from .llm import generate_summary, load_config
|
|
107
|
+
|
|
108
|
+
llm_cfg = load_config(
|
|
109
|
+
api_key=args.api_key,
|
|
110
|
+
base_url=args.base_url,
|
|
111
|
+
model=args.model,
|
|
112
|
+
)
|
|
113
|
+
print("Generating AI summary...", file=sys.stderr)
|
|
114
|
+
summary = generate_summary(reports, llm_cfg)
|
|
115
|
+
for report in reports:
|
|
116
|
+
report.ai_summary = summary
|
|
117
|
+
except RuntimeError as e:
|
|
118
|
+
print(f"AI summary failed: {e}", file=sys.stderr)
|
|
119
|
+
|
|
120
|
+
if args.output:
|
|
121
|
+
md = render_markdown(reports)
|
|
122
|
+
Path(args.output).write_text(md, encoding="utf-8")
|
|
123
|
+
print(f"Report saved to {args.output}")
|
|
124
|
+
else:
|
|
125
|
+
render_terminal(reports)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
if __name__ == "__main__":
|
|
129
|
+
main()
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""LLM-powered weekly report summarization (OpenAI-compatible API)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .report import CategorizedReport
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
import tomllib # Python 3.11+
|
|
15
|
+
except ModuleNotFoundError:
|
|
16
|
+
try:
|
|
17
|
+
import tomli as tomllib # type: ignore[no-redef]
|
|
18
|
+
except ModuleNotFoundError:
|
|
19
|
+
tomllib = None # type: ignore[assignment]
|
|
20
|
+
|
|
21
|
+
CONFIG_DIR = Path.home() / ".config" / "git-weekly"
|
|
22
|
+
CONFIG_FILE = CONFIG_DIR / "config.toml"
|
|
23
|
+
|
|
24
|
+
DEFAULT_BASE_URL = "https://api.openai.com/v1"
|
|
25
|
+
DEFAULT_MODEL = "gpt-4o-mini"
|
|
26
|
+
|
|
27
|
+
SYSTEM_PROMPT = """\
|
|
28
|
+
你是一位专业的技术周报撰写助手。根据提供的 Git commit 数据,写一段 3-5 句话的工作总结。
|
|
29
|
+
|
|
30
|
+
要求:
|
|
31
|
+
- 用中文输出
|
|
32
|
+
- 像开发者写给团队看的周报,语气专业但不生硬
|
|
33
|
+
- 突出本周重点工作和技术亮点,不要逐条罗列 commit
|
|
34
|
+
- 如果有 bug fix,简要提及稳定性改进
|
|
35
|
+
- 如果有重构,说明技术改进的意义
|
|
36
|
+
- 不要编造不存在的内容,只基于提供的数据总结
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class LLMConfig:
|
|
42
|
+
api_key: str
|
|
43
|
+
base_url: str = DEFAULT_BASE_URL
|
|
44
|
+
model: str = DEFAULT_MODEL
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _load_config_file() -> dict[str, str]:
|
|
48
|
+
"""Load config from ~/.config/git-weekly/config.toml if it exists."""
|
|
49
|
+
if tomllib is None or not CONFIG_FILE.exists():
|
|
50
|
+
return {}
|
|
51
|
+
try:
|
|
52
|
+
with open(CONFIG_FILE, "rb") as f:
|
|
53
|
+
data = tomllib.load(f)
|
|
54
|
+
return data.get("ai", {})
|
|
55
|
+
except Exception:
|
|
56
|
+
return {}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def load_config(
|
|
60
|
+
*,
|
|
61
|
+
api_key: str | None = None,
|
|
62
|
+
base_url: str | None = None,
|
|
63
|
+
model: str | None = None,
|
|
64
|
+
) -> LLMConfig:
|
|
65
|
+
"""Build LLM config from CLI args > env vars > config file."""
|
|
66
|
+
file_cfg = _load_config_file()
|
|
67
|
+
|
|
68
|
+
resolved_key = (
|
|
69
|
+
api_key
|
|
70
|
+
or os.environ.get("GIT_WEEKLY_API_KEY")
|
|
71
|
+
or file_cfg.get("api_key", "")
|
|
72
|
+
)
|
|
73
|
+
if not resolved_key:
|
|
74
|
+
raise RuntimeError(
|
|
75
|
+
"未找到 API Key。请通过以下任一方式配置:\n"
|
|
76
|
+
" 1. --api-key 参数\n"
|
|
77
|
+
" 2. 环境变量 GIT_WEEKLY_API_KEY\n"
|
|
78
|
+
f" 3. 配置文件 {CONFIG_FILE}\n\n"
|
|
79
|
+
"配置文件示例:\n"
|
|
80
|
+
" [ai]\n"
|
|
81
|
+
' api_key = "sk-xxx"\n'
|
|
82
|
+
' base_url = "https://api.openai.com/v1"\n'
|
|
83
|
+
' model = "gpt-4o-mini"'
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
resolved_url = (
|
|
87
|
+
base_url
|
|
88
|
+
or os.environ.get("GIT_WEEKLY_BASE_URL")
|
|
89
|
+
or file_cfg.get("base_url", "")
|
|
90
|
+
or DEFAULT_BASE_URL
|
|
91
|
+
)
|
|
92
|
+
resolved_model = (
|
|
93
|
+
model
|
|
94
|
+
or os.environ.get("GIT_WEEKLY_MODEL")
|
|
95
|
+
or file_cfg.get("model", "")
|
|
96
|
+
or DEFAULT_MODEL
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
return LLMConfig(api_key=resolved_key, base_url=resolved_url, model=resolved_model)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _build_prompt(reports: list[CategorizedReport]) -> str:
|
|
103
|
+
"""Serialize reports into structured text for the LLM."""
|
|
104
|
+
from .analyzer import CATEGORY_PATTERNS
|
|
105
|
+
|
|
106
|
+
parts: list[str] = []
|
|
107
|
+
for report in reports:
|
|
108
|
+
parts.append(f"仓库: {report.stats.repo_name}")
|
|
109
|
+
parts.append(f"时间范围: {report.since} ~ {report.until}")
|
|
110
|
+
parts.append(f"提交次数: {report.stats.total_commits}")
|
|
111
|
+
parts.append(f"文件变更: {report.stats.total_files_changed} 个文件")
|
|
112
|
+
parts.append(f"新增: +{report.stats.total_insertions} 行, 删除: -{report.stats.total_deletions} 行")
|
|
113
|
+
parts.append("")
|
|
114
|
+
|
|
115
|
+
for cat_key, commits in report.categories.items():
|
|
116
|
+
label = CATEGORY_PATTERNS.get(cat_key, {}).get("label", cat_key)
|
|
117
|
+
parts.append(f"【{label}】")
|
|
118
|
+
for c in commits:
|
|
119
|
+
file_hint = ""
|
|
120
|
+
if c.files:
|
|
121
|
+
top_files = c.files[:3]
|
|
122
|
+
file_hint = f" ({', '.join(top_files)})"
|
|
123
|
+
parts.append(f" - {c.message}{file_hint}")
|
|
124
|
+
parts.append("")
|
|
125
|
+
|
|
126
|
+
return "\n".join(parts)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def generate_summary(reports: list[CategorizedReport], config: LLMConfig) -> str:
|
|
130
|
+
"""Call OpenAI-compatible API to generate a narrative summary."""
|
|
131
|
+
try:
|
|
132
|
+
from openai import OpenAI
|
|
133
|
+
except ImportError:
|
|
134
|
+
raise RuntimeError(
|
|
135
|
+
"AI 功能需要安装 openai 库。请运行:\n"
|
|
136
|
+
' pip install "git-weekly[ai]"'
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
client = OpenAI(api_key=config.api_key, base_url=config.base_url)
|
|
140
|
+
|
|
141
|
+
user_content = _build_prompt(reports)
|
|
142
|
+
|
|
143
|
+
response = client.chat.completions.create(
|
|
144
|
+
model=config.model,
|
|
145
|
+
messages=[
|
|
146
|
+
{"role": "system", "content": SYSTEM_PROMPT},
|
|
147
|
+
{"role": "user", "content": user_content},
|
|
148
|
+
],
|
|
149
|
+
temperature=0.3,
|
|
150
|
+
max_tokens=1024,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
return response.choices[0].message.content or ""
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Report generation and formatting."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
from .analyzer import CATEGORY_ORDER, CATEGORY_PATTERNS, CommitInfo, RepoStats, categorize_commit
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class CategorizedReport:
|
|
14
|
+
"""Commits grouped by category with summary info."""
|
|
15
|
+
categories: dict[str, list[CommitInfo]]
|
|
16
|
+
stats: RepoStats
|
|
17
|
+
since: str
|
|
18
|
+
until: str
|
|
19
|
+
ai_summary: str | None = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def build_report(stats: RepoStats, since: str, until: str) -> CategorizedReport:
|
|
23
|
+
"""Categorize commits and build a structured report."""
|
|
24
|
+
categories: dict[str, list[CommitInfo]] = defaultdict(list)
|
|
25
|
+
for commit in stats.commits:
|
|
26
|
+
cat = categorize_commit(commit)
|
|
27
|
+
categories[cat].append(commit)
|
|
28
|
+
return CategorizedReport(
|
|
29
|
+
categories=dict(categories),
|
|
30
|
+
stats=stats,
|
|
31
|
+
since=since,
|
|
32
|
+
until=until,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _format_date_range(since: str, until: str) -> str:
|
|
37
|
+
try:
|
|
38
|
+
s = datetime.strptime(since, "%Y-%m-%d")
|
|
39
|
+
u = datetime.strptime(until, "%Y-%m-%d")
|
|
40
|
+
return f"{s.month:02d}/{s.day:02d} - {u.month:02d}/{u.day:02d}"
|
|
41
|
+
except ValueError:
|
|
42
|
+
return f"{since} ~ {until}"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def render_terminal(reports: list[CategorizedReport]) -> None:
|
|
46
|
+
"""Render reports to terminal with ANSI colors."""
|
|
47
|
+
BOLD = "\033[1m"
|
|
48
|
+
CYAN = "\033[36m"
|
|
49
|
+
GREEN = "\033[32m"
|
|
50
|
+
RED = "\033[31m"
|
|
51
|
+
DIM = "\033[2m"
|
|
52
|
+
RESET = "\033[0m"
|
|
53
|
+
|
|
54
|
+
for report in reports:
|
|
55
|
+
date_range = _format_date_range(report.since, report.until)
|
|
56
|
+
title = f" \U0001f4cb 周报 ({date_range})"
|
|
57
|
+
if len(reports) > 1:
|
|
58
|
+
title += f" — {report.stats.repo_name}"
|
|
59
|
+
|
|
60
|
+
width = 50
|
|
61
|
+
print()
|
|
62
|
+
print(f"{CYAN}{'─' * width}{RESET}")
|
|
63
|
+
print(f"{BOLD}{CYAN}{title}{RESET}")
|
|
64
|
+
print(f"{CYAN}{'─' * width}{RESET}")
|
|
65
|
+
|
|
66
|
+
if not report.stats.commits:
|
|
67
|
+
print(f"\n {DIM}这段时间没有提交记录{RESET}\n")
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
print(f"\n{BOLD}本周工作{RESET}\n")
|
|
71
|
+
|
|
72
|
+
for cat_key in CATEGORY_ORDER:
|
|
73
|
+
if cat_key not in report.categories:
|
|
74
|
+
continue
|
|
75
|
+
commits = report.categories[cat_key]
|
|
76
|
+
label = CATEGORY_PATTERNS[cat_key]["label"]
|
|
77
|
+
print(f" {BOLD}{label}{RESET}")
|
|
78
|
+
for commit in commits:
|
|
79
|
+
msg = commit.message
|
|
80
|
+
if len(msg) > 72:
|
|
81
|
+
msg = msg[:72] + "..."
|
|
82
|
+
print(f" • {msg}")
|
|
83
|
+
print()
|
|
84
|
+
|
|
85
|
+
print(f"{BOLD}代码统计{RESET}\n")
|
|
86
|
+
print(f" 提交次数 {BOLD}{report.stats.total_commits}{RESET} 次")
|
|
87
|
+
print(f" 文件变更 {BOLD}{report.stats.total_files_changed}{RESET} 个文件")
|
|
88
|
+
print(f" 新增行数 {GREEN}+{report.stats.total_insertions}{RESET}")
|
|
89
|
+
print(f" 删除行数 {RED}-{report.stats.total_deletions}{RESET}")
|
|
90
|
+
print()
|
|
91
|
+
|
|
92
|
+
if report.ai_summary:
|
|
93
|
+
print(f"{BOLD}\U0001f916 AI \u603b\u7ed3{RESET}\n")
|
|
94
|
+
for line in report.ai_summary.strip().splitlines():
|
|
95
|
+
print(f" {line}")
|
|
96
|
+
print()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def render_markdown(reports: list[CategorizedReport]) -> str:
|
|
100
|
+
"""Render reports as Markdown string."""
|
|
101
|
+
lines: list[str] = []
|
|
102
|
+
|
|
103
|
+
for report in reports:
|
|
104
|
+
date_range = _format_date_range(report.since, report.until)
|
|
105
|
+
heading = f"周报 ({date_range})"
|
|
106
|
+
if len(reports) > 1:
|
|
107
|
+
heading += f" — {report.stats.repo_name}"
|
|
108
|
+
|
|
109
|
+
lines.append(f"# {heading}")
|
|
110
|
+
lines.append("")
|
|
111
|
+
|
|
112
|
+
if not report.stats.commits:
|
|
113
|
+
lines.append("_这段时间没有提交记录_")
|
|
114
|
+
lines.append("")
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
lines.append("## 本周工作")
|
|
118
|
+
lines.append("")
|
|
119
|
+
|
|
120
|
+
for cat_key in CATEGORY_ORDER:
|
|
121
|
+
if cat_key not in report.categories:
|
|
122
|
+
continue
|
|
123
|
+
commits = report.categories[cat_key]
|
|
124
|
+
label = CATEGORY_PATTERNS[cat_key]["label"]
|
|
125
|
+
lines.append(f"### {label}")
|
|
126
|
+
lines.append("")
|
|
127
|
+
for commit in commits:
|
|
128
|
+
lines.append(f"- {commit.message}")
|
|
129
|
+
lines.append("")
|
|
130
|
+
|
|
131
|
+
lines.append("## 代码统计")
|
|
132
|
+
lines.append("")
|
|
133
|
+
lines.append("| 指标 | 数据 |")
|
|
134
|
+
lines.append("|------|------|")
|
|
135
|
+
lines.append(f"| 提交次数 | {report.stats.total_commits} 次 |")
|
|
136
|
+
lines.append(f"| 文件变更 | {report.stats.total_files_changed} 个文件 |")
|
|
137
|
+
lines.append(f"| 新增行数 | +{report.stats.total_insertions} |")
|
|
138
|
+
lines.append(f"| 删除行数 | -{report.stats.total_deletions} |")
|
|
139
|
+
lines.append("")
|
|
140
|
+
|
|
141
|
+
if report.ai_summary:
|
|
142
|
+
lines.append("## \U0001f916 AI \u603b\u7ed3")
|
|
143
|
+
lines.append("")
|
|
144
|
+
lines.append(report.ai_summary.strip())
|
|
145
|
+
lines.append("")
|
|
146
|
+
|
|
147
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: git-weekly
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Generate weekly reports from Git commit history, with optional AI summary
|
|
5
|
+
Author: woaichilajiaochaorou
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/woaichilajiaochaorou/git-weekly
|
|
8
|
+
Project-URL: Repository, https://github.com/woaichilajiaochaorou/git-weekly
|
|
9
|
+
Project-URL: Issues, https://github.com/woaichilajiaochaorou/git-weekly/issues
|
|
10
|
+
Keywords: git,weekly,report,changelog,productivity,ai,llm
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Provides-Extra: ai
|
|
25
|
+
Requires-Dist: openai>=1.0; extra == "ai"
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
28
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# git-weekly
|
|
32
|
+
|
|
33
|
+
从 Git 提交记录自动生成周报,支持 AI 智能总结。告别"我这周干了啥"的灵魂拷问。
|
|
34
|
+
|
|
35
|
+
## 功能
|
|
36
|
+
|
|
37
|
+
- 扫描指定时间范围内的所有 Git 提交
|
|
38
|
+
- 自动分析 diff,归类为:新功能、Bug 修复、重构、文档、测试等
|
|
39
|
+
- 生成结构化周报(Markdown / 终端输出)
|
|
40
|
+
- 支持多仓库聚合
|
|
41
|
+
- 代码统计(新增 / 删除 / 修改的文件数)
|
|
42
|
+
- **AI 智能总结**:接入 OpenAI 兼容 API,自动生成连贯的工作总结
|
|
43
|
+
|
|
44
|
+
## 安装
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install git-weekly
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
如需 AI 总结功能:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install "git-weekly[ai]"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
或从源码安装:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
git clone https://github.com/woaichilajiaochaorou/git-weekly.git
|
|
60
|
+
cd git-weekly
|
|
61
|
+
pip install -e ".[ai]"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## 使用
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# 生成本周周报(默认当前仓库)
|
|
68
|
+
git-weekly
|
|
69
|
+
|
|
70
|
+
# 指定时间范围
|
|
71
|
+
git-weekly --since "2025-02-24" --until "2025-02-28"
|
|
72
|
+
|
|
73
|
+
# 指定仓库路径
|
|
74
|
+
git-weekly --repo /path/to/your/project
|
|
75
|
+
|
|
76
|
+
# 多仓库聚合
|
|
77
|
+
git-weekly --repo ./project-a --repo ./project-b
|
|
78
|
+
|
|
79
|
+
# 输出为 Markdown 文件
|
|
80
|
+
git-weekly -o weekly-report.md
|
|
81
|
+
|
|
82
|
+
# 指定作者(默认为 git config 中的用户)
|
|
83
|
+
git-weekly --author "your-name"
|
|
84
|
+
|
|
85
|
+
# AI 智能总结(需要安装 git-weekly[ai])
|
|
86
|
+
git-weekly --ai
|
|
87
|
+
git-weekly --ai --api-key sk-xxx
|
|
88
|
+
git-weekly --ai --base-url https://api.deepseek.com/v1 --model deepseek-chat
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## 输出示例
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
──────────────────────────────────────────────────
|
|
95
|
+
📋 周报 (02/24 - 02/28)
|
|
96
|
+
──────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
本周工作
|
|
99
|
+
|
|
100
|
+
🚀 新功能
|
|
101
|
+
• 用户登录模块:实现 JWT 鉴权
|
|
102
|
+
• 添加文件上传接口,支持图片和 PDF
|
|
103
|
+
|
|
104
|
+
🐛 Bug 修复
|
|
105
|
+
• 修复分页查询在最后一页返回空数据的问题
|
|
106
|
+
• 修复并发请求下 session 丢失
|
|
107
|
+
|
|
108
|
+
♻️ 重构
|
|
109
|
+
• 将数据库操作抽取为 Repository 层
|
|
110
|
+
|
|
111
|
+
📝 文档
|
|
112
|
+
• 更新 API 接口文档
|
|
113
|
+
|
|
114
|
+
代码统计
|
|
115
|
+
|
|
116
|
+
提交次数 12 次
|
|
117
|
+
文件变更 23 个文件
|
|
118
|
+
新增行数 +847
|
|
119
|
+
删除行数 -312
|
|
120
|
+
|
|
121
|
+
🤖 AI 总结
|
|
122
|
+
|
|
123
|
+
本周主要完成了用户登录模块的 JWT 鉴权实现和文件上传接口开发,
|
|
124
|
+
同时修复了分页查询和并发 session 相关的两个稳定性问题。
|
|
125
|
+
技术层面对数据库操作进行了 Repository 层抽取,提升了代码可维护性。
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## AI 配置
|
|
129
|
+
|
|
130
|
+
API Key 支持三种配置方式(优先级从高到低):
|
|
131
|
+
|
|
132
|
+
**1. 命令行参数**
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
git-weekly --ai --api-key sk-xxx
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**2. 环境变量**
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
export GIT_WEEKLY_API_KEY=sk-xxx
|
|
142
|
+
export GIT_WEEKLY_BASE_URL=https://api.deepseek.com/v1 # 可选
|
|
143
|
+
export GIT_WEEKLY_MODEL=deepseek-chat # 可选
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**3. 配置文件** `~/.config/git-weekly/config.toml`
|
|
147
|
+
|
|
148
|
+
```toml
|
|
149
|
+
[ai]
|
|
150
|
+
api_key = "sk-xxx"
|
|
151
|
+
base_url = "https://api.openai.com/v1"
|
|
152
|
+
model = "gpt-4o-mini"
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### 支持的 LLM 服务
|
|
156
|
+
|
|
157
|
+
任何兼容 OpenAI API 的服务都可以使用,例如:
|
|
158
|
+
|
|
159
|
+
| 服务 | base_url | 推荐模型 |
|
|
160
|
+
|------|----------|----------|
|
|
161
|
+
| OpenAI | `https://api.openai.com/v1` (默认) | `gpt-4o-mini` |
|
|
162
|
+
| DeepSeek | `https://api.deepseek.com/v1` | `deepseek-chat` |
|
|
163
|
+
| 通义千问 | `https://dashscope.aliyuncs.com/compatible-mode/v1` | `qwen-plus` |
|
|
164
|
+
| Ollama (本地) | `http://localhost:11434/v1` | `llama3` |
|
|
165
|
+
|
|
166
|
+
## 开发
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
# 安装开发依赖
|
|
170
|
+
pip install -e ".[dev]"
|
|
171
|
+
|
|
172
|
+
# 运行测试
|
|
173
|
+
pytest
|
|
174
|
+
|
|
175
|
+
# 代码检查
|
|
176
|
+
ruff check .
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## License
|
|
180
|
+
|
|
181
|
+
MIT
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
setup.py
|
|
5
|
+
git_weekly/__init__.py
|
|
6
|
+
git_weekly/__main__.py
|
|
7
|
+
git_weekly/analyzer.py
|
|
8
|
+
git_weekly/cli.py
|
|
9
|
+
git_weekly/llm.py
|
|
10
|
+
git_weekly/report.py
|
|
11
|
+
git_weekly.egg-info/PKG-INFO
|
|
12
|
+
git_weekly.egg-info/SOURCES.txt
|
|
13
|
+
git_weekly.egg-info/dependency_links.txt
|
|
14
|
+
git_weekly.egg-info/entry_points.txt
|
|
15
|
+
git_weekly.egg-info/requires.txt
|
|
16
|
+
git_weekly.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
git_weekly
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "git-weekly"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Generate weekly reports from Git commit history, with optional AI summary"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
dependencies = []
|
|
13
|
+
authors = [
|
|
14
|
+
{name = "woaichilajiaochaorou"},
|
|
15
|
+
]
|
|
16
|
+
keywords = ["git", "weekly", "report", "changelog", "productivity", "ai", "llm"]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 3 - Alpha",
|
|
19
|
+
"Environment :: Console",
|
|
20
|
+
"Intended Audience :: Developers",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Topic :: Software Development :: Version Control :: Git",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/woaichilajiaochaorou/git-weekly"
|
|
32
|
+
Repository = "https://github.com/woaichilajiaochaorou/git-weekly"
|
|
33
|
+
Issues = "https://github.com/woaichilajiaochaorou/git-weekly/issues"
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
ai = [
|
|
37
|
+
"openai>=1.0",
|
|
38
|
+
]
|
|
39
|
+
dev = [
|
|
40
|
+
"pytest>=7.0",
|
|
41
|
+
"ruff>=0.4",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[project.scripts]
|
|
45
|
+
git-weekly = "git_weekly.cli:main"
|
|
46
|
+
|
|
47
|
+
[tool.ruff]
|
|
48
|
+
line-length = 100
|
|
49
|
+
|
|
50
|
+
[tool.pytest.ini_options]
|
|
51
|
+
testpaths = ["tests"]
|