diff --git a/pyproject.toml b/pyproject.toml index 99f9f6e65..085a603fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "GitAuto" -version = "1.24.13" +version = "1.24.14" requires-python = ">=3.14" dependencies = [ "annotated-doc==0.0.4", diff --git a/services/git/delete_file.py b/services/git/delete_file.py index 3437b67bf..b2b8db05d 100644 --- a/services/git/delete_file.py +++ b/services/git/delete_file.py @@ -1,5 +1,6 @@ # Standard imports import os +import subprocess # Third party imports from anthropic.types import ToolUnionParam @@ -38,16 +39,35 @@ def delete_file( local_path = os.path.join(clone_dir, file_path) if os.path.isdir(local_path): + logger.info("delete_file: %s is a directory, returning error", file_path) return f"Error: '{file_path}' is a directory, not a file" if not os.path.exists(local_path): + logger.info("delete_file: %s not found, returning error", file_path) return f"Error: File {file_path} not found" + # Sentry AGENT-36X/36W/344 fired with: `git add mongodb-binaries/mongodb-linux-x86_64-amazon2023-v7.0-latest.tgz.md5` → "pathspec did not match any files". The path is gitignored (mongodb-memory-server writes there) so it was never in git's index, and os.remove above cleared it from disk, leaving `git add` with nothing to match. Check tracking BEFORE os.remove so we can skip the commit when the path was never tracked. + ls_files = subprocess.run( + ["git", "ls-files", "--error-unmatch", file_path], + cwd=clone_dir, + capture_output=True, + check=False, + ) + is_tracked = ls_files.returncode == 0 + os.remove(local_path) logger.info("Deleted local: %s", local_path) + if not is_tracked: + logger.info( + "delete_file: %s is not tracked by git (gitignored or never committed); skipping commit", + file_path, + ) + return f"File {file_path} successfully deleted" + git_commit_and_push( base_args=base_args, message=f"Delete {file_path}", files=[file_path] ) + logger.info("delete_file: %s deleted and commit pushed", file_path) return f"File {file_path} successfully deleted" diff --git a/services/git/test_delete_file.py b/services/git/test_delete_file.py index 8f56c2879..0622a7a30 100644 --- a/services/git/test_delete_file.py +++ b/services/git/test_delete_file.py @@ -77,10 +77,79 @@ def test_delete_file_end_to_end(local_repo, create_test_base_args): assert not os.path.exists(os.path.join(clone_dir, "src", "main.py")) log = subprocess.run( - ["git", "log", "--oneline", "feature/delete-test", "-1"], + ["git", "log", "--format=%s", "feature/delete-test", "-1"], cwd=bare_dir, capture_output=True, text=True, check=False, ) - assert "Delete src/main.py" in log.stdout + assert log.stdout.strip() == "Delete src/main.py" + + +@pytest.mark.integration +def test_delete_gitignored_file_skips_commit(local_repo, create_test_base_args): + """Reproduces AGENT-36X/36W/344: the agent deletes a local gitignored file + (e.g. mongodb-binaries/*.tgz cached from a prior CI run). delete_file used to + rm the file then run `git add ` which fails with 'pathspec did not match + any files' because git never tracked it. Now we detect untracked paths and + skip the commit entirely.""" + bare_url, _work_dir = local_repo + bare_dir = bare_url.replace("file://", "") + + with tempfile.TemporaryDirectory() as clone_dir: + git_clone_to_tmp(clone_dir, bare_url, "main") + + # .gitignore the mongodb-binaries directory, then drop a file in it locally. + with open(os.path.join(clone_dir, ".gitignore"), "a", encoding="utf-8") as f: + f.write("mongodb-binaries/\n") + os.makedirs(os.path.join(clone_dir, "mongodb-binaries"), exist_ok=True) + ( + os.path.join(clone_dir, "mongodb-binaries", "cache.tgz") + ) # noqa: B018 -- path existence below + with open( + os.path.join(clone_dir, "mongodb-binaries", "cache.tgz"), + "w", + encoding="utf-8", + ) as f: + f.write("binary data") + + # Capture remote head BEFORE deletion so we can prove nothing was pushed. + head_before = subprocess.run( + ["git", "rev-parse", "HEAD"], + cwd=bare_dir, + capture_output=True, + text=True, + check=True, + ).stdout.strip() + + base_args = create_test_base_args( + clone_dir=clone_dir, + clone_url=bare_url, + new_branch="feature/delete-gitignored", + ) + + result = delete_file("mongodb-binaries/cache.tgz", base_args) + + assert result == "File mongodb-binaries/cache.tgz successfully deleted" + assert not os.path.exists( + os.path.join(clone_dir, "mongodb-binaries", "cache.tgz") + ) + + # No commit/push should have occurred because the file was never tracked. + head_after = subprocess.run( + ["git", "rev-parse", "HEAD"], + cwd=bare_dir, + capture_output=True, + text=True, + check=True, + ).stdout.strip() + assert head_after == head_before + + # And the new_branch should not exist on the remote. + branch_check = subprocess.run( + ["git", "ls-remote", "--heads", bare_url, "feature/delete-gitignored"], + capture_output=True, + text=True, + check=True, + ) + assert branch_check.stdout.strip() == "" diff --git a/uv.lock b/uv.lock index 61797a1e0..e95f50504 100644 --- a/uv.lock +++ b/uv.lock @@ -596,7 +596,7 @@ wheels = [ [[package]] name = "gitauto" -version = "1.24.13" +version = "1.24.14" source = { virtual = "." } dependencies = [ { name = "annotated-doc" },