auto-coder 0.1.363__py3-none-any.whl → 0.1.364__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.
Potentially problematic release.
This version of auto-coder might be problematic. Click here for more details.
- {auto_coder-0.1.363.dist-info → auto_coder-0.1.364.dist-info}/METADATA +2 -2
- {auto_coder-0.1.363.dist-info → auto_coder-0.1.364.dist-info}/RECORD +33 -18
- autocoder/agent/base_agentic/tools/execute_command_tool_resolver.py +1 -1
- autocoder/auto_coder_runner.py +2 -0
- autocoder/common/__init__.py +2 -0
- autocoder/common/file_checkpoint/__init__.py +21 -0
- autocoder/common/file_checkpoint/backup.py +264 -0
- autocoder/common/file_checkpoint/examples.py +217 -0
- autocoder/common/file_checkpoint/manager.py +404 -0
- autocoder/common/file_checkpoint/models.py +156 -0
- autocoder/common/file_checkpoint/store.py +383 -0
- autocoder/common/file_checkpoint/test_backup.py +242 -0
- autocoder/common/file_checkpoint/test_manager.py +570 -0
- autocoder/common/file_checkpoint/test_models.py +360 -0
- autocoder/common/file_checkpoint/test_store.py +327 -0
- autocoder/common/file_checkpoint/test_utils.py +297 -0
- autocoder/common/file_checkpoint/utils.py +119 -0
- autocoder/common/rulefiles/autocoderrules_utils.py +114 -55
- autocoder/common/save_formatted_log.py +76 -5
- autocoder/common/v2/agent/agentic_edit.py +318 -197
- autocoder/common/v2/agent/agentic_edit_tools/read_file_tool_resolver.py +2 -2
- autocoder/common/v2/agent/agentic_edit_tools/replace_in_file_tool_resolver.py +27 -4
- autocoder/common/v2/agent/agentic_edit_tools/test_write_to_file_tool_resolver.py +322 -0
- autocoder/common/v2/agent/agentic_edit_tools/write_to_file_tool_resolver.py +83 -61
- autocoder/compilers/normal_compiler.py +64 -0
- autocoder/events/event_manager_singleton.py +133 -4
- autocoder/linters/normal_linter.py +373 -0
- autocoder/linters/python_linter.py +4 -2
- autocoder/version.py +1 -1
- {auto_coder-0.1.363.dist-info → auto_coder-0.1.364.dist-info}/LICENSE +0 -0
- {auto_coder-0.1.363.dist-info → auto_coder-0.1.364.dist-info}/WHEEL +0 -0
- {auto_coder-0.1.363.dist-info → auto_coder-0.1.364.dist-info}/entry_points.txt +0 -0
- {auto_coder-0.1.363.dist-info → auto_coder-0.1.364.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""
|
|
2
|
+
文件变更管理模块的使用示例
|
|
3
|
+
|
|
4
|
+
展示如何使用文件变更管理模块进行文件变更的应用和撤销。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import json
|
|
10
|
+
from typing import Dict, Any
|
|
11
|
+
|
|
12
|
+
from autocoder.common.file_checkpoint.models import FileChange
|
|
13
|
+
from autocoder.common.file_checkpoint.manager import FileChangeManager
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def example_apply_changes(project_dir: str):
|
|
17
|
+
"""
|
|
18
|
+
示例:应用文件变更
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
project_dir: 项目目录
|
|
22
|
+
"""
|
|
23
|
+
print(f"示例:应用文件变更到项目 {project_dir}")
|
|
24
|
+
|
|
25
|
+
# 创建文件变更管理器
|
|
26
|
+
manager = FileChangeManager(project_dir)
|
|
27
|
+
|
|
28
|
+
# 准备文件变更
|
|
29
|
+
changes = {
|
|
30
|
+
"example.txt": FileChange(
|
|
31
|
+
file_path="example.txt",
|
|
32
|
+
content="这是一个示例文件\n用于演示文件变更管理模块的功能\n",
|
|
33
|
+
is_new=True
|
|
34
|
+
),
|
|
35
|
+
"README.md": FileChange(
|
|
36
|
+
file_path="README.md",
|
|
37
|
+
content="# 示例项目\n\n这是一个用于演示文件变更管理模块的示例项目。\n",
|
|
38
|
+
is_new=True
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# 应用变更
|
|
43
|
+
result = manager.apply_changes(changes)
|
|
44
|
+
|
|
45
|
+
# 输出结果
|
|
46
|
+
if result.success:
|
|
47
|
+
print(f"成功应用了 {len(result.change_ids)} 个文件变更")
|
|
48
|
+
for change_id in result.change_ids:
|
|
49
|
+
print(f" - 变更ID: {change_id}")
|
|
50
|
+
else:
|
|
51
|
+
print("应用变更失败")
|
|
52
|
+
for file_path, error in result.errors.items():
|
|
53
|
+
print(f" - {file_path}: {error}")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def example_preview_changes(project_dir: str):
|
|
57
|
+
"""
|
|
58
|
+
示例:预览文件变更
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
project_dir: 项目目录
|
|
62
|
+
"""
|
|
63
|
+
print(f"示例:预览文件变更")
|
|
64
|
+
|
|
65
|
+
# 创建文件变更管理器
|
|
66
|
+
manager = FileChangeManager(project_dir)
|
|
67
|
+
|
|
68
|
+
# 准备文件变更
|
|
69
|
+
changes = {
|
|
70
|
+
"example.txt": FileChange(
|
|
71
|
+
file_path="example.txt",
|
|
72
|
+
content="这是一个修改后的示例文件\n用于演示文件变更管理模块的功能\n新增的一行\n",
|
|
73
|
+
is_new=False
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# 预览变更
|
|
78
|
+
diff_results = manager.preview_changes(changes)
|
|
79
|
+
|
|
80
|
+
# 输出差异
|
|
81
|
+
for file_path, diff_result in diff_results.items():
|
|
82
|
+
print(f"\n文件: {file_path}")
|
|
83
|
+
print(diff_result.get_diff_summary())
|
|
84
|
+
if diff_result.old_content is not None and not diff_result.is_new and not diff_result.is_deletion:
|
|
85
|
+
diff_text = manager.get_diff_text(diff_result.old_content, diff_result.new_content)
|
|
86
|
+
print("\n差异:")
|
|
87
|
+
print(diff_text)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def example_undo_changes(project_dir: str):
|
|
91
|
+
"""
|
|
92
|
+
示例:撤销文件变更
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
project_dir: 项目目录
|
|
96
|
+
"""
|
|
97
|
+
print(f"示例:撤销文件变更")
|
|
98
|
+
|
|
99
|
+
# 创建文件变更管理器
|
|
100
|
+
manager = FileChangeManager(project_dir)
|
|
101
|
+
|
|
102
|
+
# 撤销最近的变更
|
|
103
|
+
result = manager.undo_last_change()
|
|
104
|
+
|
|
105
|
+
# 输出结果
|
|
106
|
+
if result.success:
|
|
107
|
+
print(f"成功撤销了变更,恢复了 {len(result.restored_files)} 个文件")
|
|
108
|
+
for file_path in result.restored_files:
|
|
109
|
+
print(f" - {file_path}")
|
|
110
|
+
else:
|
|
111
|
+
print("撤销变更失败")
|
|
112
|
+
for file_path, error in result.errors.items():
|
|
113
|
+
print(f" - {file_path}: {error}")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def example_get_history(project_dir: str):
|
|
117
|
+
"""
|
|
118
|
+
示例:获取变更历史
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
project_dir: 项目目录
|
|
122
|
+
"""
|
|
123
|
+
print(f"示例:获取变更历史")
|
|
124
|
+
|
|
125
|
+
# 创建文件变更管理器
|
|
126
|
+
manager = FileChangeManager(project_dir)
|
|
127
|
+
|
|
128
|
+
# 获取变更历史
|
|
129
|
+
changes = manager.get_change_history(limit=5)
|
|
130
|
+
|
|
131
|
+
# 输出历史记录
|
|
132
|
+
print(f"最近 {len(changes)} 条变更记录:")
|
|
133
|
+
for change in changes:
|
|
134
|
+
timestamp = change.timestamp
|
|
135
|
+
file_path = change.file_path
|
|
136
|
+
change_type = "新建" if change.is_new else "删除" if change.is_deletion else "修改"
|
|
137
|
+
print(f" - [{timestamp}] {change_type} {file_path} (ID: {change.change_id})")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def example_integration_with_agentic_edit():
|
|
141
|
+
"""
|
|
142
|
+
示例:与 AgenticEdit 集成
|
|
143
|
+
|
|
144
|
+
展示如何将文件变更管理模块集成到 AgenticEdit 中
|
|
145
|
+
"""
|
|
146
|
+
print("示例:与 AgenticEdit 集成")
|
|
147
|
+
|
|
148
|
+
# 这是一个伪代码示例,展示如何修改 AgenticEdit.apply_changes 方法
|
|
149
|
+
code = """
|
|
150
|
+
def apply_changes(self):
|
|
151
|
+
\"\"\"
|
|
152
|
+
Apply all tracked file changes to the original project directory.
|
|
153
|
+
\"\"\"
|
|
154
|
+
from autocoder.common.file_checkpoint.models import FileChange
|
|
155
|
+
from autocoder.common.file_checkpoint.manager import FileChangeManager
|
|
156
|
+
|
|
157
|
+
# 创建文件变更管理器
|
|
158
|
+
manager = FileChangeManager(self.args.source_dir)
|
|
159
|
+
|
|
160
|
+
# 将影子系统的变更转换为 FileChange 对象
|
|
161
|
+
changes = {}
|
|
162
|
+
for file_path, change in self.get_all_file_changes().items():
|
|
163
|
+
changes[file_path] = FileChange(
|
|
164
|
+
file_path=file_path,
|
|
165
|
+
content=change.content,
|
|
166
|
+
is_new=not os.path.exists(file_path)
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# 应用变更
|
|
170
|
+
result = manager.apply_changes(changes)
|
|
171
|
+
|
|
172
|
+
# 处理结果
|
|
173
|
+
if result.success:
|
|
174
|
+
# 继续执行原有的 Git 提交等逻辑
|
|
175
|
+
if not self.args.skip_commit:
|
|
176
|
+
try:
|
|
177
|
+
# ... 原有的 Git 提交代码 ...
|
|
178
|
+
except Exception as e:
|
|
179
|
+
# ... 原有的错误处理 ...
|
|
180
|
+
else:
|
|
181
|
+
# 处理应用变更失败的情况
|
|
182
|
+
error_messages = "\\n".join([f"{path}: {error}" for path, error in result.errors.items()])
|
|
183
|
+
self.printer.print_str_in_terminal(
|
|
184
|
+
f"Failed to apply changes:\\n{error_messages}",
|
|
185
|
+
style="red"
|
|
186
|
+
)
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
print(code)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def main():
|
|
193
|
+
"""主函数"""
|
|
194
|
+
if len(sys.argv) < 2:
|
|
195
|
+
print("用法: python examples.py <项目目录>")
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
project_dir = sys.argv[1]
|
|
199
|
+
|
|
200
|
+
# 运行示例
|
|
201
|
+
example_apply_changes(project_dir)
|
|
202
|
+
print("\n" + "-" * 50 + "\n")
|
|
203
|
+
|
|
204
|
+
example_preview_changes(project_dir)
|
|
205
|
+
print("\n" + "-" * 50 + "\n")
|
|
206
|
+
|
|
207
|
+
example_get_history(project_dir)
|
|
208
|
+
print("\n" + "-" * 50 + "\n")
|
|
209
|
+
|
|
210
|
+
example_undo_changes(project_dir)
|
|
211
|
+
print("\n" + "-" * 50 + "\n")
|
|
212
|
+
|
|
213
|
+
example_integration_with_agentic_edit()
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
if __name__ == "__main__":
|
|
217
|
+
main()
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
"""
|
|
2
|
+
文件变更管理器
|
|
3
|
+
|
|
4
|
+
整个模块的主入口,提供高层次的API接口,用于应用、记录和撤销文件变更。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import uuid
|
|
9
|
+
import logging
|
|
10
|
+
import difflib
|
|
11
|
+
from typing import Dict, List, Optional, Tuple, Any
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
|
|
14
|
+
from autocoder.common.file_checkpoint.models import (
|
|
15
|
+
FileChange, ChangeRecord, ApplyResult, UndoResult, DiffResult
|
|
16
|
+
)
|
|
17
|
+
from autocoder.common.file_checkpoint.backup import FileBackupManager
|
|
18
|
+
from autocoder.common.file_checkpoint.store import FileChangeStore
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class FileChangeManager:
|
|
24
|
+
"""文件变更管理器,提供高层次的API接口"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, project_dir: str, backup_dir: Optional[str] = None,
|
|
27
|
+
store_dir: Optional[str] = None, max_history: int = 50):
|
|
28
|
+
"""
|
|
29
|
+
初始化文件变更管理器
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
project_dir: 用户项目的根目录
|
|
33
|
+
backup_dir: 备份文件存储目录,默认为用户主目录下的.autocoder/backups
|
|
34
|
+
store_dir: 变更记录存储目录,默认为用户主目录下的.autocoder/changes
|
|
35
|
+
max_history: 最大保存的历史版本数量
|
|
36
|
+
"""
|
|
37
|
+
self.project_dir = os.path.abspath(project_dir)
|
|
38
|
+
self.backup_manager = FileBackupManager(backup_dir)
|
|
39
|
+
self.change_store = FileChangeStore(store_dir, max_history)
|
|
40
|
+
|
|
41
|
+
def apply_changes(self, changes: Dict[str, FileChange], change_group_id: Optional[str] = None) -> ApplyResult:
|
|
42
|
+
"""
|
|
43
|
+
应用一组文件变更
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
changes: 文件变更字典,格式为 {file_path: FileChange}
|
|
47
|
+
change_group_id: 变更组ID,用于将相关变更归为一组
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
ApplyResult: 应用结果对象
|
|
51
|
+
"""
|
|
52
|
+
# 如果没有提供变更组ID,生成一个新的
|
|
53
|
+
if change_group_id is None:
|
|
54
|
+
change_group_id = str(uuid.uuid4())
|
|
55
|
+
|
|
56
|
+
result = ApplyResult(success=True)
|
|
57
|
+
|
|
58
|
+
# 处理每个文件的变更
|
|
59
|
+
for file_path, change in changes.items():
|
|
60
|
+
try:
|
|
61
|
+
# 获取文件的绝对路径
|
|
62
|
+
abs_file_path = self._get_absolute_path(file_path)
|
|
63
|
+
|
|
64
|
+
# 确定文件是否是新文件
|
|
65
|
+
is_new = not os.path.exists(abs_file_path)
|
|
66
|
+
|
|
67
|
+
# 备份原文件(如果存在)
|
|
68
|
+
backup_id = None
|
|
69
|
+
if not is_new and not change.is_deletion:
|
|
70
|
+
backup_id = self.backup_manager.backup_file(abs_file_path)
|
|
71
|
+
|
|
72
|
+
# 处理文件变更
|
|
73
|
+
if change.is_deletion:
|
|
74
|
+
# 如果是删除操作,先备份再删除
|
|
75
|
+
backup_id = self.backup_manager.backup_file(abs_file_path)
|
|
76
|
+
if os.path.exists(abs_file_path):
|
|
77
|
+
os.remove(abs_file_path)
|
|
78
|
+
else:
|
|
79
|
+
# 确保目录存在
|
|
80
|
+
dir_path = os.path.dirname(abs_file_path)
|
|
81
|
+
if dir_path:
|
|
82
|
+
os.makedirs(dir_path, exist_ok=True)
|
|
83
|
+
|
|
84
|
+
# 写入文件内容
|
|
85
|
+
with open(abs_file_path, 'w', encoding='utf-8') as f:
|
|
86
|
+
f.write(change.content)
|
|
87
|
+
|
|
88
|
+
# 创建变更记录
|
|
89
|
+
change_record = ChangeRecord.create(
|
|
90
|
+
file_path=file_path,
|
|
91
|
+
backup_id=backup_id,
|
|
92
|
+
is_new=is_new,
|
|
93
|
+
is_deletion=change.is_deletion,
|
|
94
|
+
group_id=change_group_id
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# 保存变更记录
|
|
98
|
+
change_id = self.change_store.save_change(change_record)
|
|
99
|
+
result.add_change_id(change_id)
|
|
100
|
+
|
|
101
|
+
logger.info(f"已应用变更到文件 {file_path}")
|
|
102
|
+
|
|
103
|
+
except Exception as e:
|
|
104
|
+
error_message = f"应用变更到文件 {file_path} 失败: {str(e)}"
|
|
105
|
+
logger.error(error_message)
|
|
106
|
+
result.add_error(file_path, error_message)
|
|
107
|
+
|
|
108
|
+
return result
|
|
109
|
+
|
|
110
|
+
def preview_changes(self, changes: Dict[str, FileChange]) -> Dict[str, DiffResult]:
|
|
111
|
+
"""
|
|
112
|
+
预览变更的差异
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
changes: 文件变更字典
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Dict[str, DiffResult]: 每个文件的差异结果
|
|
119
|
+
"""
|
|
120
|
+
diff_results = {}
|
|
121
|
+
|
|
122
|
+
for file_path, change in changes.items():
|
|
123
|
+
try:
|
|
124
|
+
# 获取文件的绝对路径
|
|
125
|
+
abs_file_path = self._get_absolute_path(file_path)
|
|
126
|
+
|
|
127
|
+
# 确定文件是否是新文件
|
|
128
|
+
is_new = not os.path.exists(abs_file_path)
|
|
129
|
+
|
|
130
|
+
# 获取原文件内容
|
|
131
|
+
old_content = None
|
|
132
|
+
if not is_new and not change.is_deletion:
|
|
133
|
+
try:
|
|
134
|
+
with open(abs_file_path, 'r', encoding='utf-8') as f:
|
|
135
|
+
old_content = f.read()
|
|
136
|
+
except Exception as e:
|
|
137
|
+
logger.error(f"读取文件 {abs_file_path} 失败: {str(e)}")
|
|
138
|
+
|
|
139
|
+
# 创建差异结果
|
|
140
|
+
diff_result = DiffResult(
|
|
141
|
+
file_path=file_path,
|
|
142
|
+
old_content=old_content,
|
|
143
|
+
new_content=change.content,
|
|
144
|
+
is_new=is_new,
|
|
145
|
+
is_deletion=change.is_deletion
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
diff_results[file_path] = diff_result
|
|
149
|
+
|
|
150
|
+
except Exception as e:
|
|
151
|
+
logger.error(f"预览文件 {file_path} 的变更差异失败: {str(e)}")
|
|
152
|
+
|
|
153
|
+
return diff_results
|
|
154
|
+
|
|
155
|
+
def undo_last_change(self) -> UndoResult:
|
|
156
|
+
"""
|
|
157
|
+
撤销最近的一次变更
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
UndoResult: 撤销结果对象
|
|
161
|
+
"""
|
|
162
|
+
# 获取最近的变更记录
|
|
163
|
+
latest_changes = self.change_store.get_latest_changes(limit=1)
|
|
164
|
+
if not latest_changes:
|
|
165
|
+
return UndoResult(success=False, errors={"general": "没有找到最近的变更记录"})
|
|
166
|
+
|
|
167
|
+
latest_change = latest_changes[0]
|
|
168
|
+
|
|
169
|
+
# 如果最近的变更属于一个组,撤销整个组
|
|
170
|
+
if latest_change.group_id:
|
|
171
|
+
return self.undo_change_group(latest_change.group_id)
|
|
172
|
+
else:
|
|
173
|
+
# 否则只撤销这一个变更
|
|
174
|
+
return self.undo_change(latest_change.change_id)
|
|
175
|
+
|
|
176
|
+
def undo_change(self, change_id: str) -> UndoResult:
|
|
177
|
+
"""
|
|
178
|
+
撤销指定的变更
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
change_id: 变更记录ID
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
UndoResult: 撤销结果对象
|
|
185
|
+
"""
|
|
186
|
+
# 获取变更记录
|
|
187
|
+
change_record = self.change_store.get_change(change_id)
|
|
188
|
+
if change_record is None:
|
|
189
|
+
return UndoResult(success=False, errors={"general": f"变更记录 {change_id} 不存在"})
|
|
190
|
+
|
|
191
|
+
result = UndoResult(success=True)
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
# 获取文件的绝对路径
|
|
195
|
+
abs_file_path = self._get_absolute_path(change_record.file_path)
|
|
196
|
+
|
|
197
|
+
# 根据变更类型执行撤销操作
|
|
198
|
+
if change_record.is_new:
|
|
199
|
+
# 如果是新建文件的变更,删除该文件
|
|
200
|
+
if os.path.exists(abs_file_path):
|
|
201
|
+
os.remove(abs_file_path)
|
|
202
|
+
result.add_restored_file(change_record.file_path)
|
|
203
|
+
elif change_record.is_deletion:
|
|
204
|
+
# 如果是删除文件的变更,从备份恢复
|
|
205
|
+
if change_record.backup_id:
|
|
206
|
+
success = self.backup_manager.restore_file(abs_file_path, change_record.backup_id)
|
|
207
|
+
if success:
|
|
208
|
+
result.add_restored_file(change_record.file_path)
|
|
209
|
+
else:
|
|
210
|
+
result.add_error(change_record.file_path, "从备份恢复文件失败")
|
|
211
|
+
else:
|
|
212
|
+
result.add_error(change_record.file_path, "没有找到文件备份")
|
|
213
|
+
else:
|
|
214
|
+
# 如果是修改文件的变更,从备份恢复
|
|
215
|
+
if change_record.backup_id:
|
|
216
|
+
success = self.backup_manager.restore_file(abs_file_path, change_record.backup_id)
|
|
217
|
+
if success:
|
|
218
|
+
result.add_restored_file(change_record.file_path)
|
|
219
|
+
else:
|
|
220
|
+
result.add_error(change_record.file_path, "从备份恢复文件失败")
|
|
221
|
+
else:
|
|
222
|
+
result.add_error(change_record.file_path, "没有找到文件备份")
|
|
223
|
+
|
|
224
|
+
# 删除变更记录
|
|
225
|
+
self.change_store.delete_change(change_id)
|
|
226
|
+
|
|
227
|
+
logger.info(f"已撤销变更 {change_id}")
|
|
228
|
+
|
|
229
|
+
except Exception as e:
|
|
230
|
+
error_message = f"撤销变更 {change_id} 失败: {str(e)}"
|
|
231
|
+
logger.error(error_message)
|
|
232
|
+
result.add_error(change_record.file_path, error_message)
|
|
233
|
+
result.success = False
|
|
234
|
+
|
|
235
|
+
return result
|
|
236
|
+
|
|
237
|
+
def undo_change_group(self, group_id: str) -> UndoResult:
|
|
238
|
+
"""
|
|
239
|
+
撤销指定组的所有变更
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
group_id: 变更组ID
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
UndoResult: 撤销结果对象
|
|
246
|
+
"""
|
|
247
|
+
# 获取组内的所有变更记录
|
|
248
|
+
changes = self.change_store.get_changes_by_group(group_id)
|
|
249
|
+
if not changes:
|
|
250
|
+
return UndoResult(success=False, errors={"general": f"变更组 {group_id} 不存在或为空"})
|
|
251
|
+
|
|
252
|
+
result = UndoResult(success=True)
|
|
253
|
+
|
|
254
|
+
# 按时间戳降序排序,确保按照相反的顺序撤销
|
|
255
|
+
changes.sort(key=lambda x: x.timestamp, reverse=True)
|
|
256
|
+
|
|
257
|
+
# 逐个撤销变更
|
|
258
|
+
for change in changes:
|
|
259
|
+
change_result = self.undo_change(change.change_id)
|
|
260
|
+
|
|
261
|
+
# 合并结果
|
|
262
|
+
result.success = result.success and change_result.success
|
|
263
|
+
result.restored_files.extend(change_result.restored_files)
|
|
264
|
+
result.errors.update(change_result.errors)
|
|
265
|
+
|
|
266
|
+
return result
|
|
267
|
+
|
|
268
|
+
def undo_to_version(self, version_id: str) -> UndoResult:
|
|
269
|
+
"""
|
|
270
|
+
撤销到指定的历史版本
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
version_id: 目标版本ID(变更记录ID)
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
UndoResult: 撤销结果对象
|
|
277
|
+
"""
|
|
278
|
+
# 获取目标版本的变更记录
|
|
279
|
+
target_change = self.change_store.get_change(version_id)
|
|
280
|
+
if target_change is None:
|
|
281
|
+
return UndoResult(success=False, errors={"general": f"变更记录 {version_id} 不存在"})
|
|
282
|
+
|
|
283
|
+
# 获取最近的变更记录
|
|
284
|
+
latest_changes = self.change_store.get_latest_changes()
|
|
285
|
+
if not latest_changes:
|
|
286
|
+
return UndoResult(success=False, errors={"general": "没有找到最近的变更记录"})
|
|
287
|
+
|
|
288
|
+
# 找出需要撤销的变更记录
|
|
289
|
+
changes_to_undo = []
|
|
290
|
+
for change in latest_changes:
|
|
291
|
+
if change.timestamp > target_change.timestamp:
|
|
292
|
+
changes_to_undo.append(change)
|
|
293
|
+
|
|
294
|
+
if not changes_to_undo:
|
|
295
|
+
return UndoResult(success=True, restored_files=[])
|
|
296
|
+
|
|
297
|
+
result = UndoResult(success=True)
|
|
298
|
+
|
|
299
|
+
# 按时间戳降序排序,确保按照相反的顺序撤销
|
|
300
|
+
changes_to_undo.sort(key=lambda x: x.timestamp, reverse=True)
|
|
301
|
+
|
|
302
|
+
# 逐个撤销变更
|
|
303
|
+
for change in changes_to_undo:
|
|
304
|
+
change_result = self.undo_change(change.change_id)
|
|
305
|
+
|
|
306
|
+
# 合并结果
|
|
307
|
+
result.success = result.success and change_result.success
|
|
308
|
+
result.restored_files.extend(change_result.restored_files)
|
|
309
|
+
result.errors.update(change_result.errors)
|
|
310
|
+
|
|
311
|
+
return result
|
|
312
|
+
|
|
313
|
+
def get_change_history(self, limit: int = 10) -> List[ChangeRecord]:
|
|
314
|
+
"""
|
|
315
|
+
获取变更历史记录
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
limit: 返回的历史记录数量限制
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
List[ChangeRecord]: 变更记录列表
|
|
322
|
+
"""
|
|
323
|
+
return self.change_store.get_latest_changes(limit)
|
|
324
|
+
|
|
325
|
+
def get_file_history(self, file_path: str, limit: int = 10) -> List[ChangeRecord]:
|
|
326
|
+
"""
|
|
327
|
+
获取指定文件的变更历史
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
file_path: 文件路径
|
|
331
|
+
limit: 返回的历史记录数量限制
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
List[ChangeRecord]: 变更记录列表
|
|
335
|
+
"""
|
|
336
|
+
return self.change_store.get_changes_by_file(file_path, limit)
|
|
337
|
+
|
|
338
|
+
def get_changes_by_group(self, group_id: str) -> List[ChangeRecord]:
|
|
339
|
+
"""
|
|
340
|
+
获取指定变更组的所有变更记录
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
group_id: 变更组ID
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
List[ChangeRecord]: 变更记录列表
|
|
347
|
+
"""
|
|
348
|
+
return self.change_store.get_changes_by_group(group_id)
|
|
349
|
+
|
|
350
|
+
def get_change_groups(self, limit: int = 10) -> List[Tuple[str, float, int]]:
|
|
351
|
+
"""
|
|
352
|
+
获取变更组列表
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
limit: 返回的组数量限制
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
List[Tuple[str, float, int]]: 变更组ID、最新时间戳和变更数量的列表
|
|
359
|
+
"""
|
|
360
|
+
return self.change_store.get_change_groups(limit)
|
|
361
|
+
|
|
362
|
+
def get_diff_text(self, old_content: str, new_content: str) -> str:
|
|
363
|
+
"""
|
|
364
|
+
获取两个文本内容的差异文本
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
old_content: 原始内容
|
|
368
|
+
new_content: 新内容
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
str: 差异文本
|
|
372
|
+
"""
|
|
373
|
+
if old_content is None:
|
|
374
|
+
return "新文件"
|
|
375
|
+
|
|
376
|
+
if new_content is None:
|
|
377
|
+
return "文件已删除"
|
|
378
|
+
|
|
379
|
+
old_lines = old_content.splitlines()
|
|
380
|
+
new_lines = new_content.splitlines()
|
|
381
|
+
|
|
382
|
+
diff = difflib.unified_diff(
|
|
383
|
+
old_lines,
|
|
384
|
+
new_lines,
|
|
385
|
+
lineterm='',
|
|
386
|
+
n=3 # 上下文行数
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
return '\n'.join(diff)
|
|
390
|
+
|
|
391
|
+
def _get_absolute_path(self, file_path: str) -> str:
|
|
392
|
+
"""
|
|
393
|
+
获取文件的绝对路径
|
|
394
|
+
|
|
395
|
+
Args:
|
|
396
|
+
file_path: 文件相对路径或绝对路径
|
|
397
|
+
|
|
398
|
+
Returns:
|
|
399
|
+
str: 文件的绝对路径
|
|
400
|
+
"""
|
|
401
|
+
if os.path.isabs(file_path):
|
|
402
|
+
return file_path
|
|
403
|
+
else:
|
|
404
|
+
return os.path.join(self.project_dir, file_path)
|