Skip to content

Commit 0a42eb7

Browse files
authored
feat: add rename tool (#64)
1 parent c5db582 commit 0a42eb7

File tree

6 files changed

+108
-21
lines changed

6 files changed

+108
-21
lines changed

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,4 @@ pipx install git-draft[openai]
2323

2424
* Mechanism for reporting feedback from a bot, and possibly allowing user to
2525
interactively respond.
26-
* Support file rename tool.
2726
* Add MCP bot.

src/git_draft/__main__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,14 @@ def on_write_file(
156156
def on_delete_file(self, path: PurePosixPath, _reason: str | None) -> None:
157157
print(f"Deleted {path!r}.")
158158

159+
def on_rename_file(
160+
self,
161+
src_path: PurePosixPath,
162+
dst_path: PurePosixPath,
163+
_reason: str | None
164+
) -> None:
165+
print(f"Renamed {src_path!r} to {dst_path!r}.")
166+
159167

160168
def edit(*, path: Path | None = None, text: str | None = None) -> str:
161169
if sys.stdin.isatty():

src/git_draft/bots/openai.py

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,20 @@ def params(self) -> Sequence[openai.types.chat.ChatCompletionToolParam]:
127127
},
128128
},
129129
),
130+
self._param(
131+
name="rename_file",
132+
description="Rename a file",
133+
inputs={
134+
"src_path": {
135+
"type": "string",
136+
"description": "Old file path",
137+
},
138+
"dst_path": {
139+
"type": "string",
140+
"description": "New file path",
141+
},
142+
},
143+
),
130144
]
131145

132146

@@ -159,29 +173,39 @@ def _on_write_file(self, path: PurePosixPath) -> V:
159173
def _on_delete_file(self, path: PurePosixPath) -> V:
160174
raise NotImplementedError()
161175

176+
def _on_rename_file(
177+
self, src_path: PurePosixPath, dst_path: PurePosixPath
178+
) -> V:
179+
raise NotImplementedError()
180+
162181
def _on_list_files(self, paths: Sequence[PurePosixPath]) -> V:
163182
raise NotImplementedError()
164183

165184
def handle_function(self, function: Any) -> V:
166-
name = function.name
167185
inputs = json.loads(function.arguments)
168186
_logger.info("Requested function: %s", function)
169-
if name == "read_file":
170-
path = PurePosixPath(inputs["path"])
171-
return self._on_read_file(path, self._toolbox.read_file(path))
172-
elif name == "write_file":
173-
path = PurePosixPath(inputs["path"])
174-
contents = inputs["contents"]
175-
self._toolbox.write_file(path, contents)
176-
return self._on_write_file(path)
177-
elif name == "delete_file":
178-
path = PurePosixPath(inputs["path"])
179-
self._toolbox.delete_file(path)
180-
return self._on_delete_file(path)
181-
else:
182-
assert name == "list_files" and not inputs
183-
paths = self._toolbox.list_files()
184-
return self._on_list_files(paths)
187+
match function.name:
188+
case "read_file":
189+
path = PurePosixPath(inputs["path"])
190+
return self._on_read_file(path, self._toolbox.read_file(path))
191+
case "write_file":
192+
path = PurePosixPath(inputs["path"])
193+
contents = inputs["contents"]
194+
self._toolbox.write_file(path, contents)
195+
return self._on_write_file(path)
196+
case "delete_file":
197+
path = PurePosixPath(inputs["path"])
198+
self._toolbox.delete_file(path)
199+
return self._on_delete_file(path)
200+
case "rename_file":
201+
src_path = PurePosixPath(inputs["src_path"])
202+
dst_path = PurePosixPath(inputs["dst_path"])
203+
self._toolbox.rename_file(src_path, dst_path)
204+
return self._on_rename_file(src_path, dst_path)
205+
case _ as name:
206+
assert name == "list_files" and not inputs
207+
paths = self._toolbox.list_files()
208+
return self._on_list_files(paths)
185209

186210

187211
class _CompletionsBot(Bot):
@@ -234,6 +258,11 @@ def _on_write_file(self, _path: PurePosixPath) -> None:
234258
def _on_delete_file(self, _path: PurePosixPath) -> None:
235259
return None
236260

261+
def _on_rename_file(
262+
self, _src_path: PurePosixPath, _dst_path: PurePosixPath
263+
) -> None:
264+
return None
265+
237266
def _on_list_files(self, paths: Sequence[PurePosixPath]) -> str:
238267
joined = "\n".join(f"* {p}" for p in paths)
239268
return f"Here are the available files: {joined}"
@@ -360,5 +389,10 @@ def _on_write_file(self, _path: PurePosixPath) -> _ToolOutput:
360389
def _on_delete_file(self, _path: PurePosixPath) -> _ToolOutput:
361390
return self._wrap("OK")
362391

392+
def _on_rename_file(
393+
self, _src_path: PurePosixPath, _dst_path: PurePosixPath
394+
) -> _ToolOutput:
395+
return self._wrap("OK")
396+
363397
def _on_list_files(self, paths: Sequence[PurePosixPath]) -> _ToolOutput:
364398
return self._wrap("\n".join(str(p) for p in paths))

src/git_draft/drafter.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,19 @@ def on_write_file(
388388
def on_delete_file(self, path: PurePosixPath, reason: str | None) -> None:
389389
self._record(reason, "delete_file", path=str(path))
390390

391+
def on_rename_file(
392+
self,
393+
src_path: PurePosixPath,
394+
dst_path: PurePosixPath,
395+
reason: str | None,
396+
) -> None:
397+
self._record(
398+
reason,
399+
"rename_file",
400+
src_path=str(src_path),
401+
dst_path=str(dst_path),
402+
)
403+
391404
def _record(self, reason: str | None, tool: str, **kwargs) -> None:
392405
op = _Operation(
393406
tool=tool, details=kwargs, reason=reason, start=datetime.now()

src/git_draft/toolbox.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,15 @@ def delete_file(
6969
self._dispatch(lambda v: v.on_delete_file(path, reason))
7070
return self._delete(path)
7171

72+
def rename_file(
73+
self,
74+
src_path: PurePosixPath,
75+
dst_path: PurePosixPath,
76+
reason: str | None = None,
77+
) -> None:
78+
self._dispatch(lambda v: v.on_rename_file(src_path, dst_path, reason))
79+
self._rename(src_path, dst_path)
80+
7281
def _list(self) -> Sequence[PurePosixPath]: # pragma: no cover
7382
raise NotImplementedError()
7483

@@ -83,6 +92,14 @@ def _write(
8392
def _delete(self, path: PurePosixPath) -> bool: # pragma: no cover
8493
raise NotImplementedError()
8594

95+
def _rename(
96+
self, src_path: PurePosixPath, dst_path: PurePosixPath
97+
) -> None:
98+
# We can provide a default implementation here.
99+
contents = self._read(src_path)
100+
self._write(dst_path, contents)
101+
self._delete(src_path)
102+
86103

87104
class ToolVisitor(Protocol):
88105
"""Tool usage hook"""
@@ -103,6 +120,13 @@ def on_delete_file(
103120
self, path: PurePosixPath, reason: str | None
104121
) -> None: ... # pragma: no cover
105122

123+
def on_rename_file(
124+
self,
125+
src_path: PurePosixPath,
126+
dst_path: PurePosixPath,
127+
reason: str | None,
128+
) -> None: ... # pragma: no cover
129+
106130

107131
class StagingToolbox(Toolbox):
108132
"""Git-index backed toolbox implementation

tests/git_draft/toolbox_test.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ def test_list_files(self, repo: git.Repo) -> None:
1515
assert self._toolbox.list_files() == []
1616
names = set(["one.txt", "two.txt"])
1717
for name in names:
18-
with open(Path(repo.working_dir, name), "w") as f:
18+
with Path(repo.working_dir, name).open("w") as f:
1919
f.write("ok")
2020
repo.git.add(all=True)
2121
assert set(self._toolbox.list_files()) == names
2222

2323
def test_read_file(self, repo: git.Repo) -> None:
24-
with open(Path(repo.working_dir, "one"), "w") as f:
24+
with Path(repo.working_dir, "one").open("w") as f:
2525
f.write("ok")
2626

2727
path = PurePosixPath("one")
@@ -38,5 +38,14 @@ def test_write_file(self, repo: git.Repo) -> None:
3838
assert not path.exists()
3939

4040
repo.git.checkout_index(all=True)
41-
with open(path) as f:
41+
with path.open() as f:
42+
assert f.read() == "hi"
43+
44+
def test_rename_file(self, repo: git.Repo) -> None:
45+
self._toolbox.write_file(PurePosixPath("one"), "hi")
46+
self._toolbox.rename_file(PurePosixPath("one"), PurePosixPath("two"))
47+
48+
repo.git.checkout_index(all=True)
49+
assert not Path(repo.working_dir, "one").exists()
50+
with Path(repo.working_dir, "two").open() as f:
4251
assert f.read() == "hi"

0 commit comments

Comments
 (0)