diff --git a/aider/commands.py b/aider/commands.py index a23eaab58..e76f9f5d9 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -291,6 +291,34 @@ class Commands: commit_message = args.strip() if args else None self.coder.repo.commit(message=commit_message) + def cmd_scommit(self, args=None): + "Commit edits to the repo made outside the chat (commit message optional)" + try: + self.raw_cmd_scommit(args) + except ANY_GIT_ERROR as err: + self.io.tool_error(f"Unable to complete commit: {err}") + + def raw_cmd_scommit(self, args=None): + if not self.coder.repo: + self.io.tool_error("No git repository found.") + return + + commit_message = args.strip() if args else None + + # Show the diff of the staged changes + self.io.tool_output("Staged changes diff:") + diff = self.coder.repo.repo.git.diff("--cached") # Access git attribute of self.repo.repo + self.io.print(diff) + + # Check if the repository is dirty (optional warning) + if self.coder.repo.is_dirty(): + self.io.tool_warning("The repository has uncommitted changes in the working tree. Proceeding with the commit of staged changes.") + + # staged ONLY: + self.coder.repo.repo.git.commit("-m", commit_message or "Staged changes commit") # test1.txt should be clean after commit + + self.io.tool_output("Staged changes committed successfully.") + def cmd_lint(self, args="", fnames=None): "Lint and fix in-chat files or all dirty files if none in chat" diff --git a/tests/basic/test_commands.py b/tests/basic/test_commands.py index f2616c04a..3e1774e84 100644 --- a/tests/basic/test_commands.py +++ b/tests/basic/test_commands.py @@ -491,6 +491,77 @@ class TestCommands(TestCase): commands.cmd_commit(commit_message) self.assertFalse(repo.is_dirty()) + def test_cmd_scommit(self): + with GitTemporaryDirectory() as repo_dir: + repo = git.Repo() + + fname1 = "ignoreme1.txt" + fname2 = "ignoreme2.txt" + fname3 = "file3.txt" + + file_path = Path(repo_dir) / fname3 + file_path.write_text("Initial content\n") + + Path(fname2).touch() + repo.git.add(str(fname2)) + repo.git.commit("-m", "initial") + + repo.git.add(fname3) + + aignore = Path(".aiderignore") + aignore.write_text(f"{fname1}\n{fname2}\ndir\n") + + io = InputOutput(yes=True) + + fnames = [fname1, fname2] + repo = GitRepo( + io, + fnames, + None, + aider_ignore_file=str(aignore), + ) + + coder = Coder.create( + self.GPT35, + None, + io, + fnames=fnames, + repo=repo, + ) + commands = Commands(io, coder) + commands.cmd_scommit("") + + self.assertFalse(coder.repo.is_dirty(path=fname3)) + + def test_cmd_scommit_mock(self): + with GitTemporaryDirectory() as repo_dir: + io = InputOutput(pretty=False, fancy_input=False, yes=True) + coder = Coder.create(self.GPT35, None, io) + commands = Commands(io, coder) + + # Create and stage file + (Path(repo_dir) / "test1.txt").write_text("Initial content 1") + + # Add and commit files using the git command + coder.repo.repo.git.add("test1.txt") + coder.repo.repo.git.commit("-m", "Initial commit") + + # Modify files to make the repository dirty + (Path(repo_dir) / "test1.txt").write_text("Modified content 1") + + # Stage one of the modified files + coder.repo.repo.git.add("test1.txt") + self.assertTrue(coder.repo.repo.is_dirty(path="test1.txt")) + + # Mock the commit method on the Repo object + with mock.patch.object(coder.repo.repo, 'git', create=True) as mock_git: + mock_git.commit.return_value = None + # Run cmd_scommit + commands.cmd_scommit("") + + # Check if the commit method was called with the correct message + mock_git.commit.assert_called_once_with("-m", "Staged changes commit") + def test_cmd_add_from_outside_root(self): with ChdirTemporaryDirectory() as tmp_dname: root = Path("root")