ids.py
python
sha256:502e812e12319e30302d07d165425ceeb21687ad30e8af3a439b1f6de36c64d6
fix: update stale tag tests to use label for free-form anno…
Sonnet 4.6
19 days ago
| 1 | """Canonical ID derivation for all Muse object types.""" |
| 2 | |
| 3 | import hashlib |
| 4 | from typing import TYPE_CHECKING |
| 5 | |
| 6 | if TYPE_CHECKING: |
| 7 | from muse.core.types import Manifest |
| 8 | |
| 9 | DEFAULT_HASH_ALGO: str = "sha256" |
| 10 | |
| 11 | |
| 12 | def long_id(hex_digest: str, algo: str = DEFAULT_HASH_ALGO, *, strip: bool = False) -> str: |
| 13 | """Canonical long-form of an algorithm-prefixed object ID.""" |
| 14 | prefix = f"{algo}:" |
| 15 | if hex_digest.startswith(prefix): |
| 16 | return hex_digest[len(prefix):] if strip else hex_digest |
| 17 | return hex_digest if strip else f"{prefix}{hex_digest}" |
| 18 | |
| 19 | |
| 20 | def hash_blob(data: bytes, hash_algo: str = DEFAULT_HASH_ALGO) -> str: |
| 21 | """Return the canonical content-addressed ID for raw bytes. |
| 22 | |
| 23 | The type is baked into the hash: ``sha256("blob <size>\\0<data>")``. |
| 24 | |
| 25 | Args: |
| 26 | data: Raw bytes to hash. |
| 27 | hash_algo: Hash algorithm name (default :data:`DEFAULT_HASH_ALGO`). |
| 28 | |
| 29 | Returns: |
| 30 | A ``<hash_algo>:<hex>`` content-addressed ID string. |
| 31 | """ |
| 32 | header = f"blob {len(data)}\0".encode() |
| 33 | return long_id(hashlib.new(hash_algo, header + data).hexdigest(), hash_algo) |
| 34 | |
| 35 | |
| 36 | _SEP: str = "\x00" |
| 37 | |
| 38 | |
| 39 | def hash_snapshot( |
| 40 | manifest: "Manifest", |
| 41 | directories: list[str] | None = None, |
| 42 | hash_algo: str = DEFAULT_HASH_ALGO, |
| 43 | ) -> str: |
| 44 | """Return the canonical content-addressed ID for a snapshot manifest. |
| 45 | |
| 46 | The type is baked into the hash: ``sha256("snapshot <size>\\0<canonical>")``. |
| 47 | """ |
| 48 | from muse.core.types import split_id |
| 49 | parts = sorted( |
| 50 | f"{path}{_SEP}{split_id(oid)[1]}" for path, oid in manifest.items() |
| 51 | ) |
| 52 | if directories: |
| 53 | parts.extend(f"dir{_SEP}{d}" for d in sorted(directories)) |
| 54 | canonical = _SEP.join(parts).encode() |
| 55 | header = f"snapshot {len(canonical)}\0".encode() |
| 56 | return long_id(hashlib.new(hash_algo, header + canonical).hexdigest(), hash_algo) |
| 57 | |
| 58 | |
| 59 | def hash_commit( |
| 60 | parent_ids: list[str], |
| 61 | snapshot_id: str, |
| 62 | message: str, |
| 63 | committed_at_iso: str, |
| 64 | author: str = "", |
| 65 | signer_public_key: str = "", |
| 66 | hash_algo: str = DEFAULT_HASH_ALGO, |
| 67 | ) -> str: |
| 68 | """Return the canonical content-addressed ID for a commit. |
| 69 | |
| 70 | The type is baked into the hash: ``sha256("commit <size>\\0<canonical>")``. |
| 71 | """ |
| 72 | from muse.core.types import split_id |
| 73 | parts = [ |
| 74 | _SEP.join(sorted(split_id(p)[1] for p in parent_ids)), |
| 75 | split_id(snapshot_id)[1] if snapshot_id else "", |
| 76 | message, |
| 77 | committed_at_iso, |
| 78 | author, |
| 79 | signer_public_key, |
| 80 | ] |
| 81 | canonical = _SEP.join(parts).encode() |
| 82 | header = f"commit {len(canonical)}\0".encode() |
| 83 | return long_id(hashlib.new(hash_algo, header + canonical).hexdigest(), hash_algo) |
| 84 | |
| 85 | |
| 86 | def commit_identity_bytes( |
| 87 | parent_ids: list[str], |
| 88 | snapshot_id: str, |
| 89 | message: str, |
| 90 | committed_at_iso: str, |
| 91 | author: str = "", |
| 92 | signer_public_key: str = "", |
| 93 | ) -> bytes: |
| 94 | """Return the canonical bytes that are hashed to produce a commit ID. |
| 95 | |
| 96 | Used by signature verification — the signed payload is these bytes. |
| 97 | """ |
| 98 | from muse.core.types import split_id |
| 99 | parts = [ |
| 100 | _SEP.join(sorted(split_id(p)[1] for p in parent_ids)), |
| 101 | split_id(snapshot_id)[1] if snapshot_id else "", |
| 102 | message, |
| 103 | committed_at_iso, |
| 104 | author, |
| 105 | signer_public_key, |
| 106 | ] |
| 107 | canonical = _SEP.join(parts).encode() |
| 108 | header = f"commit {len(canonical)}\0".encode() |
| 109 | return header + canonical |
File History
1 commit
sha256:502e812e12319e30302d07d165425ceeb21687ad30e8af3a439b1f6de36c64d6
fix: update stale tag tests to use label for free-form anno…
Sonnet 4.6
19 days ago