git-gcm 1.0.2__tar.gz → 2.0.1__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_gcm-1.0.2 → git_gcm-2.0.1}/PKG-INFO +29 -13
- git_gcm-2.0.1/README.md +64 -0
- {git_gcm-1.0.2 → git_gcm-2.0.1}/gcm/__init__.py +1 -1
- git_gcm-2.0.1/gcm/cli.py +200 -0
- git_gcm-2.0.1/gcm/git.py +172 -0
- git_gcm-2.0.1/gcm/interactive.py +120 -0
- git_gcm-1.0.2/gcm/prompts.py → git_gcm-2.0.1/gcm/prompt.py +25 -31
- {git_gcm-1.0.2 → git_gcm-2.0.1}/git_gcm.egg-info/PKG-INFO +29 -13
- {git_gcm-1.0.2 → git_gcm-2.0.1}/git_gcm.egg-info/SOURCES.txt +4 -3
- {git_gcm-1.0.2 → git_gcm-2.0.1}/git_gcm.egg-info/requires.txt +1 -0
- {git_gcm-1.0.2 → git_gcm-2.0.1}/pyproject.toml +3 -3
- git_gcm-1.0.2/README.md +0 -49
- git_gcm-1.0.2/gcm/cli.py +0 -179
- git_gcm-1.0.2/gcm/git_utils.py +0 -130
- {git_gcm-1.0.2 → git_gcm-2.0.1}/LICENSE +0 -0
- /git_gcm-1.0.2/gcm/llm_client.py → /git_gcm-2.0.1/gcm/llm.py +0 -0
- {git_gcm-1.0.2 → git_gcm-2.0.1}/git_gcm.egg-info/dependency_links.txt +0 -0
- {git_gcm-1.0.2 → git_gcm-2.0.1}/git_gcm.egg-info/entry_points.txt +0 -0
- {git_gcm-1.0.2 → git_gcm-2.0.1}/git_gcm.egg-info/top_level.txt +0 -0
- {git_gcm-1.0.2 → git_gcm-2.0.1}/setup.cfg +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: git-gcm
|
|
3
|
-
Version:
|
|
4
|
-
Summary: Git
|
|
3
|
+
Version: 2.0.1
|
|
4
|
+
Summary: Write Git commits the smart way.
|
|
5
5
|
Author-email: Luca <yangshaoxiong5545@gmail.com>
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/falconluca/gcm
|
|
@@ -25,47 +25,63 @@ Description-Content-Type: text/markdown
|
|
|
25
25
|
License-File: LICENSE
|
|
26
26
|
Requires-Dist: openai>=1.0.0
|
|
27
27
|
Requires-Dist: python-dotenv>=1.0.0
|
|
28
|
+
Requires-Dist: questionary>=1.4.0
|
|
28
29
|
Dynamic: license-file
|
|
29
30
|
|
|
30
31
|
# GCM
|
|
31
32
|
|
|
32
33
|
[](https://pypi.org/project/git-gcm/)
|
|
33
34
|
|
|
34
|
-
基于大模型的 Git Commit Message
|
|
35
|
+
基于大模型的 Git Commit Message 生成,支持所有 OpenAI 协议兼容的服务。
|
|
35
36
|
|
|
36
37
|
## 安装
|
|
37
38
|
|
|
38
39
|
```bash
|
|
39
|
-
|
|
40
|
+
# Mac 用户先装 pipx
|
|
41
|
+
brew install pipx && pipx ensurepath
|
|
42
|
+
|
|
43
|
+
pipx install git-gcm
|
|
40
44
|
```
|
|
41
45
|
|
|
42
|
-
|
|
46
|
+
升级:`pipx upgrade git-gcm`
|
|
43
47
|
|
|
44
|
-
|
|
48
|
+
## 配置
|
|
45
49
|
|
|
46
50
|
```bash
|
|
47
|
-
export
|
|
48
|
-
export
|
|
49
|
-
export GCM_MODEL=
|
|
51
|
+
export GCM_API_URL=https://api.openai.com/v1
|
|
52
|
+
export GCM_API_KEY=your-api-key
|
|
53
|
+
export GCM_MODEL=gpt-4o-mini
|
|
50
54
|
```
|
|
51
55
|
|
|
56
|
+
持久化写入 `~/.zshrc` / `~/.bashrc` 后 `source` 一下。
|
|
57
|
+
|
|
52
58
|
## 使用
|
|
53
59
|
|
|
54
60
|
```bash
|
|
55
61
|
git add .
|
|
56
|
-
gcm
|
|
57
|
-
gcm -v
|
|
62
|
+
gcm # 生成 commit message 并进入交互菜单
|
|
63
|
+
gcm -v # 详细模式
|
|
64
|
+
gcm -f # 跳过 git hooks(等价 --no-verify)
|
|
65
|
+
gcm --print-only # 仅输出,不提交
|
|
58
66
|
```
|
|
59
67
|
|
|
68
|
+
生成后进入交互菜单(↑↓ 选择,Enter 确认):
|
|
69
|
+
|
|
70
|
+
- **提交** — 直接 `git commit`
|
|
71
|
+
- **编辑** — 微调 message,Alt/⌥+Enter 保存
|
|
72
|
+
- **仅输出** — 只打印不提交
|
|
73
|
+
|
|
74
|
+
> 非交互终端(如 `gcm | cat`)自动跳过菜单,直接输出。
|
|
75
|
+
|
|
60
76
|
## 示例
|
|
61
77
|
|
|
62
|
-
|
|
78
|
+
精简模式:
|
|
63
79
|
|
|
64
80
|
```
|
|
65
81
|
feat(auth): 添加用户登录功能
|
|
66
82
|
```
|
|
67
83
|
|
|
68
|
-
|
|
84
|
+
详细模式(`-v`):
|
|
69
85
|
|
|
70
86
|
```
|
|
71
87
|
feat(auth): 添加用户登录功能
|
git_gcm-2.0.1/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# GCM
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/git-gcm/)
|
|
4
|
+
|
|
5
|
+
基于大模型的 Git Commit Message 生成,支持所有 OpenAI 协议兼容的服务。
|
|
6
|
+
|
|
7
|
+
## 安装
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Mac 用户先装 pipx
|
|
11
|
+
brew install pipx && pipx ensurepath
|
|
12
|
+
|
|
13
|
+
pipx install git-gcm
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
升级:`pipx upgrade git-gcm`
|
|
17
|
+
|
|
18
|
+
## 配置
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
export GCM_API_URL=https://api.openai.com/v1
|
|
22
|
+
export GCM_API_KEY=your-api-key
|
|
23
|
+
export GCM_MODEL=gpt-4o-mini
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
持久化写入 `~/.zshrc` / `~/.bashrc` 后 `source` 一下。
|
|
27
|
+
|
|
28
|
+
## 使用
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
git add .
|
|
32
|
+
gcm # 生成 commit message 并进入交互菜单
|
|
33
|
+
gcm -v # 详细模式
|
|
34
|
+
gcm -f # 跳过 git hooks(等价 --no-verify)
|
|
35
|
+
gcm --print-only # 仅输出,不提交
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
生成后进入交互菜单(↑↓ 选择,Enter 确认):
|
|
39
|
+
|
|
40
|
+
- **提交** — 直接 `git commit`
|
|
41
|
+
- **编辑** — 微调 message,Alt/⌥+Enter 保存
|
|
42
|
+
- **仅输出** — 只打印不提交
|
|
43
|
+
|
|
44
|
+
> 非交互终端(如 `gcm | cat`)自动跳过菜单,直接输出。
|
|
45
|
+
|
|
46
|
+
## 示例
|
|
47
|
+
|
|
48
|
+
精简模式:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
feat(auth): 添加用户登录功能
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
详细模式(`-v`):
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
feat(auth): 添加用户登录功能
|
|
58
|
+
|
|
59
|
+
- 实现 JWT token 认证
|
|
60
|
+
- 添加登录表单验证
|
|
61
|
+
- 集成第三方 OAuth 登录
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
MIT License
|
git_gcm-2.0.1/gcm/cli.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""命令行入口模块"""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from dotenv import load_dotenv
|
|
9
|
+
|
|
10
|
+
from gcm import __version__
|
|
11
|
+
from gcm.git import GitRepo
|
|
12
|
+
from gcm.prompt import PromptBuilder
|
|
13
|
+
from gcm.llm import LLMClient, LLMConfig
|
|
14
|
+
from gcm.interactive import InteractiveCommitter
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _ZhHelpFormatter(argparse.HelpFormatter):
|
|
18
|
+
"""中文帮助格式:将 argparse 默认的 'usage:' 标题本地化为 '用法: '。"""
|
|
19
|
+
|
|
20
|
+
def _format_usage(self, usage, actions, groups, prefix):
|
|
21
|
+
return super()._format_usage(usage, actions, groups, prefix or "用法: ")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class GCMApp:
|
|
25
|
+
"""GCM 应用主控:加载配置、解析参数、编排「生成→交互→提交」流程"""
|
|
26
|
+
|
|
27
|
+
def __init__(self):
|
|
28
|
+
self.repo = GitRepo()
|
|
29
|
+
self.args: Optional[argparse.Namespace] = None
|
|
30
|
+
|
|
31
|
+
def run(self) -> None:
|
|
32
|
+
"""主流程入口"""
|
|
33
|
+
self._load_env()
|
|
34
|
+
self.args = self._parse_args()
|
|
35
|
+
|
|
36
|
+
# 检查是否在 git 仓库中
|
|
37
|
+
if not self.repo.is_repo():
|
|
38
|
+
print("⚠️ 错误: 当前目录不在 Git 仓库中", file=sys.stderr)
|
|
39
|
+
sys.exit(1)
|
|
40
|
+
|
|
41
|
+
# 获取暂存区变更
|
|
42
|
+
changes = self.repo.get_staged_changes()
|
|
43
|
+
if not changes.files:
|
|
44
|
+
print("📦 暂存区没有变更。请先使用 'git add' 添加变更。", file=sys.stderr)
|
|
45
|
+
sys.exit(1)
|
|
46
|
+
|
|
47
|
+
# 生成 commit message
|
|
48
|
+
try:
|
|
49
|
+
config = self._build_config()
|
|
50
|
+
client = LLMClient(config)
|
|
51
|
+
builder = PromptBuilder(verbose=self.args.verbose)
|
|
52
|
+
commit_msg = client.chat(
|
|
53
|
+
builder.system_prompt(),
|
|
54
|
+
builder.user_prompt(changes),
|
|
55
|
+
)
|
|
56
|
+
except ValueError as e:
|
|
57
|
+
print(f"⚠️ 配置错误: {e}", file=sys.stderr)
|
|
58
|
+
sys.exit(1)
|
|
59
|
+
except RuntimeError as e:
|
|
60
|
+
print(f"❌ 生成失败: {e}", file=sys.stderr)
|
|
61
|
+
sys.exit(1)
|
|
62
|
+
|
|
63
|
+
# 仅输出模式(--print-only 或非交互终端):保持旧行为,便于脚本/管道
|
|
64
|
+
if self.args.print_only or not sys.stdin.isatty():
|
|
65
|
+
print(commit_msg)
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
# 交互模式:菜单 → 修改/确认 → 提交
|
|
69
|
+
InteractiveCommitter(self.repo, no_verify=self.args.no_verify).run(commit_msg)
|
|
70
|
+
|
|
71
|
+
def _load_env(self) -> None:
|
|
72
|
+
"""加载 .env 文件"""
|
|
73
|
+
# 加载用户主目录的 .env(优先级较低)
|
|
74
|
+
home_env = Path.home() / ".env"
|
|
75
|
+
if home_env.exists():
|
|
76
|
+
load_dotenv(home_env)
|
|
77
|
+
|
|
78
|
+
# 加载项目目录的 .env(优先级较高)
|
|
79
|
+
project_env = self._find_env_file()
|
|
80
|
+
if project_env:
|
|
81
|
+
load_dotenv(project_env)
|
|
82
|
+
|
|
83
|
+
def _find_env_file(self) -> Optional[Path]:
|
|
84
|
+
"""查找 .env 文件
|
|
85
|
+
|
|
86
|
+
查找顺序:
|
|
87
|
+
1. 当前目录的 .env
|
|
88
|
+
2. 父目录(向上查找直到 git 根目录)
|
|
89
|
+
3. 用户主目录的 .env
|
|
90
|
+
"""
|
|
91
|
+
# 当前目录
|
|
92
|
+
env_path = Path.cwd() / ".env"
|
|
93
|
+
if env_path.exists():
|
|
94
|
+
return env_path
|
|
95
|
+
|
|
96
|
+
# 向上查找直到 git 根目录
|
|
97
|
+
current = Path.cwd()
|
|
98
|
+
while current != current.parent:
|
|
99
|
+
current = current.parent
|
|
100
|
+
env_path = current / ".env"
|
|
101
|
+
if env_path.exists():
|
|
102
|
+
return env_path
|
|
103
|
+
# 如果找到 .git 目录就停止
|
|
104
|
+
if (current / ".git").exists():
|
|
105
|
+
break
|
|
106
|
+
|
|
107
|
+
# 用户主目录
|
|
108
|
+
env_path = Path.home() / ".env"
|
|
109
|
+
if env_path.exists():
|
|
110
|
+
return env_path
|
|
111
|
+
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
def _parse_args(self) -> argparse.Namespace:
|
|
115
|
+
"""解析命令行参数"""
|
|
116
|
+
parser = argparse.ArgumentParser(
|
|
117
|
+
prog="gcm",
|
|
118
|
+
description="基于大模型的 Git Commit Message 生成",
|
|
119
|
+
formatter_class=_ZhHelpFormatter,
|
|
120
|
+
add_help=False,
|
|
121
|
+
)
|
|
122
|
+
# 本地化 argparse 的 "options:" 分组标题
|
|
123
|
+
parser._optionals.title = "可选参数"
|
|
124
|
+
|
|
125
|
+
parser.add_argument(
|
|
126
|
+
"-h", "--help",
|
|
127
|
+
action="help",
|
|
128
|
+
default=argparse.SUPPRESS,
|
|
129
|
+
help="显示此帮助信息并退出",
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
parser.add_argument(
|
|
133
|
+
"-v", "--verbose",
|
|
134
|
+
action="store_true",
|
|
135
|
+
help="生成详细的 commit message"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
parser.add_argument(
|
|
139
|
+
"--api-base",
|
|
140
|
+
type=str,
|
|
141
|
+
help="API 基础 URL(覆盖 GCM_API_URL)"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
parser.add_argument(
|
|
145
|
+
"--api-key",
|
|
146
|
+
type=str,
|
|
147
|
+
help="API Key(覆盖 GCM_API_KEY,不推荐在命令行中使用)"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
parser.add_argument(
|
|
151
|
+
"-m", "--model",
|
|
152
|
+
type=str,
|
|
153
|
+
help="使用的模型名称(覆盖 GCM_MODEL)"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
parser.add_argument(
|
|
157
|
+
"--print-only", "--no-commit",
|
|
158
|
+
dest="print_only",
|
|
159
|
+
action="store_true",
|
|
160
|
+
help="仅输出 commit message,不进入交互提交(用于脚本/管道)"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
parser.add_argument(
|
|
164
|
+
"-f", "--no-verify",
|
|
165
|
+
dest="no_verify",
|
|
166
|
+
action="store_true",
|
|
167
|
+
help="跳过 pre-commit / commit-msg 钩子(等价 git commit --no-verify)"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
parser.add_argument(
|
|
171
|
+
"--version",
|
|
172
|
+
action="version",
|
|
173
|
+
version=f"%(prog)s {__version__}",
|
|
174
|
+
help="显示版本号并退出",
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
return parser.parse_args()
|
|
178
|
+
|
|
179
|
+
def _build_config(self) -> LLMConfig:
|
|
180
|
+
"""构建 LLM 配置(环境变量 + 命令行覆盖)"""
|
|
181
|
+
config = LLMConfig()
|
|
182
|
+
|
|
183
|
+
# 命令行参数覆盖环境变量
|
|
184
|
+
if self.args.api_base:
|
|
185
|
+
config.api_base = self.args.api_base
|
|
186
|
+
if self.args.api_key:
|
|
187
|
+
config.api_key = self.args.api_key
|
|
188
|
+
if self.args.model:
|
|
189
|
+
config.model = self.args.model
|
|
190
|
+
|
|
191
|
+
return config
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def main():
|
|
195
|
+
"""主入口函数"""
|
|
196
|
+
GCMApp().run()
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
if __name__ == "__main__":
|
|
200
|
+
main()
|
git_gcm-2.0.1/gcm/git.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Git 操作工具模块"""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import ClassVar, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class FileChange:
|
|
10
|
+
"""文件变更信息"""
|
|
11
|
+
status: str # A=新增, M=修改, D=删除, R=重命名
|
|
12
|
+
old_path: Optional[str] # 旧路径(重命名时使用)
|
|
13
|
+
new_path: str # 新路径
|
|
14
|
+
|
|
15
|
+
# 状态码 → 中文描述(类变量,非 dataclass 字段)
|
|
16
|
+
_STATUS_SYMBOLS: ClassVar[Dict[str, str]] = {
|
|
17
|
+
"A": "新增",
|
|
18
|
+
"M": "修改",
|
|
19
|
+
"D": "删除",
|
|
20
|
+
"R": "重命名",
|
|
21
|
+
"C": "复制",
|
|
22
|
+
"T": "类型变更",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def symbol(self) -> str:
|
|
27
|
+
"""状态对应的中文描述"""
|
|
28
|
+
return self._STATUS_SYMBOLS.get(self.status, self.status)
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def summary(self) -> str:
|
|
32
|
+
"""变更摘要行,如 '- [新增] path' 或 '- [重命名] old -> new'"""
|
|
33
|
+
if self.status in ("R", "C") and self.old_path:
|
|
34
|
+
return f"- [{self.symbol}] {self.old_path} -> {self.new_path}"
|
|
35
|
+
return f"- [{self.symbol}] {self.new_path}"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class StagedChanges:
|
|
40
|
+
"""暂存区变更信息"""
|
|
41
|
+
files: List[FileChange]
|
|
42
|
+
diff_content: str
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class CommitResult:
|
|
47
|
+
"""git commit 的执行结果"""
|
|
48
|
+
success: bool
|
|
49
|
+
stdout: str
|
|
50
|
+
stderr: str
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class GitRepo:
|
|
54
|
+
"""封装对 Git 仓库的子进程操作"""
|
|
55
|
+
|
|
56
|
+
def __init__(self, path: str = "."):
|
|
57
|
+
self.path = path
|
|
58
|
+
|
|
59
|
+
def is_repo(self) -> bool:
|
|
60
|
+
"""检查是否在 git 仓库中"""
|
|
61
|
+
try:
|
|
62
|
+
result = subprocess.run(
|
|
63
|
+
["git", "rev-parse", "--is-inside-work-tree"],
|
|
64
|
+
capture_output=True,
|
|
65
|
+
text=True,
|
|
66
|
+
check=False,
|
|
67
|
+
cwd=self.path,
|
|
68
|
+
)
|
|
69
|
+
return result.returncode == 0 and result.stdout.strip() == "true"
|
|
70
|
+
except FileNotFoundError:
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
def get_staged_changes(self, max_diff_lines: int = 500) -> StagedChanges:
|
|
74
|
+
"""获取完整的暂存区变更信息
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
max_diff_lines: diff 内容最大行数限制,防止过长
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
StagedChanges 对象
|
|
81
|
+
"""
|
|
82
|
+
files = self._staged_files()
|
|
83
|
+
diff = self._staged_diff()
|
|
84
|
+
|
|
85
|
+
# 限制 diff 行数
|
|
86
|
+
if diff:
|
|
87
|
+
lines = diff.split("\n")
|
|
88
|
+
if len(lines) > max_diff_lines:
|
|
89
|
+
diff = "\n".join(lines[:max_diff_lines])
|
|
90
|
+
diff += f"\n\n... (truncated, {len(lines) - max_diff_lines} more lines)"
|
|
91
|
+
|
|
92
|
+
return StagedChanges(files=files, diff_content=diff)
|
|
93
|
+
|
|
94
|
+
def commit(self, message: str, no_verify: bool = False) -> CommitResult:
|
|
95
|
+
"""执行 git commit
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
message: commit message
|
|
99
|
+
no_verify: 是否跳过 pre-commit / commit-msg 钩子(等价 --no-verify)
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
CommitResult
|
|
103
|
+
"""
|
|
104
|
+
cmd = ["git", "commit", "-m", message]
|
|
105
|
+
if no_verify:
|
|
106
|
+
cmd.append("--no-verify")
|
|
107
|
+
|
|
108
|
+
result = subprocess.run(
|
|
109
|
+
cmd,
|
|
110
|
+
capture_output=True,
|
|
111
|
+
text=True,
|
|
112
|
+
check=False,
|
|
113
|
+
cwd=self.path,
|
|
114
|
+
)
|
|
115
|
+
return CommitResult(
|
|
116
|
+
success=result.returncode == 0,
|
|
117
|
+
stdout=result.stdout,
|
|
118
|
+
stderr=result.stderr,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def _staged_files(self) -> List[FileChange]:
|
|
122
|
+
"""获取暂存区的文件列表"""
|
|
123
|
+
result = subprocess.run(
|
|
124
|
+
["git", "diff", "--cached", "--name-status", "--diff-filter=ACDMRT"],
|
|
125
|
+
capture_output=True,
|
|
126
|
+
text=True,
|
|
127
|
+
check=False,
|
|
128
|
+
cwd=self.path,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if result.returncode != 0:
|
|
132
|
+
return []
|
|
133
|
+
|
|
134
|
+
files = []
|
|
135
|
+
for line in result.stdout.strip().split("\n"):
|
|
136
|
+
if not line:
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
parts = line.split("\t")
|
|
140
|
+
status = parts[0][0] # 取状态首字母
|
|
141
|
+
|
|
142
|
+
if status in ("R", "C"):
|
|
143
|
+
# 重命名/复制: status\told_path\tnew_path
|
|
144
|
+
files.append(FileChange(
|
|
145
|
+
status=status,
|
|
146
|
+
old_path=parts[1],
|
|
147
|
+
new_path=parts[2]
|
|
148
|
+
))
|
|
149
|
+
else:
|
|
150
|
+
# 其他: A/M/D\tpath
|
|
151
|
+
files.append(FileChange(
|
|
152
|
+
status=status,
|
|
153
|
+
old_path=None,
|
|
154
|
+
new_path=parts[1]
|
|
155
|
+
))
|
|
156
|
+
|
|
157
|
+
return files
|
|
158
|
+
|
|
159
|
+
def _staged_diff(self) -> str:
|
|
160
|
+
"""获取暂存区的详细差异"""
|
|
161
|
+
result = subprocess.run(
|
|
162
|
+
["git", "diff", "--cached"],
|
|
163
|
+
capture_output=True,
|
|
164
|
+
text=True,
|
|
165
|
+
check=False,
|
|
166
|
+
cwd=self.path,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
if result.returncode != 0:
|
|
170
|
+
return ""
|
|
171
|
+
|
|
172
|
+
return result.stdout
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""交互式提交模块(基于 questionary 的 TUI 交互)。
|
|
2
|
+
|
|
3
|
+
封装生成 commit message 后的方向键菜单交互与提交。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
import questionary
|
|
9
|
+
from questionary import Style
|
|
10
|
+
|
|
11
|
+
from gcm.git import GitRepo
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class InteractiveCommitter:
|
|
15
|
+
"""生成 commit message 后的 TUI 交互与提交"""
|
|
16
|
+
|
|
17
|
+
# 自定义 TUI 配色:箭头与当前高亮项统一为青色
|
|
18
|
+
_STYLE = Style(
|
|
19
|
+
[
|
|
20
|
+
("qmark", "fg:#5fd7ff bold"),
|
|
21
|
+
("pointer", "fg:#5fd7ff bold"),
|
|
22
|
+
("highlighted", "fg:#5fd7ff bold"),
|
|
23
|
+
("selected", "fg:#5fd7ff"),
|
|
24
|
+
("answer", "fg:#5fd7ff bold"),
|
|
25
|
+
("instruction", "fg:#808080"),
|
|
26
|
+
]
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# 菜单选项(带 emoji,提升辨识度)
|
|
30
|
+
_CHOICE_COMMIT = "🍾 提交此 message"
|
|
31
|
+
_CHOICE_EDIT = "🚀 修改 message"
|
|
32
|
+
_CHOICE_PRINT = "📋 仅输出,不提交"
|
|
33
|
+
|
|
34
|
+
def __init__(self, repo: GitRepo, no_verify: bool = False):
|
|
35
|
+
self.repo = repo
|
|
36
|
+
self.no_verify = no_verify
|
|
37
|
+
|
|
38
|
+
def run(self, commit_msg: str) -> None:
|
|
39
|
+
"""展示菜单,允许直接提交、修改后提交或仅输出。
|
|
40
|
+
|
|
41
|
+
每轮循环清屏后重新渲染 message 与菜单,避免修改时历史输出堆积。
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
commit_msg: 生成的初始 commit message
|
|
45
|
+
"""
|
|
46
|
+
current = commit_msg
|
|
47
|
+
while True:
|
|
48
|
+
self._clear_screen()
|
|
49
|
+
self._show_message(current)
|
|
50
|
+
|
|
51
|
+
choice = questionary.select(
|
|
52
|
+
"请选择操作",
|
|
53
|
+
choices=[self._CHOICE_COMMIT, self._CHOICE_EDIT, self._CHOICE_PRINT],
|
|
54
|
+
qmark="🤖",
|
|
55
|
+
pointer="❯",
|
|
56
|
+
style=self._STYLE,
|
|
57
|
+
instruction="(↑↓ 选择, Enter 确认)",
|
|
58
|
+
).ask()
|
|
59
|
+
|
|
60
|
+
# ESC / Ctrl-C → 取消
|
|
61
|
+
if choice is None:
|
|
62
|
+
self._clear_screen()
|
|
63
|
+
print("🚪 已取消。")
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
if choice == self._CHOICE_PRINT:
|
|
67
|
+
self._clear_screen()
|
|
68
|
+
print(current)
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
if choice == self._CHOICE_EDIT:
|
|
72
|
+
new_msg = questionary.text(
|
|
73
|
+
"编辑 commit message(直接修改预填内容)",
|
|
74
|
+
default=current,
|
|
75
|
+
multiline=True,
|
|
76
|
+
qmark="🚀",
|
|
77
|
+
style=self._STYLE,
|
|
78
|
+
instruction="(方向键移动光标,Alt+Enter 或 Esc→Enter 提交)\n",
|
|
79
|
+
).ask()
|
|
80
|
+
# None: 取消编辑,保留原文;否则用编辑后的内容(未改即为原文)
|
|
81
|
+
if new_msg is not None:
|
|
82
|
+
current = new_msg
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
# _CHOICE_COMMIT:清屏后干净展示最终 message 并提交(无二次确认)
|
|
86
|
+
self._clear_screen()
|
|
87
|
+
self._show_message(current)
|
|
88
|
+
self._commit(current)
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
def _clear_screen(self) -> None:
|
|
92
|
+
"""清屏并将光标移到左上角(ANSI 转义,现代终端通用)。"""
|
|
93
|
+
sys.stdout.write("\033[2J\033[H")
|
|
94
|
+
sys.stdout.flush()
|
|
95
|
+
|
|
96
|
+
def _show_message(self, commit_msg: str) -> None:
|
|
97
|
+
"""展示当前的 commit message"""
|
|
98
|
+
print("📝 生成的 commit message:")
|
|
99
|
+
print("-" * 40)
|
|
100
|
+
print(commit_msg)
|
|
101
|
+
print("-" * 40)
|
|
102
|
+
if self.no_verify:
|
|
103
|
+
print("❗️ 已启用 -f/--no-verify:提交时将跳过 git hooks")
|
|
104
|
+
|
|
105
|
+
def _commit(self, commit_msg: str) -> None:
|
|
106
|
+
"""执行 git commit 并打印结果。
|
|
107
|
+
|
|
108
|
+
提交失败时将 git 的错误信息输出到 stderr 并以非零状态码退出。
|
|
109
|
+
"""
|
|
110
|
+
result = self.repo.commit(commit_msg, no_verify=self.no_verify)
|
|
111
|
+
|
|
112
|
+
if result.success:
|
|
113
|
+
if result.stdout:
|
|
114
|
+
print(result.stdout)
|
|
115
|
+
print("✅ 提交成功")
|
|
116
|
+
else:
|
|
117
|
+
print("❌ 提交失败:", file=sys.stderr)
|
|
118
|
+
if result.stderr:
|
|
119
|
+
print(result.stderr, file=sys.stderr)
|
|
120
|
+
sys.exit(1)
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"""提示词模板模块"""
|
|
2
2
|
|
|
3
|
-
from gcm.
|
|
3
|
+
from gcm.git import StagedChanges
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
class PromptBuilder:
|
|
7
|
+
"""构建发送给大模型的系统/用户提示词"""
|
|
8
|
+
|
|
9
|
+
SYSTEM_PROMPT = """你是一位专业的 Git 提交信息生成助手。你的任务是根据代码变更生成清晰、准确、符合规范的 commit message。
|
|
8
10
|
|
|
9
11
|
## 输出规则
|
|
10
12
|
|
|
@@ -42,32 +44,31 @@ SYSTEM_PROMPT = """你是一位专业的 Git 提交信息生成助手。你的
|
|
|
42
44
|
- 使用动词原形开头(如:添加、修复、更新、优化)
|
|
43
45
|
- 简洁明了,避免冗余"""
|
|
44
46
|
|
|
47
|
+
def __init__(self, verbose: bool = False):
|
|
48
|
+
self.verbose = verbose
|
|
49
|
+
|
|
50
|
+
def system_prompt(self) -> str:
|
|
51
|
+
"""系统提示词"""
|
|
52
|
+
return self.SYSTEM_PROMPT
|
|
45
53
|
|
|
46
|
-
def
|
|
47
|
-
|
|
54
|
+
def user_prompt(self, changes: StagedChanges) -> str:
|
|
55
|
+
"""根据暂存区变更构建用户提示词
|
|
48
56
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
verbose: 是否使用详细模式
|
|
57
|
+
Args:
|
|
58
|
+
changes: 暂存区变更信息
|
|
52
59
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
60
|
+
Returns:
|
|
61
|
+
用户提示词字符串
|
|
62
|
+
"""
|
|
63
|
+
if not changes.files:
|
|
64
|
+
return "暂存区没有任何变更。"
|
|
58
65
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
for file in changes.files:
|
|
62
|
-
status_text = get_status_symbol(file.status)
|
|
63
|
-
if file.status in ("R", "C") and file.old_path:
|
|
64
|
-
file_summary.append(f"- [{status_text}] {file.old_path} -> {file.new_path}")
|
|
65
|
-
else:
|
|
66
|
-
file_summary.append(f"- [{status_text}] {file.new_path}")
|
|
66
|
+
# 构建文件变更摘要
|
|
67
|
+
file_summary = [file.summary for file in changes.files]
|
|
67
68
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
# 构建提示词
|
|
70
|
+
mode = "详细" if self.verbose else "精简"
|
|
71
|
+
return f"""请根据以下暂存区变更生成{mode}的 commit message。
|
|
71
72
|
|
|
72
73
|
## 变更文件
|
|
73
74
|
{chr(10).join(file_summary)}
|
|
@@ -78,10 +79,3 @@ def build_user_prompt(changes: StagedChanges, verbose: bool = False) -> str:
|
|
|
78
79
|
```
|
|
79
80
|
|
|
80
81
|
请生成{mode}的 commit message:"""
|
|
81
|
-
|
|
82
|
-
return prompt
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def get_system_prompt() -> str:
|
|
86
|
-
"""获取系统提示词"""
|
|
87
|
-
return SYSTEM_PROMPT
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: git-gcm
|
|
3
|
-
Version:
|
|
4
|
-
Summary: Git
|
|
3
|
+
Version: 2.0.1
|
|
4
|
+
Summary: Write Git commits the smart way.
|
|
5
5
|
Author-email: Luca <yangshaoxiong5545@gmail.com>
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/falconluca/gcm
|
|
@@ -25,47 +25,63 @@ Description-Content-Type: text/markdown
|
|
|
25
25
|
License-File: LICENSE
|
|
26
26
|
Requires-Dist: openai>=1.0.0
|
|
27
27
|
Requires-Dist: python-dotenv>=1.0.0
|
|
28
|
+
Requires-Dist: questionary>=1.4.0
|
|
28
29
|
Dynamic: license-file
|
|
29
30
|
|
|
30
31
|
# GCM
|
|
31
32
|
|
|
32
33
|
[](https://pypi.org/project/git-gcm/)
|
|
33
34
|
|
|
34
|
-
基于大模型的 Git Commit Message
|
|
35
|
+
基于大模型的 Git Commit Message 生成,支持所有 OpenAI 协议兼容的服务。
|
|
35
36
|
|
|
36
37
|
## 安装
|
|
37
38
|
|
|
38
39
|
```bash
|
|
39
|
-
|
|
40
|
+
# Mac 用户先装 pipx
|
|
41
|
+
brew install pipx && pipx ensurepath
|
|
42
|
+
|
|
43
|
+
pipx install git-gcm
|
|
40
44
|
```
|
|
41
45
|
|
|
42
|
-
|
|
46
|
+
升级:`pipx upgrade git-gcm`
|
|
43
47
|
|
|
44
|
-
|
|
48
|
+
## 配置
|
|
45
49
|
|
|
46
50
|
```bash
|
|
47
|
-
export
|
|
48
|
-
export
|
|
49
|
-
export GCM_MODEL=
|
|
51
|
+
export GCM_API_URL=https://api.openai.com/v1
|
|
52
|
+
export GCM_API_KEY=your-api-key
|
|
53
|
+
export GCM_MODEL=gpt-4o-mini
|
|
50
54
|
```
|
|
51
55
|
|
|
56
|
+
持久化写入 `~/.zshrc` / `~/.bashrc` 后 `source` 一下。
|
|
57
|
+
|
|
52
58
|
## 使用
|
|
53
59
|
|
|
54
60
|
```bash
|
|
55
61
|
git add .
|
|
56
|
-
gcm
|
|
57
|
-
gcm -v
|
|
62
|
+
gcm # 生成 commit message 并进入交互菜单
|
|
63
|
+
gcm -v # 详细模式
|
|
64
|
+
gcm -f # 跳过 git hooks(等价 --no-verify)
|
|
65
|
+
gcm --print-only # 仅输出,不提交
|
|
58
66
|
```
|
|
59
67
|
|
|
68
|
+
生成后进入交互菜单(↑↓ 选择,Enter 确认):
|
|
69
|
+
|
|
70
|
+
- **提交** — 直接 `git commit`
|
|
71
|
+
- **编辑** — 微调 message,Alt/⌥+Enter 保存
|
|
72
|
+
- **仅输出** — 只打印不提交
|
|
73
|
+
|
|
74
|
+
> 非交互终端(如 `gcm | cat`)自动跳过菜单,直接输出。
|
|
75
|
+
|
|
60
76
|
## 示例
|
|
61
77
|
|
|
62
|
-
|
|
78
|
+
精简模式:
|
|
63
79
|
|
|
64
80
|
```
|
|
65
81
|
feat(auth): 添加用户登录功能
|
|
66
82
|
```
|
|
67
83
|
|
|
68
|
-
|
|
84
|
+
详细模式(`-v`):
|
|
69
85
|
|
|
70
86
|
```
|
|
71
87
|
feat(auth): 添加用户登录功能
|
|
@@ -3,9 +3,10 @@ README.md
|
|
|
3
3
|
pyproject.toml
|
|
4
4
|
gcm/__init__.py
|
|
5
5
|
gcm/cli.py
|
|
6
|
-
gcm/
|
|
7
|
-
gcm/
|
|
8
|
-
gcm/
|
|
6
|
+
gcm/git.py
|
|
7
|
+
gcm/interactive.py
|
|
8
|
+
gcm/llm.py
|
|
9
|
+
gcm/prompt.py
|
|
9
10
|
git_gcm.egg-info/PKG-INFO
|
|
10
11
|
git_gcm.egg-info/SOURCES.txt
|
|
11
12
|
git_gcm.egg-info/dependency_links.txt
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "git-gcm"
|
|
7
|
-
version = "
|
|
8
|
-
description = "Git
|
|
7
|
+
version = "2.0.1"
|
|
8
|
+
description = "Write Git commits the smart way."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
11
11
|
requires-python = ">=3.8"
|
|
@@ -26,7 +26,7 @@ classifiers = [
|
|
|
26
26
|
"Topic :: Software Development :: Version Control :: Git",
|
|
27
27
|
"Topic :: Utilities",
|
|
28
28
|
]
|
|
29
|
-
dependencies = ["openai>=1.0.0", "python-dotenv>=1.0.0"]
|
|
29
|
+
dependencies = ["openai>=1.0.0", "python-dotenv>=1.0.0", "questionary>=1.4.0"]
|
|
30
30
|
|
|
31
31
|
[project.scripts]
|
|
32
32
|
gcm = "gcm.cli:main"
|
git_gcm-1.0.2/README.md
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
# GCM
|
|
2
|
-
|
|
3
|
-
[](https://pypi.org/project/git-gcm/)
|
|
4
|
-
|
|
5
|
-
基于大模型的 Git Commit Message 自动生成工具,支持所有 OpenAI 协议兼容的大模型服务
|
|
6
|
-
|
|
7
|
-
## 安装
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
pip install git-gcm
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
## 配置
|
|
14
|
-
|
|
15
|
-
设置环境变量:
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
export GCM_API_KEY="your-api-key"
|
|
19
|
-
export GCM_API_URL="https://api.openai.com/v1"
|
|
20
|
-
export GCM_MODEL="gpt-4o-mini"
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
## 使用
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
git add .
|
|
27
|
-
gcm # 生成精简 commit message
|
|
28
|
-
gcm -v # 生成详细 commit message
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
## 示例
|
|
32
|
-
|
|
33
|
-
**精简模式:**
|
|
34
|
-
|
|
35
|
-
```
|
|
36
|
-
feat(auth): 添加用户登录功能
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
**详细模式 (`-v`):**
|
|
40
|
-
|
|
41
|
-
```
|
|
42
|
-
feat(auth): 添加用户登录功能
|
|
43
|
-
|
|
44
|
-
- 实现 JWT token 认证
|
|
45
|
-
- 添加登录表单验证
|
|
46
|
-
- 集成第三方 OAuth 登录
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
MIT License
|
git_gcm-1.0.2/gcm/cli.py
DELETED
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
"""命令行入口模块"""
|
|
2
|
-
|
|
3
|
-
import argparse
|
|
4
|
-
import sys
|
|
5
|
-
import os
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
from typing import Optional
|
|
8
|
-
|
|
9
|
-
from dotenv import load_dotenv
|
|
10
|
-
|
|
11
|
-
from gcm import __version__
|
|
12
|
-
from gcm.git_utils import (
|
|
13
|
-
is_git_repo,
|
|
14
|
-
get_staged_changes,
|
|
15
|
-
StagedChanges
|
|
16
|
-
)
|
|
17
|
-
from gcm.prompts import build_user_prompt, get_system_prompt
|
|
18
|
-
from gcm.llm_client import LLMClient, LLMConfig
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def find_env_file() -> Optional[Path]:
|
|
22
|
-
"""查找 .env 文件
|
|
23
|
-
|
|
24
|
-
查找顺序:
|
|
25
|
-
1. 当前目录的 .env
|
|
26
|
-
2. 父目录(向上查找直到 git 根目录)
|
|
27
|
-
3. 用户主目录的 .env
|
|
28
|
-
"""
|
|
29
|
-
# 当前目录
|
|
30
|
-
env_path = Path.cwd() / ".env"
|
|
31
|
-
if env_path.exists():
|
|
32
|
-
return env_path
|
|
33
|
-
|
|
34
|
-
# 向上查找直到 git 根目录
|
|
35
|
-
current = Path.cwd()
|
|
36
|
-
while current != current.parent:
|
|
37
|
-
current = current.parent
|
|
38
|
-
env_path = current / ".env"
|
|
39
|
-
if env_path.exists():
|
|
40
|
-
return env_path
|
|
41
|
-
# 如果找到 .git 目录就停止
|
|
42
|
-
if (current / ".git").exists():
|
|
43
|
-
break
|
|
44
|
-
|
|
45
|
-
# 用户主目录
|
|
46
|
-
env_path = Path.home() / ".env"
|
|
47
|
-
if env_path.exists():
|
|
48
|
-
return env_path
|
|
49
|
-
|
|
50
|
-
return None
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def load_env_files():
|
|
54
|
-
"""加载 .env 文件"""
|
|
55
|
-
# 加载用户主目录的 .env(优先级较低)
|
|
56
|
-
home_env = Path.home() / ".env"
|
|
57
|
-
if home_env.exists():
|
|
58
|
-
load_dotenv(home_env)
|
|
59
|
-
|
|
60
|
-
# 加载项目目录的 .env(优先级较高)
|
|
61
|
-
project_env = find_env_file()
|
|
62
|
-
if project_env:
|
|
63
|
-
load_dotenv(project_env)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def parse_args() -> argparse.Namespace:
|
|
67
|
-
"""解析命令行参数"""
|
|
68
|
-
parser = argparse.ArgumentParser(
|
|
69
|
-
prog="gcm",
|
|
70
|
-
description="自动生成 Git Commit Message"
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
parser.add_argument(
|
|
74
|
-
"-v", "--verbose",
|
|
75
|
-
action="store_true",
|
|
76
|
-
help="生成详细的 commit message"
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
parser.add_argument(
|
|
80
|
-
"--api-base",
|
|
81
|
-
type=str,
|
|
82
|
-
help="API 基础 URL(覆盖 GCM_API_URL)"
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
parser.add_argument(
|
|
86
|
-
"--api-key",
|
|
87
|
-
type=str,
|
|
88
|
-
help="API Key(覆盖 GCM_API_KEY,不推荐在命令行中使用)"
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
parser.add_argument(
|
|
92
|
-
"-m", "--model",
|
|
93
|
-
type=str,
|
|
94
|
-
help="使用的模型名称(覆盖 GCM_MODEL)"
|
|
95
|
-
)
|
|
96
|
-
|
|
97
|
-
parser.add_argument(
|
|
98
|
-
"--version",
|
|
99
|
-
action="version",
|
|
100
|
-
version=f"%(prog)s {__version__}"
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
return parser.parse_args()
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
def generate_commit_message(
|
|
107
|
-
changes: StagedChanges,
|
|
108
|
-
client: LLMClient,
|
|
109
|
-
verbose: bool = False
|
|
110
|
-
) -> str:
|
|
111
|
-
"""生成 commit message
|
|
112
|
-
|
|
113
|
-
Args:
|
|
114
|
-
changes: 暂存区变更
|
|
115
|
-
client: LLM 客户端
|
|
116
|
-
verbose: 是否详细模式
|
|
117
|
-
|
|
118
|
-
Returns:
|
|
119
|
-
生成的 commit message
|
|
120
|
-
"""
|
|
121
|
-
system_prompt = get_system_prompt()
|
|
122
|
-
user_prompt = build_user_prompt(changes, verbose=verbose)
|
|
123
|
-
|
|
124
|
-
return client.chat(system_prompt, user_prompt)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
def main():
|
|
128
|
-
"""主入口函数"""
|
|
129
|
-
# 加载 .env 文件
|
|
130
|
-
load_env_files()
|
|
131
|
-
|
|
132
|
-
args = parse_args()
|
|
133
|
-
|
|
134
|
-
# 检查是否在 git 仓库中
|
|
135
|
-
if not is_git_repo():
|
|
136
|
-
print("错误: 当前目录不在 Git 仓库中", file=sys.stderr)
|
|
137
|
-
sys.exit(1)
|
|
138
|
-
|
|
139
|
-
# 获取暂存区变更
|
|
140
|
-
changes = get_staged_changes()
|
|
141
|
-
|
|
142
|
-
if not changes.files:
|
|
143
|
-
print("暂存区没有变更。请先使用 'git add' 添加变更。", file=sys.stderr)
|
|
144
|
-
sys.exit(1)
|
|
145
|
-
|
|
146
|
-
# 创建配置(从环境变量加载)
|
|
147
|
-
config = LLMConfig()
|
|
148
|
-
|
|
149
|
-
# 命令行参数覆盖环境变量
|
|
150
|
-
if args.api_base:
|
|
151
|
-
config.api_base = args.api_base
|
|
152
|
-
if args.api_key:
|
|
153
|
-
config.api_key = args.api_key
|
|
154
|
-
if args.model:
|
|
155
|
-
config.model = args.model
|
|
156
|
-
|
|
157
|
-
# 创建 LLM 客户端
|
|
158
|
-
client = LLMClient(config)
|
|
159
|
-
|
|
160
|
-
# 生成 commit message
|
|
161
|
-
try:
|
|
162
|
-
commit_msg = generate_commit_message(
|
|
163
|
-
changes,
|
|
164
|
-
client,
|
|
165
|
-
verbose=args.verbose
|
|
166
|
-
)
|
|
167
|
-
except ValueError as e:
|
|
168
|
-
print(f"配置错误: {e}", file=sys.stderr)
|
|
169
|
-
sys.exit(1)
|
|
170
|
-
except RuntimeError as e:
|
|
171
|
-
print(f"生成失败: {e}", file=sys.stderr)
|
|
172
|
-
sys.exit(1)
|
|
173
|
-
|
|
174
|
-
# 输出结果
|
|
175
|
-
print(commit_msg)
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if __name__ == "__main__":
|
|
179
|
-
main()
|
git_gcm-1.0.2/gcm/git_utils.py
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
"""Git 操作工具模块"""
|
|
2
|
-
|
|
3
|
-
import subprocess
|
|
4
|
-
from dataclasses import dataclass
|
|
5
|
-
from typing import List, Optional
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
@dataclass
|
|
10
|
-
class FileChange:
|
|
11
|
-
"""文件变更信息"""
|
|
12
|
-
status: str # A=新增, M=修改, D=删除, R=重命名
|
|
13
|
-
old_path: Optional[str] # 旧路径(重命名时使用)
|
|
14
|
-
new_path: str # 新路径
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@dataclass
|
|
18
|
-
class StagedChanges:
|
|
19
|
-
"""暂存区变更信息"""
|
|
20
|
-
files: List[FileChange]
|
|
21
|
-
diff_content: str
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def is_git_repo() -> bool:
|
|
25
|
-
"""检查当前目录是否在 git 仓库中"""
|
|
26
|
-
try:
|
|
27
|
-
result = subprocess.run(
|
|
28
|
-
["git", "rev-parse", "--is-inside-work-tree"],
|
|
29
|
-
capture_output=True,
|
|
30
|
-
text=True,
|
|
31
|
-
check=False
|
|
32
|
-
)
|
|
33
|
-
return result.returncode == 0 and result.stdout.strip() == "true"
|
|
34
|
-
except FileNotFoundError:
|
|
35
|
-
return False
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def get_staged_files() -> List[FileChange]:
|
|
39
|
-
"""获取暂存区的文件列表"""
|
|
40
|
-
result = subprocess.run(
|
|
41
|
-
["git", "diff", "--cached", "--name-status", "--diff-filter=ACDMRT"],
|
|
42
|
-
capture_output=True,
|
|
43
|
-
text=True,
|
|
44
|
-
check=False
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
if result.returncode != 0:
|
|
48
|
-
return []
|
|
49
|
-
|
|
50
|
-
files = []
|
|
51
|
-
for line in result.stdout.strip().split("\n"):
|
|
52
|
-
if not line:
|
|
53
|
-
continue
|
|
54
|
-
|
|
55
|
-
parts = line.split("\t")
|
|
56
|
-
status = parts[0][0] # 取状态首字母
|
|
57
|
-
|
|
58
|
-
if status == "R":
|
|
59
|
-
# 重命名: R\told_path\tnew_path
|
|
60
|
-
files.append(FileChange(
|
|
61
|
-
status=status,
|
|
62
|
-
old_path=parts[1],
|
|
63
|
-
new_path=parts[2]
|
|
64
|
-
))
|
|
65
|
-
elif status == "C":
|
|
66
|
-
# 复制: C\told_path\tnew_path
|
|
67
|
-
files.append(FileChange(
|
|
68
|
-
status=status,
|
|
69
|
-
old_path=parts[1],
|
|
70
|
-
new_path=parts[2]
|
|
71
|
-
))
|
|
72
|
-
else:
|
|
73
|
-
# 其他: A/M/D\tpath
|
|
74
|
-
files.append(FileChange(
|
|
75
|
-
status=status,
|
|
76
|
-
old_path=None,
|
|
77
|
-
new_path=parts[1]
|
|
78
|
-
))
|
|
79
|
-
|
|
80
|
-
return files
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def get_staged_diff() -> str:
|
|
84
|
-
"""获取暂存区的详细差异"""
|
|
85
|
-
result = subprocess.run(
|
|
86
|
-
["git", "diff", "--cached"],
|
|
87
|
-
capture_output=True,
|
|
88
|
-
text=True,
|
|
89
|
-
check=False
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
if result.returncode != 0:
|
|
93
|
-
return ""
|
|
94
|
-
|
|
95
|
-
return result.stdout
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def get_staged_changes(max_diff_lines: int = 500) -> StagedChanges:
|
|
99
|
-
"""获取完整的暂存区变更信息
|
|
100
|
-
|
|
101
|
-
Args:
|
|
102
|
-
max_diff_lines: diff 内容最大行数限制,防止过长
|
|
103
|
-
|
|
104
|
-
Returns:
|
|
105
|
-
StagedChanges 对象
|
|
106
|
-
"""
|
|
107
|
-
files = get_staged_files()
|
|
108
|
-
diff = get_staged_diff()
|
|
109
|
-
|
|
110
|
-
# 限制 diff 行数
|
|
111
|
-
if diff:
|
|
112
|
-
lines = diff.split("\n")
|
|
113
|
-
if len(lines) > max_diff_lines:
|
|
114
|
-
diff = "\n".join(lines[:max_diff_lines])
|
|
115
|
-
diff += f"\n\n... (truncated, {len(lines) - max_diff_lines} more lines)"
|
|
116
|
-
|
|
117
|
-
return StagedChanges(files=files, diff_content=diff)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def get_status_symbol(status: str) -> str:
|
|
121
|
-
"""获取状态对应的符号/描述"""
|
|
122
|
-
symbols = {
|
|
123
|
-
"A": "新增",
|
|
124
|
-
"M": "修改",
|
|
125
|
-
"D": "删除",
|
|
126
|
-
"R": "重命名",
|
|
127
|
-
"C": "复制",
|
|
128
|
-
"T": "类型变更"
|
|
129
|
-
}
|
|
130
|
-
return symbols.get(status, status)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|