auto-coder-web 0.1.16__py3-none-any.whl → 0.1.18__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.
- auto_coder_web/auto_coder_runner.py +1 -46
- auto_coder_web/auto_coder_runner_wrapper.py +51 -0
- auto_coder_web/proxy.py +24 -10
- auto_coder_web/routers/__init__.py +3 -0
- auto_coder_web/routers/auto_router.py +281 -0
- auto_coder_web/routers/commit_router.py +647 -0
- auto_coder_web/version.py +1 -1
- auto_coder_web/web/asset-manifest.json +6 -6
- auto_coder_web/web/index.html +1 -1
- auto_coder_web/web/static/css/main.57a3bfdf.css +6 -0
- auto_coder_web/web/static/css/main.57a3bfdf.css.map +1 -0
- auto_coder_web/web/static/js/main.27be9d6e.js +3 -0
- auto_coder_web/web/static/js/main.27be9d6e.js.LICENSE.txt +164 -0
- auto_coder_web/web/static/js/main.27be9d6e.js.map +1 -0
- {auto_coder_web-0.1.16.dist-info → auto_coder_web-0.1.18.dist-info}/METADATA +2 -2
- {auto_coder_web-0.1.16.dist-info → auto_coder_web-0.1.18.dist-info}/RECORD +19 -11
- {auto_coder_web-0.1.16.dist-info → auto_coder_web-0.1.18.dist-info}/WHEEL +0 -0
- {auto_coder_web-0.1.16.dist-info → auto_coder_web-0.1.18.dist-info}/entry_points.txt +0 -0
- {auto_coder_web-0.1.16.dist-info → auto_coder_web-0.1.18.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,647 @@
|
|
1
|
+
import os
|
2
|
+
import re
|
3
|
+
from datetime import datetime, timedelta
|
4
|
+
from typing import Dict, List, Optional, Any
|
5
|
+
|
6
|
+
from fastapi import APIRouter, HTTPException, Request, Depends, Query
|
7
|
+
from pydantic import BaseModel
|
8
|
+
from loguru import logger
|
9
|
+
import git
|
10
|
+
from git import Repo, GitCommandError
|
11
|
+
|
12
|
+
# 导入获取事件和action文件的相关模块
|
13
|
+
from autocoder.events.event_manager_singleton import get_event_manager, get_event_file_path
|
14
|
+
from autocoder.events.event_types import EventType
|
15
|
+
from autocoder.common.action_yml_file_manager import ActionYmlFileManager
|
16
|
+
|
17
|
+
router = APIRouter()
|
18
|
+
|
19
|
+
|
20
|
+
class CommitDetail(BaseModel):
|
21
|
+
hash: str
|
22
|
+
short_hash: str
|
23
|
+
author: str
|
24
|
+
date: str
|
25
|
+
message: str
|
26
|
+
stats: Dict[str, int]
|
27
|
+
files: Optional[List[Dict[str, Any]]] = None
|
28
|
+
|
29
|
+
|
30
|
+
async def get_project_path(request: Request) -> str:
|
31
|
+
"""
|
32
|
+
从FastAPI请求上下文中获取项目路径
|
33
|
+
"""
|
34
|
+
return request.app.state.project_path
|
35
|
+
|
36
|
+
|
37
|
+
def get_repo(project_path: str) -> Repo:
|
38
|
+
"""
|
39
|
+
获取Git仓库对象
|
40
|
+
"""
|
41
|
+
try:
|
42
|
+
return Repo(project_path)
|
43
|
+
except (git.NoSuchPathError, git.InvalidGitRepositoryError) as e:
|
44
|
+
logger.error(f"Git repository error: {str(e)}")
|
45
|
+
raise HTTPException(
|
46
|
+
status_code=404,
|
47
|
+
detail="No Git repository found in the project path"
|
48
|
+
)
|
49
|
+
|
50
|
+
|
51
|
+
@router.get("/api/commits")
|
52
|
+
async def get_commits(
|
53
|
+
limit: int = 50,
|
54
|
+
skip: int = 0,
|
55
|
+
project_path: str = Depends(get_project_path)
|
56
|
+
):
|
57
|
+
"""
|
58
|
+
获取Git提交历史
|
59
|
+
|
60
|
+
Args:
|
61
|
+
limit: 返回的最大提交数量,默认50
|
62
|
+
skip: 跳过的提交数量,用于分页
|
63
|
+
project_path: 项目路径
|
64
|
+
|
65
|
+
Returns:
|
66
|
+
提交列表
|
67
|
+
"""
|
68
|
+
try:
|
69
|
+
repo = get_repo(project_path)
|
70
|
+
commits = []
|
71
|
+
|
72
|
+
# 获取提交列表
|
73
|
+
for i, commit in enumerate(repo.iter_commits()):
|
74
|
+
if i < skip:
|
75
|
+
continue
|
76
|
+
if len(commits) >= limit:
|
77
|
+
break
|
78
|
+
|
79
|
+
# 获取提交统计信息
|
80
|
+
stats = commit.stats.total
|
81
|
+
|
82
|
+
# 构建提交信息
|
83
|
+
commit_info = {
|
84
|
+
"hash": commit.hexsha,
|
85
|
+
"short_hash": commit.hexsha[:7],
|
86
|
+
"author": f"{commit.author.name} <{commit.author.email}>",
|
87
|
+
"date": datetime.fromtimestamp(commit.committed_date).isoformat(),
|
88
|
+
"message": commit.message.strip(),
|
89
|
+
"stats": {
|
90
|
+
"insertions": stats["insertions"],
|
91
|
+
"deletions": stats["deletions"],
|
92
|
+
"files_changed": stats["files"]
|
93
|
+
}
|
94
|
+
}
|
95
|
+
commits.append(commit_info)
|
96
|
+
|
97
|
+
return {"commits": commits, "total": len(list(repo.iter_commits()))}
|
98
|
+
except Exception as e:
|
99
|
+
logger.error(f"Error getting commits: {str(e)}")
|
100
|
+
raise HTTPException(
|
101
|
+
status_code=500,
|
102
|
+
detail=f"Failed to get commits: {str(e)}"
|
103
|
+
)
|
104
|
+
|
105
|
+
|
106
|
+
@router.get("/api/commits/{commit_hash}")
|
107
|
+
async def get_commit_detail(
|
108
|
+
commit_hash: str,
|
109
|
+
project_path: str = Depends(get_project_path)
|
110
|
+
):
|
111
|
+
"""
|
112
|
+
获取特定提交的详细信息
|
113
|
+
|
114
|
+
Args:
|
115
|
+
commit_hash: 提交哈希值
|
116
|
+
project_path: 项目路径
|
117
|
+
|
118
|
+
Returns:
|
119
|
+
提交详情
|
120
|
+
"""
|
121
|
+
try:
|
122
|
+
repo = get_repo(project_path)
|
123
|
+
|
124
|
+
# 尝试获取指定的提交
|
125
|
+
try:
|
126
|
+
commit = repo.commit(commit_hash)
|
127
|
+
except ValueError:
|
128
|
+
# 如果是短哈希,尝试匹配
|
129
|
+
matching_commits = [c for c in repo.iter_commits() if c.hexsha.startswith(commit_hash)]
|
130
|
+
if not matching_commits:
|
131
|
+
raise HTTPException(status_code=404, detail=f"Commit {commit_hash} not found")
|
132
|
+
commit = matching_commits[0]
|
133
|
+
|
134
|
+
# 获取提交统计信息
|
135
|
+
stats = commit.stats.total
|
136
|
+
|
137
|
+
# 获取变更的文件列表
|
138
|
+
changed_files = []
|
139
|
+
diff_index = commit.diff(commit.parents[0] if commit.parents else git.NULL_TREE)
|
140
|
+
|
141
|
+
for diff in diff_index:
|
142
|
+
file_path = diff.a_path if diff.a_path else diff.b_path
|
143
|
+
|
144
|
+
# 确定文件状态
|
145
|
+
if diff.new_file:
|
146
|
+
status = "added"
|
147
|
+
elif diff.deleted_file:
|
148
|
+
status = "deleted"
|
149
|
+
elif diff.renamed:
|
150
|
+
status = "renamed"
|
151
|
+
else:
|
152
|
+
status = "modified"
|
153
|
+
|
154
|
+
# 获取文件级别的变更统计
|
155
|
+
file_stats = None
|
156
|
+
for filename, file_stat in commit.stats.files.items():
|
157
|
+
norm_filename = filename.replace('/', os.sep)
|
158
|
+
if norm_filename == file_path or filename == file_path:
|
159
|
+
file_stats = file_stat
|
160
|
+
break
|
161
|
+
|
162
|
+
file_info = {
|
163
|
+
"filename": file_path,
|
164
|
+
"status": status,
|
165
|
+
}
|
166
|
+
|
167
|
+
if file_stats:
|
168
|
+
file_info["changes"] = {
|
169
|
+
"insertions": file_stats["insertions"],
|
170
|
+
"deletions": file_stats["deletions"],
|
171
|
+
}
|
172
|
+
|
173
|
+
changed_files.append(file_info)
|
174
|
+
|
175
|
+
# 构建详细的提交信息
|
176
|
+
commit_detail = {
|
177
|
+
"hash": commit.hexsha,
|
178
|
+
"short_hash": commit.hexsha[:7],
|
179
|
+
"author": f"{commit.author.name} <{commit.author.email}>",
|
180
|
+
"date": datetime.fromtimestamp(commit.committed_date).isoformat(),
|
181
|
+
"message": commit.message.strip(),
|
182
|
+
"stats": {
|
183
|
+
"insertions": stats["insertions"],
|
184
|
+
"deletions": stats["deletions"],
|
185
|
+
"files_changed": stats["files"]
|
186
|
+
},
|
187
|
+
"files": changed_files
|
188
|
+
}
|
189
|
+
|
190
|
+
return commit_detail
|
191
|
+
except HTTPException:
|
192
|
+
raise
|
193
|
+
except IndexError:
|
194
|
+
# 处理没有父提交的情况(首次提交)
|
195
|
+
raise HTTPException(status_code=404, detail=f"Commit {commit_hash} has no parent commit")
|
196
|
+
except Exception as e:
|
197
|
+
logger.error(f"Error getting commit detail: {str(e)}")
|
198
|
+
raise HTTPException(
|
199
|
+
status_code=500,
|
200
|
+
detail=f"Failed to get commit detail: {str(e)}"
|
201
|
+
)
|
202
|
+
|
203
|
+
|
204
|
+
@router.get("/api/commit/action")
|
205
|
+
async def get_action_from_commit_msg(
|
206
|
+
commit_msg: str,
|
207
|
+
project_path: str = Depends(get_project_path)
|
208
|
+
):
|
209
|
+
"""
|
210
|
+
从提交消息中获取对应的 action 文件名和内容
|
211
|
+
|
212
|
+
Args:
|
213
|
+
commit_msg: 提交消息
|
214
|
+
project_path: 项目路径
|
215
|
+
|
216
|
+
Returns:
|
217
|
+
action 文件名和内容
|
218
|
+
"""
|
219
|
+
try:
|
220
|
+
# 初始化 ActionYmlFileManager
|
221
|
+
action_manager = ActionYmlFileManager(project_path)
|
222
|
+
|
223
|
+
# 从提交消息中获取文件名
|
224
|
+
file_name = action_manager.get_file_name_from_commit_msg(commit_msg)
|
225
|
+
|
226
|
+
if not file_name:
|
227
|
+
raise HTTPException(
|
228
|
+
status_code=404,
|
229
|
+
detail="No action file found in commit message"
|
230
|
+
)
|
231
|
+
|
232
|
+
# 使用 ActionYmlFileManager 的 load_yaml_content 方法读取文件内容
|
233
|
+
content = action_manager.load_yaml_content(file_name)
|
234
|
+
|
235
|
+
if not content:
|
236
|
+
logger.warning(f"Empty or invalid YAML content in file {file_name}")
|
237
|
+
raise HTTPException(
|
238
|
+
status_code=404,
|
239
|
+
detail=f"No valid content found in action file {file_name}"
|
240
|
+
)
|
241
|
+
|
242
|
+
return {
|
243
|
+
"file_name": file_name,
|
244
|
+
"content": content
|
245
|
+
}
|
246
|
+
except HTTPException:
|
247
|
+
raise
|
248
|
+
except Exception as e:
|
249
|
+
logger.error(f"Error getting action from commit message: {str(e)}")
|
250
|
+
raise HTTPException(
|
251
|
+
status_code=500,
|
252
|
+
detail=f"Failed to get action from commit message: {str(e)}"
|
253
|
+
)
|
254
|
+
|
255
|
+
|
256
|
+
@router.get("/api/branches")
|
257
|
+
async def get_branches(project_path: str = Depends(get_project_path)):
|
258
|
+
"""
|
259
|
+
获取Git分支列表
|
260
|
+
|
261
|
+
Args:
|
262
|
+
project_path: 项目路径
|
263
|
+
|
264
|
+
Returns:
|
265
|
+
分支列表
|
266
|
+
"""
|
267
|
+
try:
|
268
|
+
repo = get_repo(project_path)
|
269
|
+
branches = []
|
270
|
+
|
271
|
+
current_branch = repo.active_branch.name
|
272
|
+
|
273
|
+
for branch in repo.branches:
|
274
|
+
branches.append({
|
275
|
+
"name": branch.name,
|
276
|
+
"is_current": branch.name == current_branch,
|
277
|
+
"commit": {
|
278
|
+
"hash": branch.commit.hexsha,
|
279
|
+
"short_hash": branch.commit.hexsha[:7],
|
280
|
+
"message": branch.commit.message.strip()
|
281
|
+
}
|
282
|
+
})
|
283
|
+
|
284
|
+
return {"branches": branches, "current": current_branch}
|
285
|
+
except Exception as e:
|
286
|
+
logger.error(f"Error getting branches: {str(e)}")
|
287
|
+
raise HTTPException(
|
288
|
+
status_code=500,
|
289
|
+
detail=f"Failed to get branches: {str(e)}"
|
290
|
+
)
|
291
|
+
|
292
|
+
|
293
|
+
@router.get("/api/commits/{commit_hash}/file")
|
294
|
+
async def get_file_diff(
|
295
|
+
commit_hash: str,
|
296
|
+
file_path: str,
|
297
|
+
project_path: str = Depends(get_project_path)
|
298
|
+
):
|
299
|
+
"""
|
300
|
+
获取特定提交中特定文件的变更前后内容和差异
|
301
|
+
|
302
|
+
Args:
|
303
|
+
commit_hash: 提交哈希值
|
304
|
+
file_path: 文件路径
|
305
|
+
project_path: 项目路径
|
306
|
+
|
307
|
+
Returns:
|
308
|
+
文件变更前后内容和差异
|
309
|
+
"""
|
310
|
+
try:
|
311
|
+
repo = get_repo(project_path)
|
312
|
+
|
313
|
+
# 尝试获取指定的提交
|
314
|
+
try:
|
315
|
+
commit = repo.commit(commit_hash)
|
316
|
+
except ValueError:
|
317
|
+
# 如果是短哈希,尝试匹配
|
318
|
+
matching_commits = [c for c in repo.iter_commits() if c.hexsha.startswith(commit_hash)]
|
319
|
+
if not matching_commits:
|
320
|
+
raise HTTPException(status_code=404, detail=f"Commit {commit_hash} not found")
|
321
|
+
commit = matching_commits[0]
|
322
|
+
|
323
|
+
# 处理父提交,如果没有父提交(初始提交)
|
324
|
+
if not commit.parents:
|
325
|
+
# 如果是新增文件
|
326
|
+
if file_path in [item.path for item in commit.tree.traverse() if item.type == 'blob']:
|
327
|
+
file_content = repo.git.show(f"{commit.hexsha}:{file_path}")
|
328
|
+
return {
|
329
|
+
"before_content": "", # 初始提交前没有内容
|
330
|
+
"after_content": file_content,
|
331
|
+
"diff_content": repo.git.show(f"{commit.hexsha} -- {file_path}")
|
332
|
+
}
|
333
|
+
else:
|
334
|
+
raise HTTPException(status_code=404, detail=f"File {file_path} not found in commit {commit_hash}")
|
335
|
+
|
336
|
+
# 获取父提交
|
337
|
+
parent = commit.parents[0]
|
338
|
+
|
339
|
+
# 检查文件是否存在于当前提交
|
340
|
+
file_in_commit = False
|
341
|
+
try:
|
342
|
+
after_content = repo.git.show(f"{commit.hexsha}:{file_path}")
|
343
|
+
file_in_commit = True
|
344
|
+
except git.GitCommandError:
|
345
|
+
after_content = "" # 文件在当前提交中不存在(可能被删除)
|
346
|
+
|
347
|
+
# 检查文件是否存在于父提交
|
348
|
+
file_in_parent = False
|
349
|
+
try:
|
350
|
+
before_content = repo.git.show(f"{parent.hexsha}:{file_path}")
|
351
|
+
file_in_parent = True
|
352
|
+
except git.GitCommandError:
|
353
|
+
before_content = "" # 文件在父提交中不存在(新增文件)
|
354
|
+
|
355
|
+
# 获取文件差异
|
356
|
+
try:
|
357
|
+
diff_content = repo.git.diff(f"{parent.hexsha}..{commit.hexsha}", "--", file_path)
|
358
|
+
except git.GitCommandError:
|
359
|
+
# 如果无法直接获取差异,可能是重命名或其他特殊情况
|
360
|
+
diff_content = ""
|
361
|
+
|
362
|
+
# 尝试查找可能的重命名
|
363
|
+
diff_index = parent.diff(commit)
|
364
|
+
for diff_item in diff_index:
|
365
|
+
if diff_item.renamed:
|
366
|
+
if diff_item.b_path == file_path: # 重命名后的路径匹配
|
367
|
+
before_content = repo.git.show(f"{parent.hexsha}:{diff_item.a_path}")
|
368
|
+
diff_content = repo.git.diff(f"{parent.hexsha}..{commit.hexsha}", "--", diff_item.a_path, file_path)
|
369
|
+
break
|
370
|
+
elif diff_item.a_path == file_path: # 重命名前的路径匹配
|
371
|
+
after_content = repo.git.show(f"{commit.hexsha}:{diff_item.b_path}")
|
372
|
+
diff_content = repo.git.diff(f"{parent.hexsha}..{commit.hexsha}", "--", file_path, diff_item.b_path)
|
373
|
+
break
|
374
|
+
|
375
|
+
# 检查我们是否找到了内容
|
376
|
+
if not file_in_commit and not file_in_parent:
|
377
|
+
raise HTTPException(status_code=404, detail=f"File {file_path} not found in commit {commit_hash} or its parent")
|
378
|
+
|
379
|
+
return {
|
380
|
+
"before_content": before_content,
|
381
|
+
"after_content": after_content,
|
382
|
+
"diff_content": diff_content,
|
383
|
+
"file_status": "added" if not file_in_parent else "deleted" if not file_in_commit else "modified"
|
384
|
+
}
|
385
|
+
|
386
|
+
except HTTPException:
|
387
|
+
raise
|
388
|
+
except Exception as e:
|
389
|
+
logger.error(f"Error getting file diff: {str(e)}")
|
390
|
+
raise HTTPException(
|
391
|
+
status_code=500,
|
392
|
+
detail=f"Failed to get file diff: {str(e)}"
|
393
|
+
)
|
394
|
+
|
395
|
+
|
396
|
+
@router.get("/api/current-changes")
|
397
|
+
async def get_current_changes(
|
398
|
+
limit: int = 3,
|
399
|
+
hours_ago: int = 24,
|
400
|
+
event_file_id: Optional[str] = None,
|
401
|
+
project_path: str = Depends(get_project_path)
|
402
|
+
):
|
403
|
+
"""
|
404
|
+
获取当前变更的提交列表
|
405
|
+
|
406
|
+
有两种模式:
|
407
|
+
1. 如果提供了event_file_id,则从事件文件中提取相关的提交
|
408
|
+
2. 如果没有提供event_file_id,则返回最近的提交
|
409
|
+
|
410
|
+
Args:
|
411
|
+
limit: 返回的最大提交数量,默认3
|
412
|
+
hours_ago: 从几小时前开始查找,默认24小时
|
413
|
+
event_file_id: 事件文件ID,可选
|
414
|
+
project_path: 项目路径
|
415
|
+
|
416
|
+
Returns:
|
417
|
+
提交哈希列表
|
418
|
+
"""
|
419
|
+
try:
|
420
|
+
repo = get_repo(project_path)
|
421
|
+
|
422
|
+
# 如果提供了事件文件ID,从事件中获取相关提交
|
423
|
+
if event_file_id:
|
424
|
+
try:
|
425
|
+
# 获取事件文件路径
|
426
|
+
event_file_path = get_event_file_path(event_file_id, project_path)
|
427
|
+
|
428
|
+
# 获取事件管理器
|
429
|
+
event_manager = get_event_manager(event_file_path)
|
430
|
+
|
431
|
+
# 获取所有事件
|
432
|
+
all_events = event_manager.event_store.get_events()
|
433
|
+
logger.info(f"获取事件: {len(all_events)}")
|
434
|
+
|
435
|
+
# 创建ActionYmlFileManager实例
|
436
|
+
action_manager = ActionYmlFileManager(project_path)
|
437
|
+
|
438
|
+
action_files = set()
|
439
|
+
final_action_files = []
|
440
|
+
|
441
|
+
for event in all_events:
|
442
|
+
# 检查元数据中是否有action_file字段
|
443
|
+
if 'action_file' in event.metadata and event.metadata['action_file']:
|
444
|
+
action_file = event.metadata['action_file']
|
445
|
+
if action_file in action_files:
|
446
|
+
continue
|
447
|
+
|
448
|
+
action_files.add(action_file)
|
449
|
+
# 从action文件获取提交ID
|
450
|
+
# action_file 这里的值是 类似这样的 actions/000000000104_chat_action.yml
|
451
|
+
if action_file.startswith("actions"):
|
452
|
+
action_file = action_file[len("actions/"):]
|
453
|
+
|
454
|
+
final_action_files.append(action_file)
|
455
|
+
|
456
|
+
commits = []
|
457
|
+
for action_file in final_action_files:
|
458
|
+
commit_msg_part = action_manager.get_commit_id_from_file(action_file)
|
459
|
+
commit_id = None
|
460
|
+
logger.info(f"获取Action文件提交ID: {commit_msg_part}")
|
461
|
+
for commit in repo.iter_commits():
|
462
|
+
if commit_msg_part in commit.message:
|
463
|
+
commit_id = commit.hexsha
|
464
|
+
break
|
465
|
+
|
466
|
+
if commit_id:
|
467
|
+
# 验证提交ID是否存在于仓库中
|
468
|
+
try:
|
469
|
+
commit = repo.commit(commit_id)
|
470
|
+
# 获取提交统计信息
|
471
|
+
stats = commit.stats.total
|
472
|
+
|
473
|
+
# 构建提交信息
|
474
|
+
commit_info = {
|
475
|
+
"hash": commit.hexsha,
|
476
|
+
"short_hash": commit.hexsha[:7],
|
477
|
+
"author": f"{commit.author.name} <{commit.author.email}>",
|
478
|
+
"date": datetime.fromtimestamp(commit.committed_date).isoformat(),
|
479
|
+
"timestamp": commit.committed_date,
|
480
|
+
"message": commit.message.strip(),
|
481
|
+
"stats": {
|
482
|
+
"insertions": stats["insertions"],
|
483
|
+
"deletions": stats["deletions"],
|
484
|
+
"files_changed": stats["files"]
|
485
|
+
}
|
486
|
+
}
|
487
|
+
commits.append(commit_info)
|
488
|
+
|
489
|
+
return {"commits": commits, "total": len(list(repo.iter_commits()))}
|
490
|
+
except Exception as e:
|
491
|
+
logger.warning(f"无法获取提交 {commit_id}: {str(e)}")
|
492
|
+
|
493
|
+
# 按提交时间戳排序(降序 - 最新的在前面)
|
494
|
+
commits.sort(key=lambda x: x['timestamp'], reverse=True)
|
495
|
+
|
496
|
+
|
497
|
+
return {"commits": commits, "total": len(commits)}
|
498
|
+
|
499
|
+
except Exception as e:
|
500
|
+
logger.error(f"从事件文件获取提交失败: {str(e)}")
|
501
|
+
|
502
|
+
return {"commits": [], "total": 0}
|
503
|
+
else:
|
504
|
+
# 如果没有提供事件文件ID,返回最近的提交
|
505
|
+
return {"commits": [], "total": 0}
|
506
|
+
|
507
|
+
except Exception as e:
|
508
|
+
logger.error(f"获取当前变更失败: {str(e)}")
|
509
|
+
raise HTTPException(
|
510
|
+
status_code=500,
|
511
|
+
detail=f"获取当前变更失败: {str(e)}"
|
512
|
+
)
|
513
|
+
|
514
|
+
async def get_recent_commits(repo: Repo, limit: int, hours_ago: int):
|
515
|
+
"""
|
516
|
+
获取最近的提交
|
517
|
+
|
518
|
+
Args:
|
519
|
+
repo: Git仓库对象
|
520
|
+
limit: 最大提交数量
|
521
|
+
hours_ago: 时间范围(小时)
|
522
|
+
|
523
|
+
Returns:
|
524
|
+
最近的提交哈希列表,按时间降序排序
|
525
|
+
"""
|
526
|
+
# 计算时间范围
|
527
|
+
since_time = datetime.now() - timedelta(hours=hours_ago)
|
528
|
+
since_timestamp = since_time.timestamp()
|
529
|
+
|
530
|
+
# 获取最近的提交,带时间戳
|
531
|
+
commit_data = []
|
532
|
+
|
533
|
+
for commit in repo.iter_commits():
|
534
|
+
if commit.committed_date >= since_timestamp:
|
535
|
+
commit_data.append({
|
536
|
+
'hash': commit.hexsha,
|
537
|
+
'timestamp': commit.committed_date
|
538
|
+
})
|
539
|
+
if len(commit_data) >= limit:
|
540
|
+
break
|
541
|
+
|
542
|
+
# 按时间戳排序(降序 - 最新的在前面)
|
543
|
+
commit_data.sort(key=lambda x: x['timestamp'], reverse=True)
|
544
|
+
|
545
|
+
# 提取排序后的哈希值,并去重(保持顺序)
|
546
|
+
seen_hashes = set()
|
547
|
+
commit_hashes_list = []
|
548
|
+
for item in commit_data:
|
549
|
+
if item['hash'] not in seen_hashes:
|
550
|
+
seen_hashes.add(item['hash'])
|
551
|
+
commit_hashes_list.append(item['hash'])
|
552
|
+
|
553
|
+
return {"commit_hashes": commit_hashes_list}
|
554
|
+
|
555
|
+
@router.post("/api/commits/{commit_hash}/revert")
|
556
|
+
async def revert_commit(
|
557
|
+
commit_hash: str,
|
558
|
+
project_path: str = Depends(get_project_path)
|
559
|
+
):
|
560
|
+
"""
|
561
|
+
撤销指定的提交,创建一个新的 revert 提交
|
562
|
+
|
563
|
+
Args:
|
564
|
+
commit_hash: 要撤销的提交哈希值
|
565
|
+
project_path: 项目路径
|
566
|
+
|
567
|
+
Returns:
|
568
|
+
新创建的 revert 提交信息
|
569
|
+
"""
|
570
|
+
try:
|
571
|
+
repo = get_repo(project_path)
|
572
|
+
|
573
|
+
# 尝试获取指定的提交
|
574
|
+
try:
|
575
|
+
commit = repo.commit(commit_hash)
|
576
|
+
except ValueError:
|
577
|
+
# 如果是短哈希,尝试匹配
|
578
|
+
matching_commits = [c for c in repo.iter_commits() if c.hexsha.startswith(commit_hash)]
|
579
|
+
if not matching_commits:
|
580
|
+
raise HTTPException(status_code=404, detail=f"Commit {commit_hash} not found")
|
581
|
+
commit = matching_commits[0]
|
582
|
+
|
583
|
+
# 检查工作目录是否干净
|
584
|
+
if repo.is_dirty():
|
585
|
+
raise HTTPException(
|
586
|
+
status_code=400,
|
587
|
+
detail="Cannot revert: working directory has uncommitted changes"
|
588
|
+
)
|
589
|
+
|
590
|
+
try:
|
591
|
+
# 执行 git revert
|
592
|
+
# 使用 -n 选项不自动创建提交,而是让我们手动提交
|
593
|
+
repo.git.revert(commit.hexsha, no_commit=True)
|
594
|
+
|
595
|
+
# 创建带有信息的 revert 提交
|
596
|
+
revert_message = f"Revert \"{commit.message.strip()}\"\n\nThis reverts commit {commit.hexsha}"
|
597
|
+
new_commit = repo.index.commit(
|
598
|
+
revert_message,
|
599
|
+
author=repo.active_branch.commit.author,
|
600
|
+
committer=repo.active_branch.commit.committer
|
601
|
+
)
|
602
|
+
|
603
|
+
# 构建新提交的信息
|
604
|
+
stats = new_commit.stats.total
|
605
|
+
new_commit_info = {
|
606
|
+
"new_commit_hash": new_commit.hexsha,
|
607
|
+
"new_commit_short_hash": new_commit.hexsha[:7],
|
608
|
+
"reverted_commit": {
|
609
|
+
"hash": commit.hexsha,
|
610
|
+
"short_hash": commit.hexsha[:7],
|
611
|
+
"message": commit.message.strip()
|
612
|
+
},
|
613
|
+
"stats": {
|
614
|
+
"insertions": stats["insertions"],
|
615
|
+
"deletions": stats["deletions"],
|
616
|
+
"files_changed": stats["files"]
|
617
|
+
}
|
618
|
+
}
|
619
|
+
|
620
|
+
return new_commit_info
|
621
|
+
|
622
|
+
except git.GitCommandError as e:
|
623
|
+
# 如果发生 Git 命令错误,尝试恢复工作目录
|
624
|
+
try:
|
625
|
+
repo.git.reset("--hard", "HEAD")
|
626
|
+
except:
|
627
|
+
pass # 如果恢复失败,继续抛出原始错误
|
628
|
+
|
629
|
+
if "patch does not apply" in str(e):
|
630
|
+
raise HTTPException(
|
631
|
+
status_code=409,
|
632
|
+
detail="Cannot revert: patch does not apply (likely due to conflicts)"
|
633
|
+
)
|
634
|
+
else:
|
635
|
+
raise HTTPException(
|
636
|
+
status_code=500,
|
637
|
+
detail=f"Git error during revert: {str(e)}"
|
638
|
+
)
|
639
|
+
|
640
|
+
except HTTPException:
|
641
|
+
raise
|
642
|
+
except Exception as e:
|
643
|
+
logger.error(f"Error reverting commit: {str(e)}")
|
644
|
+
raise HTTPException(
|
645
|
+
status_code=500,
|
646
|
+
detail=f"Failed to revert commit: {str(e)}"
|
647
|
+
)
|
auto_coder_web/version.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "0.1.
|
1
|
+
__version__ = "0.1.18"
|
@@ -1,15 +1,15 @@
|
|
1
1
|
{
|
2
2
|
"files": {
|
3
|
-
"main.css": "/static/css/main.
|
4
|
-
"main.js": "/static/js/main.
|
3
|
+
"main.css": "/static/css/main.57a3bfdf.css",
|
4
|
+
"main.js": "/static/js/main.27be9d6e.js",
|
5
5
|
"static/js/453.d855a71b.chunk.js": "/static/js/453.d855a71b.chunk.js",
|
6
6
|
"index.html": "/index.html",
|
7
|
-
"main.
|
8
|
-
"main.
|
7
|
+
"main.57a3bfdf.css.map": "/static/css/main.57a3bfdf.css.map",
|
8
|
+
"main.27be9d6e.js.map": "/static/js/main.27be9d6e.js.map",
|
9
9
|
"453.d855a71b.chunk.js.map": "/static/js/453.d855a71b.chunk.js.map"
|
10
10
|
},
|
11
11
|
"entrypoints": [
|
12
|
-
"static/css/main.
|
13
|
-
"static/js/main.
|
12
|
+
"static/css/main.57a3bfdf.css",
|
13
|
+
"static/js/main.27be9d6e.js"
|
14
14
|
]
|
15
15
|
}
|
auto_coder_web/web/index.html
CHANGED
@@ -1 +1 @@
|
|
1
|
-
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site created using create-react-app"/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>React App</title><script defer="defer" src="/static/js/main.
|
1
|
+
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site created using create-react-app"/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>React App</title><script defer="defer" src="/static/js/main.27be9d6e.js"></script><link href="/static/css/main.57a3bfdf.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|