memory.sharing.bundle_git¶
memory.sharing.bundle_git ¶
Bundle-repo history rewrite for retroactive revocation (--purge-history).
Revocation is forward-only by default: re-publishing a bundle excludes the revoked party from the new ciphertext, but the bundle repo's PRIOR git commits still contain the OLD ciphertext that WAS wrapped to the revoked party's key. If they kept (or can re-fetch) an old commit, their key still opens it.
This module closes the re-fetchable-from-remote gap by rewriting the bundle repo's history down to a SINGLE commit whose tree is the current (post-revoke) bundle, then making the superseded ciphertext blobs unreachable so they can be garbage-collected. It is deliberately split into two steps:
- :func:
purge_bundle_historyrewrites history LOCALLY (orphan a fresh single-commit history onto the repo's branch, then expire reflogs +git gc --prune=nowso old objects are collectible). It never touches any remote, so it works on a repo with nooriginat all. - :func:
force_push_purgedpropagates that rewrite to a remote. A history rewrite is NOT a fast-forward, so a normal push is rejected — this REQUIRES a force push, which the return value / message makes loud and explicit. It is a separate, opt-in step so the CLI only force-pushes when the user asked for it AND a remote exists.
Mechanism note: we orphan a fresh history rather than shelling out to
git-filter-repo. A bundle is fully regenerable from the source .memory
tree and carries no authorship value, so collapsing to one commit is simpler,
needs no external dependency, and yields the same end state (superseded blobs
unreachable). Commits are authored as memory-mcp to match the identity the
other bundle commits use (see :mod:memory.commit). All git work goes through
subprocess git for consistency with the rest of the repo's git plumbing.
BundleGitError ¶
Bases: RuntimeError
A bundle-repo git operation could not be completed.
Raised (instead of leaking a raw CalledProcessError / traceback) when the
target is not a git repo, an empty repo, or a git subprocess fails.
Source code in memory/sharing/bundle_git.py
PurgeResult
dataclass
¶
Outcome of :func:purge_bundle_history.
branch is the branch that now holds the single collapsed commit;
new_commit is that commit's full sha; previous_head is the sha HEAD
pointed at before the rewrite (now unreachable). had_remote says whether
an origin remote exists, and force_push_required is True exactly
when a remote exists (a history rewrite can only reach a remote via a force
push — a normal push is rejected as non-fast-forward).
Source code in memory/sharing/bundle_git.py
PushResult
dataclass
¶
Outcome of :func:force_push_purged.
pushed is True only when a force push actually ran and succeeded;
forced echoes that the push used --force (a rewrite cannot be pushed
any other way). When no remote exists pushed/forced are False and
message explains that there was nothing to push.
Source code in memory/sharing/bundle_git.py
purge_bundle_history ¶
purge_bundle_history(repo_root: str | Path, *, remote: str = 'origin', message: str = DEFAULT_PURGE_MESSAGE, source_repo_root: str | Path | None = None) -> PurgeResult
Collapse the bundle repo's history to one commit of the current tree.
In the bundle repo working tree (which already holds the freshly re-published
manifest.json + units/ from the current publish) this:
- Orphans a fresh branch (
git checkout --orphan), stages the current working-tree contents (git add -A), and commits a singlememory-mcp-authored commit — so the new commit has NO parent and its tree is exactly the post-revoke bundle. - Moves that commit onto the repo's real branch (deletes the old branch and renames the orphan onto it), so the branch now has exactly ONE commit.
- Expires ALL reflogs and runs
git gc --prune=nowso the superseded commits/blobs (which held ciphertext wrapped to a revoked key) become unreachable and collectible.
This is purely LOCAL — no remote is contacted. Propagating the rewrite to a
remote requires a SEPARATE force push (see :func:force_push_purged); the
returned :class:PurgeResult reports whether a remote exists and whether a
force push is therefore required.
Raises :class:BundleGitError (not a traceback) when repo_root is not a
git repo, the repo has no commits yet, or a git step fails. A dirty working
tree is fine — its current contents become the single commit's tree.
repo_root MUST be the bundle repo's own toplevel — a nested/enclosing
repo, or one whose toplevel equals source_repo_root (the angelo
source/workspace repo), is refused (see :func:_require_standalone_repo) so
this can never rewrite the workspace repo it happens to sit inside or point
at.
Source code in memory/sharing/bundle_git.py
531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 | |
force_push_purged ¶
force_push_purged(repo_root: str | Path, *, remote: str = 'origin', branch: str | None = None, source_repo_root: str | Path | None = None) -> PushResult
Force-push a rewritten branch to remote (opt-in, loud, gated).
A history rewrite is not a fast-forward, so this uses git push --force —
it OVERWRITES the remote branch with the local (post-purge) single-commit
history, discarding the superseded ciphertext commits on the remote. This is
the only way to propagate a :func:purge_bundle_history rewrite, and it is
intentionally destructive of the remote's old history.
When no remote is configured this is a clean no-op: it returns a
:class:PushResult with pushed=False and an explanatory message rather
than raising. Raises :class:BundleGitError when repo_root is not a git
repo, is a nested/enclosing repo or the source/workspace repo itself (see
:func:_require_standalone_repo), or the force push itself fails.
When source_repo_root is given, BOTH the bundle remote's fetch AND push
url (remote.<name>.pushurl — what git push actually uses) are
compared, after canonicalization, against every remote URL (fetch + push, all
remotes) of the source/workspace repo BEFORE pushing; a normalized match on
either side is refused with :class:BundleGitError and NO push happens. This
closes the "standalone bundle repo whose origin (or pushurl) was cloned from
the workspace" gap, where a single-commit force push would clobber the
project's own remote — including the scp-form / scheme / userinfo / host-case
spellings that a naive compare would miss.