diff --git a/packages/bot/pyproject.toml b/packages/bot/pyproject.toml index fae1741..564a2e1 100644 --- a/packages/bot/pyproject.toml +++ b/packages/bot/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "automa-bot" -version = "0.3.0" +version = "0.4.0" authors = [{ name = "Sunkara, Inc.", email = "engineering@automa.app" }] description = "Bot SDK for Automa" diff --git a/packages/bot/src/automa/bot/resources/code.py b/packages/bot/src/automa/bot/resources/code.py index 20ed1be..fd2bb28 100644 --- a/packages/bot/src/automa/bot/resources/code.py +++ b/packages/bot/src/automa/bot/resources/code.py @@ -24,10 +24,15 @@ # TODO: Use programmatic git instead of git command -def get_diff(path: str) -> str: +def get_diff(path: str, base_commit: str | None = None) -> str: """Get the diff for the given git repository path.""" + args = ["git", "diff"] + + if base_commit: + args.append(base_commit) + result = subprocess.run( - ["git", "diff"], + args, cwd=path, capture_output=True, text=True, @@ -78,12 +83,12 @@ def _read_token(self, folder: str) -> str | None: return token - def _read_base_commit(self, folder: str) -> str | None: + def _read_base_commit(self, folder: str, type: str) -> str | None: base_commit = None try: with open( - f"{folder}/.git/automa_proposal_base_commit", "r", encoding="utf8" + f"{folder}/.git/automa_{type}_base_commit", "r", encoding="utf8" ) as f: base_commit = f.read().strip() except FileNotFoundError: @@ -103,16 +108,8 @@ def _clone_code(self, folder: str, url: str) -> None: check=True, ) - # Note down the base commit - result = subprocess.run( - ["git", "rev-parse", "HEAD"], - cwd=path, - capture_output=True, - text=True, - check=True, - ) - - self._write_base_commit(folder, result.stdout.strip()) + # Note down the base commit to send in proposal + self._write_base_commit(folder, "proposal") def _extract_download(self, folder: str) -> None: rmtree(folder, ignore_errors=True) @@ -121,17 +118,27 @@ def _extract_download(self, folder: str) -> None: with tarfile.open(f"{folder}.tar.gz", "r:gz") as tar: tar.extractall(path=folder) + # Note down the base commit to be used in generating diff + self._write_base_commit(folder, "diff") + def _write_token(self, folder: str, token: str) -> None: # Save the proposal token for later use with open(f"{folder}/.git/automa_proposal_token", "w", encoding="utf8") as f: f.write(token) - def _write_base_commit(self, folder: str, base_commit: str) -> None: - # Save the base commit for later use + def _write_base_commit(self, folder: str, type: str) -> None: + result = subprocess.run( + ["git", "rev-parse", "HEAD"], + cwd=folder, + capture_output=True, + text=True, + check=True, + ) + with open( - f"{folder}/.git/automa_proposal_base_commit", "w", encoding="utf8" + f"{folder}/.git/automa_{type}_base_commit", "w", encoding="utf8" ) as f: - f.write(base_commit) + f.write(result.stdout.strip()) class CodeResource(SyncAPIResource, BaseCodeResource): @@ -202,12 +209,13 @@ def propose( ): path = self._path(body["task"]) token = self._read_token(path) - base_commit = self._read_base_commit(path) + base_commit = self._read_base_commit(path, "proposal") + diff_base_commit = self._read_base_commit(path, "diff") if not token: raise ValueError("Failed to read the stored proposal token") - diff = get_diff(path) + diff = get_diff(path, base_commit or diff_base_commit) return self._client.post( "/bot/code/propose", @@ -295,12 +303,13 @@ async def propose( ): path = self._path(body["task"]) token = await to_thread(self._read_token, path) - base_commit = await to_thread(self._read_base_commit, path) + base_commit = await to_thread(self._read_base_commit, path, "proposal") + diff_base_commit = await to_thread(self._read_base_commit, path, "diff") if not token: raise ValueError("Failed to read the stored proposal token") - diff = await to_thread(get_diff, path) + diff = await to_thread(get_diff, path, base_commit or diff_base_commit) return await self._client.post( "/bot/code/propose", diff --git a/packages/bot/tests/fixtures/download/LICENSE b/packages/bot/tests/fixtures/download/LICENSE new file mode 100644 index 0000000..e69de29 diff --git a/packages/bot/tests/fixtures/download_git/HEAD b/packages/bot/tests/fixtures/download/_git/HEAD similarity index 100% rename from packages/bot/tests/fixtures/download_git/HEAD rename to packages/bot/tests/fixtures/download/_git/HEAD diff --git a/packages/bot/tests/fixtures/download/_git/config b/packages/bot/tests/fixtures/download/_git/config new file mode 100644 index 0000000..515f483 --- /dev/null +++ b/packages/bot/tests/fixtures/download/_git/config @@ -0,0 +1,5 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = false + logallrefupdates = true diff --git a/packages/bot/tests/fixtures/download/_git/index b/packages/bot/tests/fixtures/download/_git/index new file mode 100644 index 0000000..9ba53f5 Binary files /dev/null and b/packages/bot/tests/fixtures/download/_git/index differ diff --git a/packages/bot/tests/fixtures/download/_git/logs/HEAD b/packages/bot/tests/fixtures/download/_git/logs/HEAD new file mode 100644 index 0000000..3daa442 --- /dev/null +++ b/packages/bot/tests/fixtures/download/_git/logs/HEAD @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 5575f40bc4ead411b690b6f2f09636e3468ef12e automa[bot] <60525818+automa[bot]@users.noreply.github.com> 1772718655 +0530 commit (initial): Downloaded code diff --git a/packages/bot/tests/fixtures/download/_git/logs/refs/heads/master b/packages/bot/tests/fixtures/download/_git/logs/refs/heads/master new file mode 100644 index 0000000..3daa442 --- /dev/null +++ b/packages/bot/tests/fixtures/download/_git/logs/refs/heads/master @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 5575f40bc4ead411b690b6f2f09636e3468ef12e automa[bot] <60525818+automa[bot]@users.noreply.github.com> 1772718655 +0530 commit (initial): Downloaded code diff --git a/packages/bot/tests/fixtures/download/_git/objects/55/75f40bc4ead411b690b6f2f09636e3468ef12e b/packages/bot/tests/fixtures/download/_git/objects/55/75f40bc4ead411b690b6f2f09636e3468ef12e new file mode 100644 index 0000000..2f2c1b1 Binary files /dev/null and b/packages/bot/tests/fixtures/download/_git/objects/55/75f40bc4ead411b690b6f2f09636e3468ef12e differ diff --git a/packages/bot/tests/fixtures/download/_git/objects/58/2ebe2cc578f2c9b9c124a3d2a65afa0c3c9df2 b/packages/bot/tests/fixtures/download/_git/objects/58/2ebe2cc578f2c9b9c124a3d2a65afa0c3c9df2 new file mode 100644 index 0000000..b43b21f Binary files /dev/null and b/packages/bot/tests/fixtures/download/_git/objects/58/2ebe2cc578f2c9b9c124a3d2a65afa0c3c9df2 differ diff --git a/packages/bot/tests/fixtures/download_git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 b/packages/bot/tests/fixtures/download/_git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 similarity index 100% rename from packages/bot/tests/fixtures/download_git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 rename to packages/bot/tests/fixtures/download/_git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/packages/bot/tests/fixtures/download/_git/refs/heads/master b/packages/bot/tests/fixtures/download/_git/refs/heads/master new file mode 100644 index 0000000..ffa0078 --- /dev/null +++ b/packages/bot/tests/fixtures/download/_git/refs/heads/master @@ -0,0 +1 @@ +5575f40bc4ead411b690b6f2f09636e3468ef12e diff --git a/packages/bot/tests/fixtures/download_git/index b/packages/bot/tests/fixtures/download_git/index deleted file mode 100644 index 083dc80..0000000 Binary files a/packages/bot/tests/fixtures/download_git/index and /dev/null differ diff --git a/packages/bot/tests/fixtures/download_git/objects/cc/3f46ae7fdf71747b66b3e4272c0e5fe290d116 b/packages/bot/tests/fixtures/download_git/objects/cc/3f46ae7fdf71747b66b3e4272c0e5fe290d116 deleted file mode 100644 index 51a0b9f..0000000 Binary files a/packages/bot/tests/fixtures/download_git/objects/cc/3f46ae7fdf71747b66b3e4272c0e5fe290d116 and /dev/null differ diff --git a/packages/bot/tests/fixtures/download_git/objects/f9/3e3a1a1525fb5b91020da86e44810c87a2d7bc b/packages/bot/tests/fixtures/download_git/objects/f9/3e3a1a1525fb5b91020da86e44810c87a2d7bc deleted file mode 100644 index 91fccd4..0000000 Binary files a/packages/bot/tests/fixtures/download_git/objects/f9/3e3a1a1525fb5b91020da86e44810c87a2d7bc and /dev/null differ diff --git a/packages/bot/tests/fixtures/download_git/refs/heads/master b/packages/bot/tests/fixtures/download_git/refs/heads/master deleted file mode 100644 index be3d3dc..0000000 --- a/packages/bot/tests/fixtures/download_git/refs/heads/master +++ /dev/null @@ -1 +0,0 @@ -cc3f46ae7fdf71747b66b3e4272c0e5fe290d116 diff --git a/packages/bot/tests/resources/test_code.py b/packages/bot/tests/resources/test_code.py index 97f466e..2437f5b 100644 --- a/packages/bot/tests/resources/test_code.py +++ b/packages/bot/tests/resources/test_code.py @@ -12,6 +12,7 @@ folder = "/tmp/automa/tasks/28" proposal_token_file = f"{folder}/.git/automa_proposal_token" proposal_base_commit_file = f"{folder}/.git/automa_proposal_base_commit" +diff_base_commit_file = f"{folder}/.git/automa_diff_base_commit" @pytest.fixture @@ -45,7 +46,7 @@ def async_code_resource(): @pytest.fixture def fixture_git(): tests_folder = Path(__file__).parent.parent - git_path = tests_folder / "fixtures" / "download_git" + git_path = tests_folder / "fixtures" / "download" / "_git" yield git_path @@ -55,23 +56,20 @@ def fixture_tarfile(): tests_folder = Path(__file__).parent.parent fixture = tests_folder / "fixtures" / "download" tarfile_path = tests_folder / "fixture.tar.gz" + git_path = fixture / ".git" + _git_path = fixture / "_git" - run(["git", "init"], cwd=fixture, capture_output=True) - run(["git", "add", "."], cwd=fixture, capture_output=True) - run(["git", "config", "user.name", "Tmp"], cwd=fixture, capture_output=True) - run( - ["git", "config", "user.email", "tmp@tmp.com"], cwd=fixture, capture_output=True - ) - run(["git", "commit", "-m", "Initial commit"], cwd=fixture, capture_output=True) + _git_path.rename(git_path) with tarfile.open(tarfile_path, "w:gz") as tar: for subpath in listdir(fixture): tar.add(fixture / subpath, arcname=subpath) + git_path.rename(_git_path) + yield tarfile_path remove(tarfile_path) - rmtree(fixture / ".git", ignore_errors=True) def test_cleanup_proxy(code_resource): @@ -154,7 +152,7 @@ def test_download_invalid_token(code_resource): @pytest.mark.asyncio -async def test_download_async_invalid_token(async_code_resource): +async def test_download_invalid_token_async(async_code_resource): # Mock client response response_mock = MagicMock() response_mock.status_code = 403 @@ -299,6 +297,7 @@ def test_download_proxy(fixture_tarfile, code_resource): assert path.exists(folder) assert sorted(listdir(folder)) == [ ".git", + "LICENSE", "README.md", ] @@ -306,6 +305,11 @@ def test_download_proxy(fixture_tarfile, code_resource): with open(proposal_token_file, "r") as f: assert f.read() == "ghijkl" + # Saves diff base commit + assert not path.exists(proposal_base_commit_file) + with open(diff_base_commit_file, "r") as f: + assert f.read() == "5575f40bc4ead411b690b6f2f09636e3468ef12e" + return created_folder @@ -352,6 +356,7 @@ async def test_download_proxy_async(fixture_tarfile, async_code_resource): assert path.exists(folder) assert sorted(listdir(folder)) == [ ".git", + "LICENSE", "README.md", ] @@ -359,6 +364,11 @@ async def test_download_proxy_async(fixture_tarfile, async_code_resource): with open(proposal_token_file, "r") as f: assert f.read() == "ghijkl" + # Saves diff base commit + assert not path.exists(proposal_base_commit_file) + with open(diff_base_commit_file, "r") as f: + assert f.read() == "5575f40bc4ead411b690b6f2f09636e3468ef12e" + def test_propose_no_token(fixture_tarfile, code_resource): test_download_proxy(fixture_tarfile, code_resource) @@ -373,7 +383,7 @@ def test_propose_no_token(fixture_tarfile, code_resource): @pytest.mark.asyncio -async def test_propose_async_no_token(fixture_tarfile, async_code_resource): +async def test_propose_no_token_async(fixture_tarfile, async_code_resource): await test_download_proxy_async(fixture_tarfile, async_code_resource) remove(proposal_token_file) @@ -421,7 +431,7 @@ def test_propose_invalid_token(fixture_tarfile, code_resource): @pytest.mark.asyncio -async def test_propose_async_invalid_token(fixture_tarfile, async_code_resource): +async def test_propose_invalid_token_async(fixture_tarfile, async_code_resource): await test_download_proxy_async(fixture_tarfile, async_code_resource) with open(f"{folder}/README.md", "w") as f: @@ -593,6 +603,58 @@ def test_propose_with_added_files_using_add_all(fixture_tarfile, code_resource): ) +def test_propose_with_intermediate_commits(fixture_tarfile, code_resource): + test_download_proxy(fixture_tarfile, code_resource) + + with open(f"{folder}/LICENSE", "w") as f: + f.write("MIT\n") + + run(["git", "add", "LICENSE"], cwd=folder, capture_output=True) + run( + [ + "git", + "-c", + "user.name=Tmp", + "-c", + "user.email=tmp@tmp.com", + "commit", + "-m", + "Intermediate commit", + ], + cwd=folder, + capture_output=True, + ) + + with open(f"{folder}/README.md", "w") as f: + f.write("Content\n") + + # Mock client response + response_mock = MagicMock() + response_mock.status_code = 204 + response_mock.is_error = False + + code_resource._client._client.request.return_value = response_mock + + code_resource.propose({"task": {"id": 28, "token": "abcdef"}}) + + # Hits the API + code_resource._client._client.request.assert_called_once_with( + "post", + "/bot/code/propose", + json={ + "task": {"id": 28, "token": "abcdef"}, + "proposal": { + "token": "ghijkl", + "diff": "diff --git a/LICENSE b/LICENSE\nindex e69de29..a22a2da 100644\n--- a/LICENSE\n+++ b/LICENSE\n@@ -0,0 +1 @@\n+MIT\ndiff --git a/README.md b/README.md\nindex e69de29..39c9f36 100644\n--- a/README.md\n+++ b/README.md\n@@ -0,0 +1 @@\n+Content\n", + }, + }, + headers={ + "Accept": "application/json", + "Content-Type": "application/json", + }, + ) + + def test_propose_with_proposal_properties(fixture_tarfile, code_resource): test_download_proxy(fixture_tarfile, code_resource) @@ -710,6 +772,7 @@ def test_download_direct(fixture_git, code_resource): assert path.exists(folder) assert sorted(listdir(folder)) == [ ".git", + "LICENSE", "README.md", ] @@ -719,7 +782,8 @@ def test_download_direct(fixture_git, code_resource): # Saves the base commit with open(proposal_base_commit_file, "r") as f: - assert f.read() == "cc3f46ae7fdf71747b66b3e4272c0e5fe290d116" + assert f.read() == "5575f40bc4ead411b690b6f2f09636e3468ef12e" + assert not path.exists(diff_base_commit_file) @pytest.mark.asyncio @@ -766,6 +830,7 @@ async def test_download_direct_async(fixture_git, async_code_resource): assert path.exists(folder) assert sorted(listdir(folder)) == [ ".git", + "LICENSE", "README.md", ] @@ -775,7 +840,8 @@ async def test_download_direct_async(fixture_git, async_code_resource): # Saves the base commit with open(proposal_base_commit_file, "r") as f: - assert f.read() == "cc3f46ae7fdf71747b66b3e4272c0e5fe290d116" + assert f.read() == "5575f40bc4ead411b690b6f2f09636e3468ef12e" + assert not path.exists(diff_base_commit_file) def test_propose_direct(fixture_git, code_resource): @@ -802,7 +868,7 @@ def test_propose_direct(fixture_git, code_resource): "proposal": { "token": "ghijkl", "diff": "diff --git a/README.md b/README.md\nindex e69de29..39c9f36 100644\n--- a/README.md\n+++ b/README.md\n@@ -0,0 +1 @@\n+Content\n", - "base_commit": "cc3f46ae7fdf71747b66b3e4272c0e5fe290d116", + "base_commit": "5575f40bc4ead411b690b6f2f09636e3468ef12e", }, }, headers={ @@ -837,7 +903,7 @@ async def test_propose_direct_async(fixture_git, async_code_resource): "proposal": { "token": "ghijkl", "diff": "diff --git a/README.md b/README.md\nindex e69de29..39c9f36 100644\n--- a/README.md\n+++ b/README.md\n@@ -0,0 +1 @@\n+Content\n", - "base_commit": "cc3f46ae7fdf71747b66b3e4272c0e5fe290d116", + "base_commit": "5575f40bc4ead411b690b6f2f09636e3468ef12e", }, }, headers={ diff --git a/uv.lock b/uv.lock index 42302ce..ba9b4af 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.11" [manifest] @@ -32,7 +32,7 @@ wheels = [ [[package]] name = "automa-bot" -version = "0.3.0" +version = "0.4.0" source = { virtual = "packages/bot" } dependencies = [ { name = "anyio" },