git-gcm 1.0.2__tar.gz → 2.0.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.
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: git-gcm
3
- Version: 1.0.2
4
- Summary: Git Commit Message 自动生成工具
3
+ Version: 2.0.0
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,13 +25,14 @@ 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
  [![PyPI version](https://badge.fury.io/py/git-gcm.svg)](https://pypi.org/project/git-gcm/)
33
34
 
34
- 基于大模型的 Git Commit Message 自动生成工具,支持所有 OpenAI 协议兼容的大模型服务
35
+ 基于大模型的 Git Commit Message 生成,支持所有 OpenAI 协议兼容的大模型服务
35
36
 
36
37
  ## 安装
37
38
 
@@ -44,19 +45,29 @@ pip install git-gcm
44
45
  设置环境变量:
45
46
 
46
47
  ```bash
47
- export GCM_API_KEY="your-api-key"
48
- export GCM_API_URL="https://api.openai.com/v1"
49
- export GCM_MODEL="gpt-4o-mini"
48
+ export GCM_API_URL=https://api.openai.com/v1
49
+ export GCM_API_KEY=your-api-key
50
+ export GCM_MODEL=gpt-4o-mini
50
51
  ```
51
52
 
52
53
  ## 使用
53
54
 
54
55
  ```bash
55
56
  git add .
56
- gcm # 生成精简 commit message
57
- gcm -v # 生成详细 commit message
57
+ gcm # 生成 commit message 并进入交互菜单
58
+ gcm -v # 生成详细 commit message 并进入交互菜单
59
+ gcm -f # 提交时跳过 git hooks(等价 git commit --no-verify)
60
+ gcm --print-only # 仅输出 message,不提交(用于脚本/管道)
58
61
  ```
59
62
 
63
+ 运行 `gcm` 后会展示生成的 commit message,并用方向键导航的菜单让你选择:
64
+
65
+ - **提交此 message**:选中后按 Enter 直接执行 `git commit`
66
+ - **修改 message**:在预填的编辑框里直接微调(支持多行,兼容 `-v`),Alt+Enter 提交后回到菜单
67
+ - **仅输出,不提交**:只打印 message
68
+
69
+ > ↑↓ 移动选择,Enter 确认,ESC / Ctrl-C 取消。在管道或非交互终端(如 `gcm | cat`)中,gcm 会自动仅输出 message,不弹交互。
70
+
60
71
  ## 示例
61
72
 
62
73
  **精简模式:**
@@ -0,0 +1,59 @@
1
+ # GCM
2
+
3
+ [![PyPI version](https://badge.fury.io/py/git-gcm.svg)](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_URL=https://api.openai.com/v1
19
+ export GCM_API_KEY=your-api-key
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
+ gcm -f # 提交时跳过 git hooks(等价 git commit --no-verify)
30
+ gcm --print-only # 仅输出 message,不提交(用于脚本/管道)
31
+ ```
32
+
33
+ 运行 `gcm` 后会展示生成的 commit message,并用方向键导航的菜单让你选择:
34
+
35
+ - **提交此 message**:选中后按 Enter 直接执行 `git commit`
36
+ - **修改 message**:在预填的编辑框里直接微调(支持多行,兼容 `-v`),Alt+Enter 提交后回到菜单
37
+ - **仅输出,不提交**:只打印 message
38
+
39
+ > ↑↓ 移动选择,Enter 确认,ESC / Ctrl-C 取消。在管道或非交互终端(如 `gcm | cat`)中,gcm 会自动仅输出 message,不弹交互。
40
+
41
+ ## 示例
42
+
43
+ **精简模式:**
44
+
45
+ ```
46
+ feat(auth): 添加用户登录功能
47
+ ```
48
+
49
+ **详细模式 (`-v`):**
50
+
51
+ ```
52
+ feat(auth): 添加用户登录功能
53
+
54
+ - 实现 JWT token 认证
55
+ - 添加登录表单验证
56
+ - 集成第三方 OAuth 登录
57
+ ```
58
+
59
+ MIT License
@@ -1,3 +1,3 @@
1
1
  """GCM - Git Commit Message Generator"""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "2.0.0"
@@ -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()
@@ -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.git_utils import StagedChanges, get_status_symbol
3
+ from gcm.git import StagedChanges
4
4
 
5
5
 
6
- # 系统提示词
7
- SYSTEM_PROMPT = """你是一位专业的 Git 提交信息生成助手。你的任务是根据代码变更生成清晰、准确、符合规范的 commit message。
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 build_user_prompt(changes: StagedChanges, verbose: bool = False) -> str:
47
- """构建用户提示词
54
+ def user_prompt(self, changes: StagedChanges) -> str:
55
+ """根据暂存区变更构建用户提示词
48
56
 
49
- Args:
50
- changes: 暂存区变更信息
51
- verbose: 是否使用详细模式
57
+ Args:
58
+ changes: 暂存区变更信息
52
59
 
53
- Returns:
54
- 用户提示词字符串
55
- """
56
- if not changes.files:
57
- return "暂存区没有任何变更。"
60
+ Returns:
61
+ 用户提示词字符串
62
+ """
63
+ if not changes.files:
64
+ return "暂存区没有任何变更。"
58
65
 
59
- # 构建文件变更摘要
60
- file_summary = []
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
- mode = "详细" if verbose else "精简"
70
- prompt = f"""请根据以下暂存区变更生成{mode}的 commit message。
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: 1.0.2
4
- Summary: Git Commit Message 自动生成工具
3
+ Version: 2.0.0
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,13 +25,14 @@ 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
  [![PyPI version](https://badge.fury.io/py/git-gcm.svg)](https://pypi.org/project/git-gcm/)
33
34
 
34
- 基于大模型的 Git Commit Message 自动生成工具,支持所有 OpenAI 协议兼容的大模型服务
35
+ 基于大模型的 Git Commit Message 生成,支持所有 OpenAI 协议兼容的大模型服务
35
36
 
36
37
  ## 安装
37
38
 
@@ -44,19 +45,29 @@ pip install git-gcm
44
45
  设置环境变量:
45
46
 
46
47
  ```bash
47
- export GCM_API_KEY="your-api-key"
48
- export GCM_API_URL="https://api.openai.com/v1"
49
- export GCM_MODEL="gpt-4o-mini"
48
+ export GCM_API_URL=https://api.openai.com/v1
49
+ export GCM_API_KEY=your-api-key
50
+ export GCM_MODEL=gpt-4o-mini
50
51
  ```
51
52
 
52
53
  ## 使用
53
54
 
54
55
  ```bash
55
56
  git add .
56
- gcm # 生成精简 commit message
57
- gcm -v # 生成详细 commit message
57
+ gcm # 生成 commit message 并进入交互菜单
58
+ gcm -v # 生成详细 commit message 并进入交互菜单
59
+ gcm -f # 提交时跳过 git hooks(等价 git commit --no-verify)
60
+ gcm --print-only # 仅输出 message,不提交(用于脚本/管道)
58
61
  ```
59
62
 
63
+ 运行 `gcm` 后会展示生成的 commit message,并用方向键导航的菜单让你选择:
64
+
65
+ - **提交此 message**:选中后按 Enter 直接执行 `git commit`
66
+ - **修改 message**:在预填的编辑框里直接微调(支持多行,兼容 `-v`),Alt+Enter 提交后回到菜单
67
+ - **仅输出,不提交**:只打印 message
68
+
69
+ > ↑↓ 移动选择,Enter 确认,ESC / Ctrl-C 取消。在管道或非交互终端(如 `gcm | cat`)中,gcm 会自动仅输出 message,不弹交互。
70
+
60
71
  ## 示例
61
72
 
62
73
  **精简模式:**
@@ -3,9 +3,10 @@ README.md
3
3
  pyproject.toml
4
4
  gcm/__init__.py
5
5
  gcm/cli.py
6
- gcm/git_utils.py
7
- gcm/llm_client.py
8
- gcm/prompts.py
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
@@ -1,2 +1,3 @@
1
1
  openai>=1.0.0
2
2
  python-dotenv>=1.0.0
3
+ questionary>=1.4.0
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "git-gcm"
7
- version = "1.0.2"
8
- description = "Git Commit Message 自动生成工具"
7
+ version = "2.0.0"
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
- [![PyPI version](https://badge.fury.io/py/git-gcm.svg)](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()
@@ -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