Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 23 additions & 16 deletions aider/coders/base_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,11 @@ def abs_root_path(self, path):
self.abs_root_path_cache[key] = res
return res

def abs_new_file_path(self, path):
if self.repo:
return self.repo.abs_new_file_path(path)
return self.abs_root_path(path)

fences = all_fences
fence = fences[0]

Expand Down Expand Up @@ -2190,12 +2195,13 @@ def check_for_dirty_commit(self, path):

def allowed_to_edit(self, path):
full_path = self.abs_root_path(path)
new_file_path = self.abs_new_file_path(path)
if self.repo:
need_to_add = not self.repo.path_in_repo(path)
else:
need_to_add = False

if full_path in self.abs_fnames:
if full_path in self.abs_fnames or new_file_path in self.abs_fnames:
self.check_for_dirty_commit(path)
return True

Expand All @@ -2204,24 +2210,25 @@ def allowed_to_edit(self, path):
return

if not Path(full_path).exists():
if not self.io.confirm_ask("Create new file?", subject=path):
self.io.tool_output(f"Skipping edits to {path}")
return

if not self.dry_run:
if not utils.touch_file(full_path):
self.io.tool_error(f"Unable to create {path}, skipping edits.")
if new_file_path != full_path and Path(new_file_path).exists():
full_path = new_file_path
else:
if not self.io.confirm_ask("Create new file?", subject=path):
self.io.tool_output(f"Skipping edits to {path}")
return

# Seems unlikely that we needed to create the file, but it was
# actually already part of the repo.
# But let's only add if we need to, just to be safe.
if need_to_add and self.auto_commits:
self.repo.repo.git.add(full_path)
if not self.dry_run:
if not utils.touch_file(new_file_path):
self.io.tool_error(f"Unable to create {path}, skipping edits.")
return

self.abs_fnames.add(full_path)
self.check_added_files()
return True
# Add the file at the path we actually created.
if need_to_add and self.auto_commits:
self.repo.repo.git.add(new_file_path)

self.abs_fnames.add(new_file_path)
self.check_added_files()
return True

if not self.io.confirm_ask(
"Allow edits to file that has not been added to the chat?",
Expand Down
27 changes: 18 additions & 9 deletions aider/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -808,11 +808,11 @@ def cmd_add(self, args):
else:
fname = Path(self.coder.root) / word

if self.coder.repo and self.coder.repo.ignored_file(fname):
self.io.tool_warning(f"Skipping {fname} due to aiderignore or --subtree-only.")
continue

if fname.exists():
if self.coder.repo and self.coder.repo.ignored_file(fname):
self.io.tool_warning(f"Skipping {fname} due to aiderignore or --subtree-only.")
continue

if fname.is_file():
all_matched_files.add(str(fname))
continue
Expand All @@ -835,13 +835,22 @@ def cmd_add(self, args):
self.io.tool_output(f"You can add to git with: /git add {fname}")
continue

if self.io.confirm_ask(f"No files matched '{word}'. Do you want to create {fname}?"):
create_path = Path(self.coder.abs_new_file_path(word))
if self.coder.repo and self.coder.repo.ignored_file(create_path):
self.io.tool_warning(
f"Skipping {create_path} due to aiderignore or --subtree-only."
)
continue

if self.io.confirm_ask(
f"No files matched '{word}'. Do you want to create {create_path}?"
):
try:
fname.parent.mkdir(parents=True, exist_ok=True)
fname.touch()
all_matched_files.add(str(fname))
create_path.parent.mkdir(parents=True, exist_ok=True)
create_path.touch()
all_matched_files.add(str(create_path))
except OSError as e:
self.io.tool_error(f"Error creating file {fname}: {e}")
self.io.tool_error(f"Error creating file {create_path}: {e}")

for matched_file in sorted(all_matched_files):
abs_file_path = self.coder.abs_root_path(matched_file)
Expand Down
20 changes: 20 additions & 0 deletions aider/repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,26 @@ def abs_root_path(self, path):
res = Path(self.root) / path
return utils.safe_abs_path(res)

def abs_new_file_path(self, path):
path = Path(path)
if path.is_absolute() or not self.subtree_only:
return utils.safe_abs_path(path) if path.is_absolute() else self.abs_root_path(path)

try:
cwd_path = Path.cwd().resolve()
root_path = Path(self.root).resolve()
cwd_relative = cwd_path.relative_to(root_path)
except ValueError:
return self.abs_root_path(path)

if (
cwd_relative != Path(".")
and path.parts[: len(cwd_relative.parts)] == cwd_relative.parts
):
return self.abs_root_path(path)

return utils.safe_abs_path(cwd_path / path)

def get_dirty_files(self):
"""
Returns a list of all files which are dirty (not committed), either staged or in the working
Expand Down
62 changes: 62 additions & 0 deletions tests/basic/test_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,68 @@ def test_allowed_to_edit(self):

self.assertFalse(coder.need_commit_before_edits)

def test_allowed_to_edit_subtree_only_creates_new_file_in_cwd(self):
with GitTemporaryDirectory():
repo_root = Path.cwd()
subdir = repo_root / "subdir"
subdir.mkdir()
start_cwd = Path.cwd()
os.chdir(subdir)
try:
io = InputOutput(yes=True)
repo = GitRepo(io, None, None, subtree_only=True)
coder = Coder.create(self.GPT35, None, io, repo=repo)

self.assertEqual(coder.abs_root_path("new.txt"), str(repo_root / "new.txt"))

self.assertTrue(coder.allowed_to_edit("new.txt"))

self.assertTrue(Path("new.txt").exists())
self.assertFalse((repo_root / "new.txt").exists())
self.assertIn(str(Path("new.txt").resolve()), coder.abs_fnames)
finally:
os.chdir(start_cwd)

def test_allowed_to_edit_without_subtree_only_keeps_repo_root_for_new_files(self):
with GitTemporaryDirectory():
repo_root = Path.cwd()
subdir = repo_root / "subdir"
subdir.mkdir()
start_cwd = Path.cwd()
os.chdir(subdir)
try:
io = InputOutput(yes=True)
repo = GitRepo(io, None, None, subtree_only=False)
coder = Coder.create(self.GPT35, None, io, repo=repo)

self.assertTrue(coder.allowed_to_edit("new.txt"))

self.assertTrue((repo_root / "new.txt").exists())
self.assertFalse((subdir / "new.txt").exists())
self.assertIn(str((repo_root / "new.txt").resolve()), coder.abs_fnames)
finally:
os.chdir(start_cwd)

def test_allowed_to_edit_subtree_only_keeps_repo_relative_subtree_paths(self):
with GitTemporaryDirectory():
repo_root = Path.cwd()
subdir = repo_root / "subdir"
subdir.mkdir()
start_cwd = Path.cwd()
os.chdir(subdir)
try:
io = InputOutput(yes=True)
repo = GitRepo(io, None, None, subtree_only=True)
coder = Coder.create(self.GPT35, None, io, repo=repo)

self.assertTrue(coder.allowed_to_edit("subdir/new.txt"))

self.assertTrue((repo_root / "subdir" / "new.txt").exists())
self.assertFalse((repo_root / "subdir" / "subdir" / "new.txt").exists())
self.assertIn(str((repo_root / "subdir" / "new.txt").resolve()), coder.abs_fnames)
finally:
os.chdir(start_cwd)

def test_allowed_to_edit_no(self):
with GitTemporaryDirectory():
repo = git.Repo()
Expand Down
21 changes: 21 additions & 0 deletions tests/basic/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,27 @@ def test_cmd_add_no_match_but_make_it(self):
self.assertEqual(len(coder.abs_fnames), 1)
self.assertTrue(fname.exists())

def test_cmd_add_subtree_only_creates_unmatched_file_in_cwd(self):
with GitTemporaryDirectory():
repo_root = Path.cwd()
subdir = repo_root / "subdir"
subdir.mkdir()
start_cwd = Path.cwd()
os.chdir(subdir)
try:
io = InputOutput(pretty=False, fancy_input=False, yes=True)
repo = GitRepo(io, None, None, subtree_only=True)
coder = Coder.create(self.GPT35, None, io, repo=repo)
commands = Commands(io, coder)

commands.cmd_add("new.txt")

self.assertTrue(Path("new.txt").exists())
self.assertFalse((repo_root / "new.txt").exists())
self.assertIn(str(Path("new.txt").resolve()), coder.abs_fnames)
finally:
os.chdir(start_cwd)

def test_cmd_add_drop_directory(self):
# Initialize the Commands and InputOutput objects
io = InputOutput(pretty=False, fancy_input=False, yes=False)
Expand Down
37 changes: 37 additions & 0 deletions tests/basic/test_repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,43 @@ def test_subtree_only(self):
self.assertNotIn(str(root_file), tracked_files)
self.assertNotIn(str(another_subdir_file), tracked_files)

def test_subtree_only_new_file_path_resolution(self):
with GitTemporaryDirectory():
raw_repo = git.Repo()

subdir_file = Path("subdir/subdir_file.txt")
subdir_file.parent.mkdir()
subdir_file.touch()

raw_repo.git.add(str(subdir_file))
raw_repo.git.commit("-m", "Initial commit")

repo_root = Path.cwd()
start_cwd = Path.cwd()
os.chdir(subdir_file.parent)
try:
subtree_repo = GitRepo(InputOutput(), None, None, subtree_only=True)
self.assertEqual(
subtree_repo.abs_new_file_path("new.txt"),
str(repo_root / "subdir" / "new.txt"),
)
self.assertEqual(
subtree_repo.abs_new_file_path("subdir/new.txt"),
str(repo_root / "subdir" / "new.txt"),
)
self.assertNotEqual(
subtree_repo.abs_new_file_path("subdir/new.txt"),
str(repo_root / "subdir" / "subdir" / "new.txt"),
)

full_repo = GitRepo(InputOutput(), None, None, subtree_only=False)
self.assertEqual(
full_repo.abs_new_file_path("new.txt"),
str(repo_root / "new.txt"),
)
finally:
os.chdir(start_cwd)

@patch("aider.models.Model.simple_send_with_retries")
def test_noop_commit(self, mock_send):
mock_send.return_value = '"a good commit message"'
Expand Down