From a662ace8dd1b3ecabd5c0f0b5d07b63240b1892b Mon Sep 17 00:00:00 2001 From: chengyongru Date: Fri, 3 Apr 2026 14:05:17 +0800 Subject: [PATCH] fix(memory): use parent tree in dream-restore revert to properly undo commit The revert method was using the commit's own tree instead of its parent's, which meant /dream-restore would restore TO that commit rather than UNDO it. Also add root commit guard to prevent crash when reverting the initial commit. --- nanobot/agent/git_store.py | 20 ++++++++++++++------ tests/agent/test_git_store.py | 6 +++++- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/nanobot/agent/git_store.py b/nanobot/agent/git_store.py index a09c7730f..c2f7d2372 100644 --- a/nanobot/agent/git_store.py +++ b/nanobot/agent/git_store.py @@ -238,10 +238,11 @@ class GitStore: # -- restore --------------------------------------------------------------- def revert(self, commit: str) -> str | None: - """Restore all tracked memory files to their state at the given commit. + """Revert (undo) the changes introduced by the given commit. + + Restores all tracked memory files to the state at the commit's parent, + then creates a new commit recording the revert. - This reads the file contents from the target commit, writes them back, - and creates a new commit. Does not require merge3. Returns the new commit SHA, or None on failure. """ if not self.is_initialized(): @@ -255,13 +256,20 @@ class GitStore: logger.warning("Git revert: SHA not found: {}", commit) return None - restored: list[str] = [] with Repo(str(self._workspace)) as repo: commit_obj = repo[full_sha] if commit_obj.type_name != b"commit": return None - tree = repo[commit_obj.tree] + if not commit_obj.parents: + logger.warning("Git revert: cannot revert root commit {}", commit) + return None + + # Use the parent's tree — this undoes the commit's changes + parent_obj = repo[commit_obj.parents[0]] + tree = repo[parent_obj.tree] + + restored: list[str] = [] for filepath in self._tracked_files: content = self._read_blob_from_tree(repo, tree, filepath) if content is not None: @@ -273,7 +281,7 @@ class GitStore: return None # Commit the restored state - msg = f"revert: restore to {commit}" + msg = f"revert: undo {commit}" return self.auto_commit(msg) except Exception: logger.warning("Git revert failed for {}", commit) diff --git a/tests/agent/test_git_store.py b/tests/agent/test_git_store.py index d374cfbad..adc1fc08c 100644 --- a/tests/agent/test_git_store.py +++ b/tests/agent/test_git_store.py @@ -205,10 +205,14 @@ class TestRevert: git_ready.auto_commit("v2") commits = git_ready.log() - new_sha = git_ready.revert(commits[1].sha) # revert to init + new_sha = git_ready.revert(commits[0].sha) # undo v2 → back to init assert new_sha is not None assert (ws / "SOUL.md").read_text(encoding="utf-8") == "" + def test_cannot_revert_root_commit(self, git_ready): + commits = git_ready.log() + assert git_ready.revert(commits[-1].sha) is None + def test_invalid_sha_returns_none(self, git_ready): assert git_ready.revert("deadbeef") is None