ai_git_utils 0.4.2__py3-none-any.whl
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.
- ai_git_utils/__init__.py +3 -0
- ai_git_utils/ai_model.py +6 -0
- ai_git_utils/cli.py +322 -0
- ai_git_utils/config_manager.py +50 -0
- ai_git_utils/git_operations.py +19 -0
- ai_git_utils/main.py +4 -0
- ai_git_utils/utils.py +91 -0
- ai_git_utils-0.4.2.dist-info/METADATA +109 -0
- ai_git_utils-0.4.2.dist-info/RECORD +12 -0
- ai_git_utils-0.4.2.dist-info/WHEEL +4 -0
- ai_git_utils-0.4.2.dist-info/entry_points.txt +2 -0
- ai_git_utils-0.4.2.dist-info/licenses/LICENSE +0 -0
ai_git_utils/__init__.py
ADDED
ai_git_utils/ai_model.py
ADDED
ai_git_utils/cli.py
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
from rich.table import Table
|
|
4
|
+
from typing import Optional, List
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from .config_manager import (
|
|
7
|
+
add_model_to_config,
|
|
8
|
+
remove_model_from_config,
|
|
9
|
+
set_active_model_in_config,
|
|
10
|
+
get_active_model,
|
|
11
|
+
load_config,
|
|
12
|
+
)
|
|
13
|
+
from .git_operations import get_git_diff, commit_changes
|
|
14
|
+
from .ai_model import get_model
|
|
15
|
+
from .utils import beautify_diff, edit_commit_message
|
|
16
|
+
from git import Repo
|
|
17
|
+
from git.exc import InvalidGitRepositoryError, GitCommandError
|
|
18
|
+
from .git_operations import get_commit_diff
|
|
19
|
+
from . import __version__
|
|
20
|
+
from openai import OpenAI
|
|
21
|
+
from dataclasses import dataclass, asdict
|
|
22
|
+
import json
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
app = typer.Typer()
|
|
26
|
+
model_app = typer.Typer()
|
|
27
|
+
diff_app = typer.Typer()
|
|
28
|
+
console = Console()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class CommitMessage:
|
|
33
|
+
type: str
|
|
34
|
+
scope: str
|
|
35
|
+
emoji: str
|
|
36
|
+
subject: str
|
|
37
|
+
fix_items: List[str]
|
|
38
|
+
|
|
39
|
+
example_commit = CommitMessage(
|
|
40
|
+
type="fix",
|
|
41
|
+
scope="cli",
|
|
42
|
+
emoji="🐛",
|
|
43
|
+
subject="segmentation fault in inference",
|
|
44
|
+
fix_items=["fix segmentation fault in inference"],
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@model_app.command("add")
|
|
49
|
+
def add_model(
|
|
50
|
+
name: str = typer.Option(..., prompt=True),
|
|
51
|
+
model: str = typer.Option(..., prompt=True),
|
|
52
|
+
base_url: str = typer.Option(..., prompt=True),
|
|
53
|
+
temperature: float = typer.Option(..., prompt=True),
|
|
54
|
+
api_key: str = typer.Option(..., prompt=True),
|
|
55
|
+
):
|
|
56
|
+
"""添加新的模型配置"""
|
|
57
|
+
model_config = {
|
|
58
|
+
"model": model,
|
|
59
|
+
"base_url": base_url,
|
|
60
|
+
"temperature": temperature,
|
|
61
|
+
"api_key": api_key,
|
|
62
|
+
}
|
|
63
|
+
add_model_to_config(name, model_config)
|
|
64
|
+
typer.echo(f"模型 '{name}' 已添加并激活。")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@model_app.command("remove")
|
|
68
|
+
def remove_model(name: str = typer.Option(..., prompt=True)):
|
|
69
|
+
"""删除指定的模型配置"""
|
|
70
|
+
remove_model_from_config(name)
|
|
71
|
+
typer.echo(f"模型 '{name}' 已删除。")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@model_app.command("active")
|
|
75
|
+
def activate_model(name: str = typer.Option(..., prompt=True)):
|
|
76
|
+
"""激活指定的模型配置"""
|
|
77
|
+
set_active_model_in_config(name)
|
|
78
|
+
typer.echo(f"模型 '{name}' 已激活。")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@model_app.command("list")
|
|
82
|
+
def list_models():
|
|
83
|
+
"""列出所有可用的模型配置"""
|
|
84
|
+
config = load_config()
|
|
85
|
+
active_model = config["active_model"]
|
|
86
|
+
|
|
87
|
+
table = Table(title="可用模型配置")
|
|
88
|
+
table.add_column("模型名称", style="cyan")
|
|
89
|
+
table.add_column("状态", style="magenta")
|
|
90
|
+
|
|
91
|
+
for name in config["models"]:
|
|
92
|
+
status = "激活" if name == active_model else ""
|
|
93
|
+
table.add_row(name, status)
|
|
94
|
+
|
|
95
|
+
console.print(table)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@model_app.command("show")
|
|
99
|
+
def show_config():
|
|
100
|
+
"""显示当前的模型配置"""
|
|
101
|
+
config = load_config()
|
|
102
|
+
active_model = config["active_model"]
|
|
103
|
+
|
|
104
|
+
if not config["models"]:
|
|
105
|
+
typer.echo("当前没有保存的模型配置。请运行 'aigit model add' 命令添加模型。")
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
table = Table(title=f"当前激活的模型配置: {active_model}")
|
|
109
|
+
table.add_column("配置项", style="cyan", no_wrap=True)
|
|
110
|
+
table.add_column("值", style="magenta")
|
|
111
|
+
|
|
112
|
+
active_config = config["models"].get(active_model, {})
|
|
113
|
+
for key, value in active_config.items():
|
|
114
|
+
if key == "api_key" and value:
|
|
115
|
+
value = value[:4] + "*" * (len(value) - 4)
|
|
116
|
+
table.add_row(key, str(value))
|
|
117
|
+
|
|
118
|
+
console.print(table)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@app.command()
|
|
122
|
+
def commit(
|
|
123
|
+
file_path: Optional[str] = typer.Option(None, "--file", "-f", help="指定文件路径"),
|
|
124
|
+
language: str = typer.Option(
|
|
125
|
+
"English", "--lang", "-l", help="设置语言(English/Chinese)"
|
|
126
|
+
),
|
|
127
|
+
):
|
|
128
|
+
"""
|
|
129
|
+
使用 AI 智能生成代码更改信息
|
|
130
|
+
"""
|
|
131
|
+
active_config = get_active_model()
|
|
132
|
+
if not active_config:
|
|
133
|
+
typer.echo(
|
|
134
|
+
"错误:未找到激活的模型配置。请先运行 'aigit model add' 或 'aigit model active' 命令。"
|
|
135
|
+
)
|
|
136
|
+
raise typer.Exit(code=1)
|
|
137
|
+
try:
|
|
138
|
+
repo = Repo(".")
|
|
139
|
+
repo.git.add('.')
|
|
140
|
+
diff_output = get_git_diff(repo, True, file_path)
|
|
141
|
+
|
|
142
|
+
if not diff_output:
|
|
143
|
+
typer.echo("没有检测到更改。")
|
|
144
|
+
else:
|
|
145
|
+
|
|
146
|
+
lite_system_prompt = f'''
|
|
147
|
+
Craft clear and concise commit messages following the Conventional Commits standard format for git.
|
|
148
|
+
When presented with a git diff summary, your task is to convert it into a useful commit message and add a brief description of the changes made, ensuring that lines are not longer than 74 characters.
|
|
149
|
+
Your commit message should describe the nature and purpose of the changes in a comprehensive, informative, and concise manner.
|
|
150
|
+
The commit message should return the json object, keep the content concise and to the point.
|
|
151
|
+
emoji list:
|
|
152
|
+
|
|
153
|
+
- ✨ New feature
|
|
154
|
+
- 🐛 Fix bug
|
|
155
|
+
- 📚 Documentation update
|
|
156
|
+
- 🚀 Deploy stuff
|
|
157
|
+
- 💄 UI/style update
|
|
158
|
+
- 🎨 Improve structure/format of the code
|
|
159
|
+
- 🔧 Configuration modification
|
|
160
|
+
- 🔥 Delete code/file
|
|
161
|
+
- 🚑 Critical fix
|
|
162
|
+
- ➕ Add dependency
|
|
163
|
+
- ⚡️ Performance improvement
|
|
164
|
+
- ♻️ Refactor code
|
|
165
|
+
- 👷 Add or update CI build system
|
|
166
|
+
|
|
167
|
+
EXAMPLE JSON OUTPUT:
|
|
168
|
+
{json.dumps(asdict(example_commit), indent=2, ensure_ascii=False)}
|
|
169
|
+
|
|
170
|
+
output only the json object and answer all my questions in {language}.
|
|
171
|
+
'''
|
|
172
|
+
|
|
173
|
+
lite_message_content = f"""git diff summary: \n{diff_output}"""
|
|
174
|
+
lite_messages = [
|
|
175
|
+
{"role": "system", "content": lite_system_prompt},
|
|
176
|
+
{"role": "user", "content": lite_message_content}
|
|
177
|
+
]
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
active_model = get_model()
|
|
181
|
+
model_name = active_model.get('model')
|
|
182
|
+
|
|
183
|
+
client = OpenAI(
|
|
184
|
+
base_url=active_model.get('base_url'),
|
|
185
|
+
api_key=active_model.get('api_key'),
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
response = client.chat.completions.create(
|
|
189
|
+
extra_headers={
|
|
190
|
+
"X-Title": "AIGit",
|
|
191
|
+
},
|
|
192
|
+
extra_body={},
|
|
193
|
+
model=model_name,
|
|
194
|
+
messages=lite_messages,
|
|
195
|
+
temperature=active_model.get('temperature'),
|
|
196
|
+
response_format={
|
|
197
|
+
'type': 'json_object'
|
|
198
|
+
}
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
json_str = response.choices[0].message.content
|
|
202
|
+
try:
|
|
203
|
+
data = json.loads(json_str)
|
|
204
|
+
except json.JSONDecodeError:
|
|
205
|
+
typer.echo("请检查模型是否支持 json 结构化输出。")
|
|
206
|
+
raise typer.Exit(code=1)
|
|
207
|
+
commit_message = CommitMessage(**data)
|
|
208
|
+
|
|
209
|
+
typer.echo("AI 生成的提交信息:")
|
|
210
|
+
initial_commit_message = f"""{commit_message.type}({commit_message.scope}): {commit_message.emoji} {commit_message.subject}
|
|
211
|
+
|
|
212
|
+
{chr(10).join(f'- {item}' for item in commit_message.fix_items)}
|
|
213
|
+
"""
|
|
214
|
+
typer.echo()
|
|
215
|
+
typer.echo("\n请编辑提交信息,保存并关闭编辑器以继续。")
|
|
216
|
+
|
|
217
|
+
edited_message = edit_commit_message(initial_commit_message)
|
|
218
|
+
|
|
219
|
+
typer.echo("编辑后的提交信息:")
|
|
220
|
+
typer.echo(edited_message)
|
|
221
|
+
|
|
222
|
+
if typer.confirm("确认提交这些更改?"):
|
|
223
|
+
commit_changes(repo, edited_message)
|
|
224
|
+
typer.echo("更改已成功提交!")
|
|
225
|
+
else:
|
|
226
|
+
typer.echo("提交已取消。")
|
|
227
|
+
|
|
228
|
+
except InvalidGitRepositoryError:
|
|
229
|
+
typer.echo("错误:当前目录不是有效的Git仓库。", err=True)
|
|
230
|
+
except GitCommandError as e:
|
|
231
|
+
typer.echo(f"Git命令执行错误:{str(e)}", err=True)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@diff_app.command("current")
|
|
235
|
+
def diff(
|
|
236
|
+
file_path: Optional[str] = typer.Option(None, "--file", "-f", help="指定文件路径"),
|
|
237
|
+
):
|
|
238
|
+
"""
|
|
239
|
+
查看代码更改
|
|
240
|
+
"""
|
|
241
|
+
try:
|
|
242
|
+
repo = Repo(".")
|
|
243
|
+
repo.git.add('.')
|
|
244
|
+
diff_output = get_git_diff(repo, True, file_path)
|
|
245
|
+
|
|
246
|
+
if not diff_output:
|
|
247
|
+
typer.echo("没有检测到更改。")
|
|
248
|
+
else:
|
|
249
|
+
beautify_diff(diff_output)
|
|
250
|
+
|
|
251
|
+
except InvalidGitRepositoryError:
|
|
252
|
+
typer.echo("错误:当前目录不是有效的Git仓库。", err=True)
|
|
253
|
+
except GitCommandError as e:
|
|
254
|
+
typer.echo(f"Git命令执行错误:{str(e)}", err=True)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@app.command()
|
|
258
|
+
def log(
|
|
259
|
+
limit: int = typer.Option(10, "--limit", "-n", help="显示的提交数量"),
|
|
260
|
+
since: str = typer.Option(
|
|
261
|
+
None, "--since", "-s", help="显示指定日期之后的提交,格式:YYYY-MM-DD"
|
|
262
|
+
),
|
|
263
|
+
until: str = typer.Option(
|
|
264
|
+
None, "--until", "-u", help="显示指定日期之前的提交,格式:YYYY-MM-DD"
|
|
265
|
+
),
|
|
266
|
+
):
|
|
267
|
+
"""美观地显示git log"""
|
|
268
|
+
try:
|
|
269
|
+
repo = Repo(".")
|
|
270
|
+
commits = repo.iter_commits()
|
|
271
|
+
|
|
272
|
+
if since:
|
|
273
|
+
since_date = datetime.strptime(since, "%Y-%m-%d")
|
|
274
|
+
commits = filter(lambda c: c.committed_datetime > since_date, commits)
|
|
275
|
+
|
|
276
|
+
if until:
|
|
277
|
+
until_date = datetime.strptime(until, "%Y-%m-%d")
|
|
278
|
+
commits = filter(lambda c: c.committed_datetime < until_date, commits)
|
|
279
|
+
|
|
280
|
+
table = Table(title="Git Log")
|
|
281
|
+
table.add_column("提交哈希", style="cyan", no_wrap=True)
|
|
282
|
+
table.add_column("作者", style="magenta")
|
|
283
|
+
table.add_column("日期", style="green")
|
|
284
|
+
table.add_column("提交信息", style="yellow")
|
|
285
|
+
|
|
286
|
+
for _commit in list(commits)[:limit]:
|
|
287
|
+
table.add_row(
|
|
288
|
+
_commit.hexsha[:7],
|
|
289
|
+
_commit.author.name,
|
|
290
|
+
_commit.committed_datetime.strftime("%Y-%m-%d %H:%M:%S"),
|
|
291
|
+
_commit.message.split("\n")[0],
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
console.print(table)
|
|
295
|
+
|
|
296
|
+
except InvalidGitRepositoryError:
|
|
297
|
+
typer.echo("错误:当前目录不是有效的Git仓库。", err=True)
|
|
298
|
+
except GitCommandError as e:
|
|
299
|
+
typer.echo(f"Git命令执行错误:{str(e)}", err=True)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
@diff_app.command("commit")
|
|
303
|
+
def commit_diff(commit_hash: str = typer.Argument(..., help="指定的commit哈希值")):
|
|
304
|
+
"""显示指定commit的diff"""
|
|
305
|
+
try:
|
|
306
|
+
repo = Repo(".")
|
|
307
|
+
diff_output = get_commit_diff(repo, commit_hash)
|
|
308
|
+
beautify_diff(diff_output)
|
|
309
|
+
except InvalidGitRepositoryError:
|
|
310
|
+
typer.echo("错误:当前目录不是有效的Git仓库。", err=True)
|
|
311
|
+
except GitCommandError as e:
|
|
312
|
+
typer.echo(f"Git命令执行错误:{str(e)}", err=True)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
@app.command()
|
|
316
|
+
def version():
|
|
317
|
+
"""显示当前软件版本"""
|
|
318
|
+
typer.echo(f"AI Git Utils V{__version__}")
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
app.add_typer(model_app, name="model", help="管理AI模型")
|
|
322
|
+
app.add_typer(diff_app, name="diff", help="查看代码更改")
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
from typing import Dict, Any
|
|
4
|
+
|
|
5
|
+
CONFIG_FILE = os.path.expanduser("~/.aigit/model.json")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def load_config() -> Dict[str, Any]:
|
|
9
|
+
if os.path.exists(CONFIG_FILE):
|
|
10
|
+
with open(CONFIG_FILE, "r") as f:
|
|
11
|
+
return json.load(f)
|
|
12
|
+
return {"models": {}, "active_model": None}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def save_config(config: Dict[str, Any]) -> None:
|
|
16
|
+
os.makedirs(os.path.dirname(CONFIG_FILE), exist_ok=True)
|
|
17
|
+
with open(CONFIG_FILE, "w") as f:
|
|
18
|
+
json.dump(config, f, indent=2)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def add_model_to_config(name: str, model_config: Dict[str, Any]) -> None:
|
|
22
|
+
config = load_config()
|
|
23
|
+
config["models"][name] = model_config
|
|
24
|
+
if config["active_model"] is None:
|
|
25
|
+
config["active_model"] = name
|
|
26
|
+
save_config(config)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def remove_model_from_config(name: str) -> None:
|
|
30
|
+
config = load_config()
|
|
31
|
+
if name in config["models"]:
|
|
32
|
+
del config["models"][name]
|
|
33
|
+
if config["active_model"] == name:
|
|
34
|
+
config["active_model"] = next(iter(config["models"]), None)
|
|
35
|
+
save_config(config)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def set_active_model_in_config(name: str) -> None:
|
|
39
|
+
config = load_config()
|
|
40
|
+
if name in config["models"]:
|
|
41
|
+
config["active_model"] = name
|
|
42
|
+
save_config(config)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_active_model() -> Dict[str, Any]:
|
|
46
|
+
config = load_config()
|
|
47
|
+
active_model = config["active_model"]
|
|
48
|
+
if active_model and active_model in config["models"]:
|
|
49
|
+
return config["models"][active_model]
|
|
50
|
+
return {}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from git import Repo
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def get_git_diff(repo: Repo, staged: bool = False, file_path: Optional[str] = None):
|
|
6
|
+
exclude_pattern = ":(exclude)*lock*"
|
|
7
|
+
if staged:
|
|
8
|
+
return repo.git.diff("--staged", file_path, exclude_pattern)
|
|
9
|
+
else:
|
|
10
|
+
return repo.git.diff(file_path, exclude_pattern)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def commit_changes(repo: Repo, commit_message: str):
|
|
14
|
+
repo.git.add(A=True)
|
|
15
|
+
repo.index.commit(commit_message)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_commit_diff(repo: Repo, commit_hash: str):
|
|
19
|
+
return repo.git.diff(f"{commit_hash}^!", "--word-diff=color")
|
ai_git_utils/main.py
ADDED
ai_git_utils/utils.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import tempfile
|
|
3
|
+
import subprocess
|
|
4
|
+
import shutil
|
|
5
|
+
from rich.syntax import Syntax
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
console = Console()
|
|
9
|
+
|
|
10
|
+
ALLOWED_EDITORS = ["vim", "vi", "nano", "emacs", "code", "subl"]
|
|
11
|
+
|
|
12
|
+
def beautify_diff(diff_output: str) -> None:
|
|
13
|
+
syntax = Syntax(
|
|
14
|
+
diff_output,
|
|
15
|
+
"diff",
|
|
16
|
+
line_numbers=True,
|
|
17
|
+
word_wrap=True,
|
|
18
|
+
indent_guides=False,
|
|
19
|
+
theme="monokai",
|
|
20
|
+
)
|
|
21
|
+
console.print(syntax)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def edit_commit_message(initial_message: str) -> str:
|
|
25
|
+
with tempfile.NamedTemporaryFile(mode="w+", suffix=".tmp", delete=False) as tf:
|
|
26
|
+
tf.write(initial_message)
|
|
27
|
+
tf.flush()
|
|
28
|
+
tf_name = tf.name
|
|
29
|
+
|
|
30
|
+
editor_from_env = os.environ.get("EDITOR")
|
|
31
|
+
editor_to_use = None
|
|
32
|
+
|
|
33
|
+
if editor_from_env:
|
|
34
|
+
# 提取命令的基本名称 (例如 /usr/bin/vim -> vim)
|
|
35
|
+
editor_basename = os.path.basename(editor_from_env)
|
|
36
|
+
# 检查基本名称是否在允许列表中,并且该命令确实存在于 PATH 中
|
|
37
|
+
if editor_basename in ALLOWED_EDITORS and shutil.which(editor_from_env):
|
|
38
|
+
editor_to_use = editor_from_env
|
|
39
|
+
else:
|
|
40
|
+
console.print(f"[yellow]Warning:[/yellow] EDITOR environment variable ('{editor_from_env}') is not in the allowed list ({ALLOWED_EDITORS}) or not found. Falling back to 'vim'.")
|
|
41
|
+
# Fallback to default if validation fails
|
|
42
|
+
|
|
43
|
+
# 如果环境变量未设置或验证失败,则尝试默认的 'vim'
|
|
44
|
+
if editor_to_use is None:
|
|
45
|
+
if shutil.which("vim"):
|
|
46
|
+
editor_to_use = "vim"
|
|
47
|
+
else:
|
|
48
|
+
# 如果连 vim 都没有,则需要处理错误情况
|
|
49
|
+
console.print("[red]Error:[/red] Default editor 'vim' not found. Please install it or set a valid EDITOR environment variable from the allowed list.")
|
|
50
|
+
os.unlink(tf_name) # 清理临时文件
|
|
51
|
+
# 根据你的应用逻辑,这里可以抛出异常或返回原始消息
|
|
52
|
+
# raise RuntimeError("No valid editor found.")
|
|
53
|
+
return initial_message # 或者简单返回未编辑的消息
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
# 推荐使用 subprocess.run 替代 subprocess.call
|
|
57
|
+
# check=True 会在命令返回非零退出码时抛出 CalledProcessError 异常
|
|
58
|
+
subprocess.run([editor_to_use, tf_name], check=True)
|
|
59
|
+
except FileNotFoundError:
|
|
60
|
+
console.print(f"[red]Error:[/red] Editor command '{editor_to_use}' not found. Please check your installation and PATH.")
|
|
61
|
+
os.unlink(tf_name)
|
|
62
|
+
return initial_message # 返回未编辑的消息
|
|
63
|
+
except subprocess.CalledProcessError as e:
|
|
64
|
+
console.print(f"[red]Error:[/red] Editor '{editor_to_use}' exited with status {e.returncode}.")
|
|
65
|
+
# 即使编辑器出错,文件可能已被部分修改,但我们通常认为编辑失败
|
|
66
|
+
os.unlink(tf_name)
|
|
67
|
+
return initial_message # 返回未编辑的消息
|
|
68
|
+
except Exception as e:
|
|
69
|
+
console.print(f"[red]An unexpected error occurred while running the editor:[/red] {e}")
|
|
70
|
+
os.unlink(tf_name)
|
|
71
|
+
return initial_message # 返回未编辑的消息
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# 确保文件最终被读取和删除
|
|
75
|
+
edited_message = initial_message # 默认值以防文件读取失败
|
|
76
|
+
if os.path.exists(tf_name):
|
|
77
|
+
try:
|
|
78
|
+
with open(tf_name, "r") as tf:
|
|
79
|
+
edited_message = tf.read().strip()
|
|
80
|
+
except Exception as e:
|
|
81
|
+
console.print(f"[red]Error reading temporary file {tf_name}:[/red] {e}")
|
|
82
|
+
# 保留原始消息
|
|
83
|
+
finally:
|
|
84
|
+
os.unlink(tf_name) # 确保在任何情况下都尝试删除
|
|
85
|
+
else:
|
|
86
|
+
console.print(f"[yellow]Warning:[/yellow] Temporary file {tf_name} seems to have been deleted by the editor.")
|
|
87
|
+
# 在这种情况下,我们无法获取编辑后的消息,返回原始消息
|
|
88
|
+
edited_message = initial_message
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
return edited_message
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: ai_git_utils
|
|
3
|
+
Version: 0.4.2
|
|
4
|
+
Summary: AI git utils is an intelligent Git commit assistant that leverages AI to enhance your Git workflow.
|
|
5
|
+
Project-URL: Homepage, https://github.com/twn39/aigit
|
|
6
|
+
Project-URL: Documentation, https://github.com/twn39/aigit
|
|
7
|
+
Project-URL: Repository, https://github.com/twn39/aigit
|
|
8
|
+
Project-URL: Source Code, https://github.com/twn39/aigit
|
|
9
|
+
Author-email: curry tang <twn39@163.com>
|
|
10
|
+
Maintainer-email: curry tang <twn39@163.com>
|
|
11
|
+
Keywords: ai,aigit,git
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Requires-Dist: gitpython>=3.1.45
|
|
19
|
+
Requires-Dist: openai>=1.101.0
|
|
20
|
+
Requires-Dist: pip>=25.2
|
|
21
|
+
Requires-Dist: rich>=14.1.0
|
|
22
|
+
Requires-Dist: setuptools>=80.9.0
|
|
23
|
+
Requires-Dist: typer>=0.16.1
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# AI Git Utils: 智能 Git Commit 助手 🚀
|
|
27
|
+
|
|
28
|
+
[](https://badge.fury.io/py/ai-git-utils)
|
|
29
|
+
|
|
30
|
+
**AI Git Utils** 是一个利用 AI 技术增强您 Git 工作流程的智能化工具。它能够根据您的代码变更自动生成规范、清晰且富有表现力的 Commit Message,并智能推荐相关的 Emoji,显著提升版本控制的效率和体验。
|
|
31
|
+
|
|
32
|
+

|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## ✨ 核心特性
|
|
37
|
+
|
|
38
|
+
- **🤖 AI 驱动的 Commit Message 生成:** 基于代码 `diff`,智能生成符合 [Conventional Commits](https://www.conventionalcommits.org/) 规范的提交信息(包含 `type`, `scope`, `subject`, `body`)。
|
|
39
|
+
- **✍️ 交互式编辑:** 在 AI 生成建议后,提供便捷的交互式编辑界面,允许您轻松修改和确认最终的 Commit Message。
|
|
40
|
+
- **😄 智能 Emoji 选择:** 根据 Commit 类型自动推荐合适的 Emoji,让您的提交记录更生动、直观。
|
|
41
|
+
- **🔌 多模型支持与灵活配置:**
|
|
42
|
+
- 支持接入多种兼容 OpenAI API 标准的大语言模型 (LLM)。
|
|
43
|
+
- 通过简单的命令行指令即可添加、删除、切换和管理不同的 AI 模型配置。
|
|
44
|
+
- **📜 增强的 Git Log:** 使用 `aigit log` 命令,以美观的表格形式展示提交历史,支持限制数量和时间范围过滤。
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 🛠️ 安装
|
|
49
|
+
|
|
50
|
+
确保您已安装 Python 3.8+。然后通过 pip 安装:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install ai-git-utils
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## ⚙️ 配置 AI 模型
|
|
59
|
+
|
|
60
|
+
在使用 `aigit commit` 功能前,您需要至少配置一个 AI 模型。
|
|
61
|
+
|
|
62
|
+
1. **添加模型配置:**
|
|
63
|
+
运行 `aigit model add` 并根据提示输入模型信息:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
aigit model add
|
|
67
|
+
# Name: my-gpt4o (自定义模型名称)
|
|
68
|
+
# Model: gpt-4o (模型 ID)
|
|
69
|
+
# Base Url: https://api.openai.com/v1 (模型服务 API 地址)
|
|
70
|
+
# Temperature: 0.7 (模型温度参数)
|
|
71
|
+
# Api Key: sk-xxxx (您的 API 密钥)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
添加的第一个模型会自动设为当前激活模型。
|
|
75
|
+
|
|
76
|
+
2. **管理模型:**
|
|
77
|
+
- 列出所有已配置模型: `aigit model list`
|
|
78
|
+
- 查看当前激活模型的详细配置: `aigit model show`
|
|
79
|
+
- 激活其他已配置模型: `aigit model active` (根据提示输入名称)
|
|
80
|
+
- 删除指定模型配置: `aigit model remove` (根据提示输入名称)
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 🚀 使用指南
|
|
85
|
+
|
|
86
|
+
### 1. 生成 AI Commit Message (核心功能)
|
|
87
|
+
|
|
88
|
+
在您的 Git 仓库中,当您有暂存的更改 (staged changes) 时,运行:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
# 默认使用英文生成
|
|
92
|
+
aigit commit
|
|
93
|
+
|
|
94
|
+
# 指定使用中文生成
|
|
95
|
+
aigit commit --lang Chinese
|
|
96
|
+
|
|
97
|
+
# 只针对特定文件的更改生成 commit message
|
|
98
|
+
aigit commit --file path/to/your/file.py
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## 🤝 贡献
|
|
102
|
+
|
|
103
|
+
欢迎各种形式的贡献!如果您有任何建议、发现 Bug 或想改进功能,请随时:
|
|
104
|
+
|
|
105
|
+
1. Fork 本仓库
|
|
106
|
+
2. 创建您的特性分支 (`git checkout -b feature/AmazingFeature`)
|
|
107
|
+
3. 提交您的更改 (`aigit commit` 😉)
|
|
108
|
+
4. 推送到分支 (`git push origin feature/AmazingFeature`)
|
|
109
|
+
5. 提交 Pull Request
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
ai_git_utils/__init__.py,sha256=gALy2JYqB4lc5pnAe4itvsSN5dIu8x8-5lZpmSNTnzk,78
|
|
2
|
+
ai_git_utils/ai_model.py,sha256=EUUr8eLmwTdBbnIhoAYENh0TJOiZiruHx6EMgZ3pcp8,128
|
|
3
|
+
ai_git_utils/cli.py,sha256=ABoROjoenxDPr-ZhU2AeXWa_frG4zAZdGbRO0FpEfo4,10414
|
|
4
|
+
ai_git_utils/config_manager.py,sha256=dkaJrL4WiJyNQD3VzlttvlhdNfmmELrARKqcHwXncqg,1439
|
|
5
|
+
ai_git_utils/git_operations.py,sha256=faiwp7LrfOzxkdUWdgUHhU24JdFlV6KT3HtPQseW9j0,565
|
|
6
|
+
ai_git_utils/main.py,sha256=Qd-f8z2Q2vpiEP2x6PBFsJrpACWDVxFKQk820MhFmHo,59
|
|
7
|
+
ai_git_utils/utils.py,sha256=1_VRY5vAuiYpsbROsYmTNkfJwzQ9Qi-bYrtmCQiRAHI,3855
|
|
8
|
+
ai_git_utils-0.4.2.dist-info/METADATA,sha256=ZkgGBg90Xp7IFFkzoI8oKl3nRqMa4VqNT1mDkRqt9mE,4047
|
|
9
|
+
ai_git_utils-0.4.2.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
|
|
10
|
+
ai_git_utils-0.4.2.dist-info/entry_points.txt,sha256=Sn1zgttLjMOqSGZM1wEVGiD49g6_CG9tcKlkBdfK6Ds,48
|
|
11
|
+
ai_git_utils-0.4.2.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
ai_git_utils-0.4.2.dist-info/RECORD,,
|
|
File without changes
|