fix(gitstore): return real git object ids instead of hex-of-hex

`porcelain.commit()` and `repo.refs[...]` hand back object ids as a
40-character hex string that is already encoded to bytes. Calling `.hex()`
on that encodes the ASCII a second time, so every id GitStore produced or
displayed was double-encoded:

    auto_commit()          -> '62623234'
    git log --abbrev=8     -> 'bb244606'

The module is self-consistently wrong, so `/dream-log` and `/dream-restore`
work as long as the id came from nanobot itself. What does not work is
crossing the boundary: ids in logs and commit output match nothing in
`git log`, and an id copied from `git log` cannot be resolved:

    _resolve_sha(own id)      -> b'bb244606d780...'
    _resolve_sha(real git id) -> None

Use `.decode()` at the four sites that consume dulwich object ids. Nothing
persists an id — callers either display it or resolve it live — so there is
no stored state in the old format.

Adds two regression tests: the id returned by `auto_commit` must equal
`git log --abbrev=8`, and a real git id must resolve through `_resolve_sha`.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
ATECHPCS 2026-07-27 11:20:11 -04:00 committed by Xubin Ren
parent bb2f6cf324
commit 92361cbeac
2 changed files with 29 additions and 4 deletions

View File

@ -176,7 +176,10 @@ class GitStore:
)
if cast(object, sha_bytes) is None:
return None
sha = sha_bytes.hex()[:8]
# porcelain.commit returns the id as a 40-char hex string that is
# already encoded to bytes; .hex() would encode those ASCII bytes
# again and produce an id no git command can resolve.
sha = sha_bytes.decode()[:8]
logger.debug("Git auto-commit: {} ({})", sha, message)
return sha
except Exception as exc:
@ -200,7 +203,7 @@ class GitStore:
return None
while sha:
if sha.hex().startswith(short_sha):
if sha.decode().startswith(short_sha):
return sha
commit_obj = repo[sha]
if commit_obj.type_name != b"commit":
@ -280,7 +283,7 @@ class GitStore:
msg = commit.message.decode("utf-8", errors="replace").strip()
if message_prefix is None or msg.startswith(message_prefix):
entries.append(CommitInfo(
sha=sha.hex()[:8],
sha=sha.decode()[:8],
message=msg,
timestamp=ts,
))
@ -484,7 +487,7 @@ class GitStore:
with Repo(str(self._workspace)) as repo:
commit = cast("Commit", repo[full_sha])
parent = commit.parents[0] if commit.parents else None
diff = self.diff_commits(parent.hex()[:8], c.sha) if parent else ""
diff = self.diff_commits(parent.decode()[:8], c.sha) if parent else ""
return c, diff
return None
except Exception as exc:

View File

@ -310,3 +310,25 @@ class TestNestedRepoProtection:
assert result is False
assert not (workspace / ".git").exists()
class TestCommitIdEncoding:
"""Commit ids must be usable with git, not hex-of-hex."""
def test_auto_commit_returns_the_real_short_sha(self, git, tmp_path):
(tmp_path / "MEMORY.md").write_text("- a fact\n", encoding="utf-8")
sha = git.auto_commit("memory update")
expected = subprocess.run(
["git", "-C", str(tmp_path), "log", "-1", "--format=%h", "--abbrev=8"],
capture_output=True, text=True, check=True,
).stdout.strip()
assert sha == expected
def test_a_real_git_sha_resolves(self, git, tmp_path):
(tmp_path / "MEMORY.md").write_text("- a fact\n", encoding="utf-8")
git.auto_commit("memory update")
real = subprocess.run(
["git", "-C", str(tmp_path), "log", "-1", "--format=%h", "--abbrev=8"],
capture_output=True, text=True, check=True,
).stdout.strip()
assert git._resolve_sha(real) is not None