Skip to content
rlsbl.release_commit_remap
On this page

Moves the release record's release commits through a history rewrite's commit map, re-checking each recorded tree hash so a redacted release cannot be re-recorded.

#rlsbl.release_commit_remap

#rlsbl.release_commit_remap

Moving the release record's release commits through a history rewrite's commit map.

A release archive records the commit a version shipped from -- its candidate_sha -- and the git tree each released path carried. Both name objects in the commit graph, and a history rewrite replaces that graph. The JSONL changelog has been remapped through the rewrite's old-to-new commit map since scrubbing existed; the archives were not, so after a scrub the release record's release commit and the (correctly moved) tag disagreed, and :func:rlsbl.release_record.read_entry refused every read with the DISAGREEMENT error -- an error that blames the tag for moving, which is precisely backwards.

The repair is this module, and it is deliberately narrow:

  • a release commit whose commit the map names is rewritten to the new commit;
  • a release commit the map does not mention is left exactly as it is;
  • an unrecoverable or never_released archive has no commit to move and is skipped.

The content check, and the one caller that cannot pass it. Each recorded tree hash is recomputed at the new commit and compared. A rewrite that only re-parents commits leaves every tree byte-identical, and the version really did ship the same content from a differently-named commit. A rewrite that REDACTED something under a released path did not, and re-recording the archive at it would make the archive claim content the release never shipped -- so by default a mismatch is a hard error naming the version and both hashes, and nothing is written.

rlsbl release scrub is the caller that must state otherwise, and the reason is its own doing: it passes --remap-shas-in so safegit rewrites the JSONL changelog files AT EVERY COMMIT of the rewritten history. Those files are INSIDE the released tree, and they name commits the rewrite moved, so a scrub of an rlsbl-managed repository changes every released tree by construction -- the default check would refuse every scrub that repository ever runs. That caller passes on_content_change="record", which recomputes and records the new trees and PRINTS every path whose tree changed, so the change is visible in the scrub's output rather than absorbed. Which behavior applies is the caller's declared choice, never something decided from what is observed.

Every archive is validated before ANY of them is written, so a failure on one version leaves the whole release record untouched rather than half-remapped.

#ReleaseCommitRemap

One archive's release commit, moved.

tree_hashes is what the rewritten archive will record, and changed_paths names the released paths whose tree is a different object at the new commit -- empty whenever the content was proved identical.

#_map_sha

python
def _map_sha(sha, commit_map)

The rewritten commit for sha, or None when the map does not name it.

Abbreviation-tolerant in the same way the changelog remap is: a stored release commit may be shorter than 40 characters, and it maps when it prefixes exactly one key.

#_tree_at

python
def _tree_at(sha, path, *, cwd)

The git tree object for path at commit sha.

"." (and the empty path) resolve to the commit's root tree, matching exactly how the release flow writes the release commit's content half -- both go through :func:~rlsbl.git_util.tree_rev_spec.

#plan_release_commit_remap

python
def plan_release_commit_remap(releases_dir: str, commit_map: dict, *, cwd=None, on_content_change=ON_CONTENT_CHANGE_REFUSE)

Which archives the rewrite moves, verified but not yet written.

Reads every archive, maps the release commits, recomputes and compares the tree hashes, and returns one :class:ReleaseCommitRemap per version that moves.

on_content_change declares what a released path whose tree differs at the rewritten commit means to this caller: "refuse" raises :class:~rlsbl.errors.RlsblError naming the version and both hashes, so the caller writes nothing; "record" recomputes the tree and reports the change on the returned item's changed_paths. See the module docstring for why rlsbl release scrub is the caller that declares the second.

#remap_release_commits

python
def remap_release_commits(releases_dir: str, commit_map: dict, *, cwd=None, on_content_change=ON_CONTENT_CHANGE_REFUSE)

Move every archived release commit in releases_dir through commit_map.

Returns the :class:ReleaseCommitRemap records for the archives that moved, in the release record's own highest-first order. The whole set is planned and verified before the first write, so a mismatch on any version leaves every archive untouched.

Each write unlocks the read-only archive, rewrites the release commit, and relocks it -- the documented edit path, so the archive is never observable as a writable file that already records its release commit.

#release_commit_remap_event

python
def release_commit_remap_event(rewrite: str, remaps)

The transition record event recording a release commit remap, or None when nothing moved.

The record is what lets a later reader EXPLAIN why a version's archive names a commit that no earlier record mentions -- including a reader in a fresh clone, where safegit's own journal (which lives under .git) is not there to consult.

#record_release_commit_remap

python
def record_release_commit_remap(transition_record_path: str, rewrite: str, remaps)

Append the release-commit-remap event for remaps to the transition record.

Returns the path when an event was written, None when nothing moved.

#releases_dirs_for

python
def releases_dirs_for(project_root: str, workspace_root=None, workspace_projects=None)

Every release-archive directory a rewrite in this repository can touch.

Mirrors :func:rlsbl.changelog.files.enumerate_changelog_dirs -- the same walk the JSONL remap already takes -- so the two halves of the repair cover the same set of projects rather than each deciding for itself.

A directory is included only when it actually HOLDS an archive. Its mere existence proves nothing: several unrelated paths (the release lock, a scaffold) create releases/ before anything is released, and treating an empty one as a release record would make every caller pay for a rewrite-journal read on a repository with no release to repair.

#transition_record_path_for_releases_dir

python
def transition_record_path_for_releases_dir(releases_dir: str) -> str

The transition record that pairs with a release-archive directory.

.rlsbl/releases/ -> .rlsbl/transitions.jsonl, and a releasable's <releasable>/releases/ -> <releasable>/transitions.jsonl. Both are the homes :func:rlsbl.transition_record.get_transition_record_path resolves, expressed as the derivation from the directory a caller already holds.

#repair_release_commits

python
def repair_release_commits(*, project_root, commit_map, rewrite_id, workspace_root=None, workspace_projects=None, cwd=None, on_content_change=ON_CONTENT_CHANGE_REFUSE)

Move every release commit the release records name, and record that it happened.

The whole-repository half of the repair: it finds every release-archive directory the same walk the JSONL remap uses finds, remaps each one's release commits through commit_map, and appends a release-commit-remap transition record event beside each release record that moved.

Returns (remaps, touched) -- the :class:ReleaseCommitRemap records across every release record, and the repo paths a commit must carry (the rewritten archives plus the transition records that now name them).

Verification runs across ALL release records before the first write, so a content mismatch in one project leaves every project's archives untouched.

Search