From 9f5a56f1ec224ca794017d4ab3f5f2d0ad60e4fc Mon Sep 17 00:00:00 2001 From: qtds Date: Fri, 21 Aug 2026 22:47:28 +0800 Subject: [PATCH] fix(gitstore): detect rapid same-size rewrites --- nanobot/utils/gitstore.py | 10 +++++----- tests/agent/test_git_store.py | 13 +++++++++++++ 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/nanobot/utils/gitstore.py b/nanobot/utils/gitstore.py index 4140b153b..b6a7109a2 100644 --- a/nanobot/utils/gitstore.py +++ b/nanobot/utils/gitstore.py @@ -133,12 +133,13 @@ class GitStore: try: from dulwich import porcelain - # .gitignore excludes everything except tracked files, - # so any staged/unstaged change must be in our files. + # Stage first so Dulwich refreshes the content hashes. A status + # check can miss rapid same-size rewrites when the filesystem also + # preserves the file's mtime. + porcelain.add(str(self._workspace), paths=self._staging_paths(*self._tracked_files)) st = porcelain.status(str(self._workspace)) - unstaged = cast(list[object], st.unstaged) staged = cast(dict[object, list[object]], st.staged) - if not unstaged and not any(staged.values()): + if not any(staged.values()): return None message_value = cast(object, message) @@ -147,7 +148,6 @@ class GitStore: if isinstance(message_value, str) else cast(bytes, message_value) ) - porcelain.add(str(self._workspace), paths=self._staging_paths(*self._tracked_files)) sha_bytes = porcelain.commit( str(self._workspace), message=msg_bytes, diff --git a/tests/agent/test_git_store.py b/tests/agent/test_git_store.py index 473624811..29180ad95 100644 --- a/tests/agent/test_git_store.py +++ b/tests/agent/test_git_store.py @@ -1,5 +1,6 @@ """Tests for GitStore — git-backed version control for memory files.""" +import os from unittest.mock import patch import pytest @@ -98,6 +99,18 @@ class TestAutoCommit: assert len(commits) == 2 assert commits[0].sha == sha + def test_commits_same_size_rewrite_with_unchanged_mtime(self, git_ready): + path = git_ready._workspace / "SOUL.md" + path.write_text("v1", encoding="utf-8") + git_ready.auto_commit("v1") + previous_stat = path.stat() + + path.write_text("v2", encoding="utf-8") + os.utime(path, ns=(previous_stat.st_atime_ns, previous_stat.st_mtime_ns)) + + assert git_ready.auto_commit("v2") is not None + assert [commit.message for commit in git_ready.log()[:2]] == ["v2", "v1"] + def test_does_not_create_empty_commits(self, git_ready): git_ready.auto_commit("nothing 1") git_ready.auto_commit("nothing 2")