55
66from beeai_framework .context import RunContext
77from beeai_framework .emitter import Emitter
8- from beeai_framework .tools import JSONToolOutput , Tool , ToolRunOptions
8+ from beeai_framework .tools import StringToolOutput , Tool , ToolRunOptions
99
1010
1111class 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" )
2414
2515
2616async 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]:
4030 "stderr" : stderr .decode () if stderr else None ,
4131 }
4232
43- class GitPatchCreationTool (Tool [GitPatchCreationToolInput , ToolRunOptions , GitPatchCreationToolOutput ]):
33+ class GitPatchCreationTool (Tool [GitPatchCreationToolInput , ToolRunOptions , StringToolOutput ]):
4434 name = "git_patch_create"
4535 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.
5039 """
5140 input_schema = GitPatchCreationToolInput
5241
@@ -58,117 +47,64 @@ def _create_emitter(self) -> Emitter:
5847
5948 async def _run (
6049 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