5
5
6
6
from beeai_framework .context import RunContext
7
7
from beeai_framework .emitter import Emitter
8
- from beeai_framework .tools import JSONToolOutput , Tool , ToolRunOptions
8
+ from beeai_framework .tools import StringToolOutput , Tool , ToolRunOptions
9
9
10
10
11
11
class GitPatchCreationToolInput (BaseModel ):
12
- repository_path : Path = Field (description = "Absolute path to the git repository" )
13
- patch_file_path : Path = Field (description = "Absolute path where the patch file should be saved" )
14
-
15
-
16
- class GitPatchCreationToolResult (BaseModel ):
17
- success : bool = Field (description = "Whether the patch creation was successful" )
18
- patch_file_path : str = Field (description = "Path to the created patch file" )
19
- error : str | None = Field (description = "Error message if patch creation failed" , default = None )
20
-
21
-
22
- class GitPatchCreationToolOutput (JSONToolOutput [GitPatchCreationToolResult ]):
23
- """ Returns a dictionary with success or error and the path to the created patch file. """
12
+ repository_path : str = Field (description = "Absolute path to the git repository" )
13
+ patch_file_path : str = Field (description = "Absolute path where the patch file should be saved" )
24
14
25
15
26
16
async def run_command (cmd : list [str ], cwd : Path ) -> dict [str , str | int ]:
@@ -40,13 +30,12 @@ async def run_command(cmd: list[str], cwd: Path) -> dict[str, str | int]:
40
30
"stderr" : stderr .decode () if stderr else None ,
41
31
}
42
32
43
- class GitPatchCreationTool (Tool [GitPatchCreationToolInput , ToolRunOptions , GitPatchCreationToolOutput ]):
33
+ class GitPatchCreationTool (Tool [GitPatchCreationToolInput , ToolRunOptions , StringToolOutput ]):
44
34
name = "git_patch_create"
45
35
description = """
46
- Creates a patch file from the specified git repository with an active git-am session
47
- and after you resolved all merge conflicts. The tool generates a patch file that can be
48
- applied later in the RPM build process. Returns a dictionary with success or error and
49
- the path to the created patch file.
36
+ Creates a patch file from the specified git repository with an active git-am session.
37
+ The tool expects you resolved all conflicts. It generates a patch file that can be
38
+ applied later in the RPM build process.
50
39
"""
51
40
input_schema = GitPatchCreationToolInput
52
41
@@ -58,117 +47,64 @@ def _create_emitter(self) -> Emitter:
58
47
59
48
async def _run (
60
49
self , tool_input : GitPatchCreationToolInput , options : ToolRunOptions | None , context : RunContext
61
- ) -> GitPatchCreationToolOutput :
62
- # Ensure the repository path exists and is a git repository
63
- if not tool_input .repository_path .exists ():
64
- return GitPatchCreationToolOutput (
65
- result = GitPatchCreationToolResult (
66
- success = False ,
67
- patch_file_path = "" ,
68
- patch_content = "" ,
69
- error = f"Repository path does not exist: { tool_input .repository_path } "
70
- )
71
- )
72
-
73
- git_dir = tool_input .repository_path / ".git"
74
- if not git_dir .exists ():
75
- return GitPatchCreationToolOutput (
76
- result = GitPatchCreationToolResult (
77
- success = False ,
78
- patch_file_path = "" ,
79
- patch_content = "" ,
80
- error = f"Not a git repository: { tool_input .repository_path } "
81
- )
82
- )
83
-
84
- # list all untracked files in the repository
85
- cmd = ["git" , "ls-files" , "--others" , "--exclude-standard" ]
86
- result = await run_command (cmd , cwd = tool_input .repository_path )
87
- if result ["exit_code" ] != 0 :
88
- return GitPatchCreationToolOutput (
89
- result = GitPatchCreationToolResult (
90
- success = False ,
91
- patch_file_path = "" ,
92
- patch_content = "" ,
93
- error = f"Git command failed: { result ['stderr' ]} "
94
- )
95
- )
96
- untracked_files = result ["stdout" ].splitlines ()
97
- # list staged as well since that's what the agent usually does after it resolves conflicts
98
- cmd = ["git" , "diff" , "--name-only" , "--cached" ]
99
- result = await run_command (cmd , cwd = tool_input .repository_path )
100
- if result ["exit_code" ] != 0 :
101
- return GitPatchCreationToolOutput (
102
- result = GitPatchCreationToolResult (
103
- success = False ,
104
- patch_file_path = "" ,
105
- patch_content = "" ,
106
- error = f"Git command failed: { result ['stderr' ]} "
107
- )
108
- )
109
- staged_files = result ["stdout" ].splitlines ()
110
- all_files = untracked_files + staged_files
111
- # make sure there are no *.rej files in the repository
112
- rej_files = [file for file in all_files if file .endswith (".rej" )]
113
- if rej_files :
114
- return GitPatchCreationToolOutput (
115
- result = GitPatchCreationToolResult (
116
- success = False ,
117
- patch_file_path = "" ,
118
- patch_content = "" ,
119
- error = "Merge conflicts detected in the repository: "
120
- f"{ tool_input .repository_path } , { rej_files } "
121
- )
122
- )
123
-
124
- # git-am leaves the repository in a dirty state, so we need to stage everything
125
- # I considered to inspect the patch and only stage the files that are changed by the patch,
126
- # but the backport process could create new files or change new ones
127
- # so let's go the naive route: git add -A
128
- cmd = ["git" , "add" , "-A" ]
129
- result = await run_command (cmd , cwd = tool_input .repository_path )
130
- if result ["exit_code" ] != 0 :
131
- return GitPatchCreationToolOutput (
132
- result = GitPatchCreationToolResult (
133
- success = False ,
134
- patch_file_path = "" ,
135
- patch_content = "" ,
136
- error = f"Git command failed: { result ['stderr' ]} "
137
- )
138
- )
139
- # continue git-am process
140
- cmd = ["git" , "am" , "--continue" ]
141
- result = await run_command (cmd , cwd = tool_input .repository_path )
142
- if result ["exit_code" ] != 0 :
143
- return GitPatchCreationToolOutput (
144
- result = GitPatchCreationToolResult (
145
- success = False ,
146
- patch_file_path = "" ,
147
- patch_content = "" ,
148
- error = f"git-am failed: { result ['stderr' ]} , out={ result ['stdout' ]} "
149
- )
150
- )
151
- # good, now we should have the patch committed, so let's get the file
152
- cmd = [
153
- "git" , "format-patch" ,
154
- "--output" ,
155
- str (tool_input .patch_file_path ),
156
- "HEAD~1..HEAD"
157
- ]
158
- result = await run_command (cmd , cwd = tool_input .repository_path )
159
- if result ["exit_code" ] != 0 :
160
- return GitPatchCreationToolOutput (
161
- result = GitPatchCreationToolResult (
162
- success = False ,
163
- patch_file_path = "" ,
164
- patch_content = "" ,
165
- error = f"git-format-patch failed: { result ['stderr' ]} "
166
- )
167
- )
168
- return GitPatchCreationToolOutput (
169
- result = GitPatchCreationToolResult (
170
- success = True ,
171
- patch_file_path = str (tool_input .patch_file_path ),
172
- error = None
173
- )
174
- )
50
+ ) -> StringToolOutput :
51
+ try :
52
+ # Ensure the repository path exists and is a git repository
53
+ tool_input_path = Path (tool_input .repository_path )
54
+ if not tool_input_path .exists ():
55
+ return StringToolOutput (result = f"ERROR: Repository path does not exist: { tool_input_path } " )
56
+
57
+ git_dir = tool_input_path / ".git"
58
+ if not git_dir .exists ():
59
+ return StringToolOutput (result = f"ERROR: Not a git repository: { tool_input_path } " )
60
+
61
+ # list all untracked files in the repository
62
+ rej_candidates = []
63
+ cmd = ["git" , "ls-files" , "--others" , "--exclude-standard" ]
64
+ result = await run_command (cmd , cwd = tool_input_path )
65
+ if result ["exit_code" ] != 0 :
66
+ return StringToolOutput (result = f"ERROR: Git command failed: { result ['stderr' ]} " )
67
+ if result ["stdout" ]: # none means no untracked files
68
+ rej_candidates .extend (result ["stdout" ].splitlines ())
69
+ # list staged as well since that's what the agent usually does after it resolves conflicts
70
+ cmd = ["git" , "diff" , "--name-only" , "--cached" ]
71
+ result = await run_command (cmd , cwd = tool_input_path )
72
+ if result ["exit_code" ] != 0 :
73
+ return StringToolOutput (result = f"ERROR: Git command failed: { result ['stderr' ]} " )
74
+ if result ["stdout" ]:
75
+ rej_candidates .extend (result ["stdout" ].splitlines ())
76
+ if rej_candidates :
77
+ # make sure there are no *.rej files in the repository
78
+ rej_files = [file for file in rej_candidates if file .endswith (".rej" )]
79
+ if rej_files :
80
+ return StringToolOutput (result = f"ERROR: Merge conflicts detected in the repository: "
81
+ f"{ tool_input .repository_path } , { rej_files } " )
82
+
83
+ # git-am leaves the repository in a dirty state, so we need to stage everything
84
+ # I considered to inspect the patch and only stage the files that are changed by the patch,
85
+ # but the backport process could create new files or change new ones
86
+ # so let's go the naive route: git add -A
87
+ cmd = ["git" , "add" , "-A" ]
88
+ result = await run_command (cmd , cwd = tool_input_path )
89
+ if result ["exit_code" ] != 0 :
90
+ return StringToolOutput (result = f"ERROR: Git command failed: { result ['stderr' ]} " )
91
+ # continue git-am process
92
+ cmd = ["git" , "am" , "--continue" ]
93
+ result = await run_command (cmd , cwd = tool_input_path )
94
+ if result ["exit_code" ] != 0 :
95
+ return StringToolOutput (result = f"ERROR: git-am failed: { result ['stderr' ]} ,"
96
+ f" out={ result ['stdout' ]} " )
97
+ # good, now we should have the patch committed, so let's get the file
98
+ cmd = [
99
+ "git" , "format-patch" ,
100
+ "--output" ,
101
+ tool_input .patch_file_path ,
102
+ "HEAD~1..HEAD"
103
+ ]
104
+ result = await run_command (cmd , cwd = tool_input_path )
105
+ if result ["exit_code" ] != 0 :
106
+ return StringToolOutput (result = f"ERROR: git-format-patch failed: { result ['stderr' ]} " )
107
+ return StringToolOutput (result = f"Successfully created a patch file: { tool_input .patch_file_path } " )
108
+ except Exception as e :
109
+ # we absolutely need to do this otherwise the error won't appear anywhere
110
+ return StringToolOutput (result = f"ERROR: { e } " )
0 commit comments