-
Notifications
You must be signed in to change notification settings - Fork 237
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add rename file action #46
Changes from all commits
b12154b
0b33f8f
425f757
48da49e
e68ce26
1aa1649
9145516
263791f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -86,7 +86,7 @@ def _get_files( | |
files = {} | ||
for file in files_direct: | ||
if file.path not in excluded_files | excluded_files_from_dir: | ||
files[file.path] = file | ||
files[Path(os.path.realpath(file.path))] = file | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the thinking for using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Or did you mean to use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well the real reason was because we were actually just putting a relative Path in there (when it was supposed to be an absolute path!); but yeah, the reason I use realpath instead of abspath is because of symlinks |
||
|
||
return files | ||
|
||
|
@@ -135,7 +135,6 @@ def __init__( | |
exclude_paths: Iterable[str], | ||
): | ||
self.config = config | ||
|
||
self.files = _get_files(self.config, paths, exclude_paths) | ||
|
||
def display_context(self): | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -72,23 +72,34 @@ def get_code_message(self): | |
|
||
return "\n".join(code_message) | ||
|
||
def _add_file(self, abs_path): | ||
logging.info(f"Adding new file {abs_path} to context") | ||
self.code_context.files[abs_path] = CodeFile(abs_path) | ||
# create any missing directories in the path | ||
abs_path.parent.mkdir(parents=True, exist_ok=True) | ||
|
||
def _delete_file(self, abs_path: Path): | ||
logging.info(f"Deleting file {abs_path}") | ||
if abs_path in self.code_context.files: | ||
del self.code_context.files[abs_path] | ||
abs_path.unlink() | ||
|
||
def _handle_delete(self, delete_change): | ||
file_path = self.config.git_root / delete_change.file | ||
if not file_path.exists(): | ||
logging.error(f"Path {file_path} non-existent on delete") | ||
abs_path = self.config.git_root / delete_change.file | ||
if not abs_path.exists(): | ||
logging.error(f"Path {abs_path} non-existent on delete") | ||
return | ||
Comment on lines
+88
to
91
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is actually necessary; abspath appends the CWD to the beginning of the path, which might not necessarily be the current git repo (it could be a subdirectory). This does remind me though, I don't think we have any tests where we run mentat from a subdiretory of the git repo it's actually in; we should probably think about adding some of those! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
@PCSwingle can you add an issue for this |
||
|
||
cprint(f"Are you sure you want to delete {delete_change.file}?", "red") | ||
if self.user_input_manager.ask_yes_no(default_yes=False): | ||
logging.info(f"Deleting file {file_path}") | ||
cprint(f"Deleting {delete_change.file}...") | ||
if file_path in self.code_context.files: | ||
del self.code_context.files[file_path] | ||
file_path.unlink() | ||
self._delete_file(abs_path) | ||
else: | ||
cprint(f"Not deleting {delete_change.file}") | ||
|
||
def _get_new_code_lines(self, changes) -> Iterable[str] | None: | ||
def _get_new_code_lines(self, rel_path, changes) -> Iterable[str]: | ||
if not changes: | ||
return [] | ||
if len(set(map(lambda change: change.file, changes))) > 1: | ||
raise Exception("All changes passed in must be for the same file") | ||
|
||
|
@@ -103,7 +114,6 @@ def _get_new_code_lines(self, changes) -> Iterable[str] | None: | |
if not changes: | ||
return [] | ||
|
||
rel_path = str(changes[0].file) | ||
new_code_lines = self.file_lines[rel_path].copy() | ||
if new_code_lines != self._read_file(rel_path): | ||
logging.info(f"File '{rel_path}' changed while generating changes") | ||
|
@@ -131,31 +141,41 @@ def _get_new_code_lines(self, changes) -> Iterable[str] | None: | |
return new_code_lines | ||
|
||
def write_changes_to_files(self, code_changes: list[CodeChange]) -> None: | ||
files_to_write = dict() | ||
file_changes = defaultdict(list) | ||
for code_change in code_changes: | ||
# here keys are str not path object | ||
rel_path = str(code_change.file) | ||
if code_change.action == CodeChangeAction.CreateFile: | ||
cprint(f"Creating new file {rel_path}", color="light_green") | ||
files_to_write[rel_path] = code_change.code_lines | ||
elif code_change.action == CodeChangeAction.DeleteFile: | ||
self._handle_delete(code_change) | ||
else: | ||
file_changes[rel_path].append(code_change) | ||
|
||
for file_path, changes in file_changes.items(): | ||
new_code_lines = self._get_new_code_lines(changes) | ||
abs_path = self.config.git_root / rel_path | ||
match code_change.action: | ||
case CodeChangeAction.CreateFile: | ||
cprint(f"Creating new file {rel_path}", color="light_green") | ||
self._add_file(abs_path) | ||
with open(abs_path, "w") as f: | ||
f.write("\n".join(code_change.code_lines)) | ||
case CodeChangeAction.DeleteFile: | ||
self._handle_delete(code_change) | ||
case CodeChangeAction.RenameFile: | ||
abs_new_path = self.config.git_root / code_change.name | ||
self._add_file(abs_new_path) | ||
code_lines = self.file_lines[rel_path] | ||
with open(abs_new_path, "w") as f: | ||
f.write("\n".join(code_lines)) | ||
self._delete_file(abs_path) | ||
file_changes[str(code_change.name)] += file_changes[rel_path] | ||
file_changes[rel_path] = [] | ||
self.file_lines[str(code_change.name)] = self._read_file( | ||
abs_new_path | ||
) | ||
case _: | ||
file_changes[rel_path].append(code_change) | ||
|
||
for rel_path, changes in file_changes.items(): | ||
abs_path = self.config.git_root / rel_path | ||
new_code_lines = self._get_new_code_lines(rel_path, changes) | ||
if new_code_lines: | ||
files_to_write[file_path] = new_code_lines | ||
|
||
for rel_path, code_lines in files_to_write.items(): | ||
file_path = self.config.git_root / rel_path | ||
if file_path not in self.code_context.files: | ||
# newly created files added to Mentat's context | ||
logging.info(f"Adding new file {file_path} to context") | ||
self.code_context.files[file_path] = CodeFile(file_path) | ||
# create any missing directories in the path | ||
file_path.parent.mkdir(parents=True, exist_ok=True) | ||
with open(file_path, "w") as f: | ||
f.write("\n".join(code_lines)) | ||
if abs_path not in self.code_context.files: | ||
raise MentatError( | ||
f"Attempted to edit file {abs_path} not in context" | ||
) | ||
with open(abs_path, "w") as f: | ||
f.write("\n".join(new_code_lines)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What edge cases have you discovered/thought about so far?
One that comes to mind is a file being renamed and imports not being updated accordingly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking more edge cases in how we handle it; the big one being that if we don't rename the file, then any changes the model suggests that try to change the renamed file will be trying to change a non-existent file. It gets worse if the model is renaming multiple files to similar names, or one file twice, and so on.