rlsbl v0.113.0 /rlsbl.changelog.files
On this page

File management layer for JSONL changelog files including reading, writing, appending entries, and path resolution for .rlsbl directories.

#rlsbl.changelog.files

#rlsbl.changelog.files

File management layer for JSONL changelog files including reading, writing, appending entries, and path resolution for .rlsbl directories.

#RemapResult

Result of remapping hashes in one JSONL file.

#RemapReport

Full report of a remap across all JSONL files in one changes dir.

results lists the files that were modified. unmapped and ambiguous record, per file path, hashes that could NOT be mapped: hashes matching no rewrite key, and abbreviated hashes matching more than one rewrite key, respectively. Callers must decide whether unmapped hashes are a problem (e.g. by checking that they still resolve after the rewrite).

#_map_hash

python
def _map_hash(h: str, sha_map: dict) -> 'tuple[str | None, bool]'

Map one (possibly abbreviated) hash through the rewrites map.

Returns (new_sha, ambiguous). new_sha is None when the hash matched no key; ambiguous is True when an abbreviated hash matched more than one key.

#can_remap_hash

python
def can_remap_hash(h: str, sha_map: dict) -> bool

Whether remap_jsonl_hashes could fix hash h with sha_map.

True when the (possibly abbreviated) hash matches exactly one key of the map. Used by the scrub recovery path to decide whether a rewrite journal can repair a dangling changelog hash before mutating any files.

#_parse_semver

python
def _parse_semver(filename: str) -> _SemverKey | None

Extract a sort key from a versioned filename, or None.

Returns (major, minor, patch, is_stable, preid_rank, counter) where is_stable is 1 for stable versions (so they sort after pre-releases) and preid_rank maps alpha=0, beta=1, rc=2.

#get_changes_dir

python
def get_changes_dir(project_path: str) -> str

Return the path to .rlsbl/changes/ inside the project.

#enumerate_changelog_dirs

python
def enumerate_changelog_dirs(project_root, workspace_root=None, workspace_projects=None)

Enumerate every changelog changes-dir whose JSONL files may reference commit hashes.

Standalone: the project's own .rlsbl/changes/. Monorepo: every workspace project's .rlsbl/changes/ PLUS every releasable's .rlsbl-monorepo/releasables/<name>/changes/ (enumerated from disk, so coverage matches what is actually in the working tree).

workspace_projects may be passed by callers that already loaded the workspace; when omitted it is loaded from workspace_root.

Only directories that exist are returned.

#changelog_remap_globs

python
def changelog_remap_globs(project_root, workspace_root=None, workspace_projects=None)

Build the safegit --remap-shas-in glob list for a scrub.

The globs are repo-relative and use Go path.Match semantics (safegit's matchScope): * never crosses /, so every glob is an exact per-directory pattern. Coverage is derived from the SAME enumeration as hash validation (enumerate_changelog_dirs) so remap coverage and validation coverage can never diverge:

  • Standalone: .rlsbl/changes/*.jsonl -- emitted unconditionally, since

historical commits may contain changelog files even when the working tree currently has none.

  • Monorepo: one exact glob per enumerated per-project changes dir, plus a

single wildcard glob .rlsbl-monorepo/releasables/*/changes/*.jsonl that also covers releasables deleted from the working tree but still present in history.

DELIBERATELY EXCLUDED: committed scrub archives (.rlsbl/scrubs/*.json and the releasable-level equivalent). They are records of what WAS -- the old-side SHAs they record dangle by design as soon as the original scrub prunes the old objects, so remapping them on a later scrub would falsify the record without ever making the old side resolvable. Validation (validate_all_hashes_resolve) likewise never reads them, so remap and validation agree on the exclusion. .validated caches carry no extension match and are deleted by the scrub flow anyway.

#_list_jsonl_files

python
def _list_jsonl_files(changes_dir)

All JSONL files in a changes dir: unreleased.jsonl plus versioned.

#validate_all_hashes_resolve

python
def validate_all_hashes_resolve(dirs, *, repo_root)

Verify that every commit hash in every JSONL file resolves via git.

Runs git rev-parse for each distinct hash in repo_root — the repository the hashes belong to. The parameter is mandatory so callers (including the planned validation-only mode) can never accidentally resolve against whatever repo the process CWD happens to be in.

Returns {filepath: [unresolvable hashes]} — empty when everything resolves.

#changes_dir_exists

python
def changes_dir_exists(project_path: str) -> bool

Check if .rlsbl/changes/ exists in the project.

#list_versioned_files

python
def list_versioned_files(changes_dir: str) -> list[tuple[str, str]]

List all versioned JSONL files, sorted by semver (newest first).

Matches both stable (x.y.z.jsonl) and pre-release (x.y.z-preid.N.jsonl) filenames.

Returns (version_string, filepath) pairs.

#read_unreleased

python
def read_unreleased(changes_dir: str, *, enforce_format_version: bool=False) -> list[ChangelogEntry]

Read unreleased.jsonl and return entries. Empty list if file missing.

enforce_format_version is threaded to :func:parse_jsonl: when True, a line lacking format_version is a hard error.

#append_entry

python
def append_entry(changes_dir: str, entry: ChangelogEntry) -> None

Append one entry to unreleased.jsonl atomically.

Writes the serialized line to a temp file, then appends it to the target. Creates the changes directory and unreleased.jsonl if they don't exist.

#append_entry_to_version

python
def append_entry_to_version(changes_dir: str, version: str, entry: ChangelogEntry) -> None

Append one entry to a versioned JSONL file (e.g., 0.39.0.jsonl).

The caller is responsible for unlocking/re-locking the file if it is read-only.

#_append_entry_to_file

python
def _append_entry_to_file(target: str, entry: ChangelogEntry) -> None

Append one entry to any JSONL file, creating parents when missing.

One append of one line, through the effect seam. It used to stage the line in a tempfile.mkstemp file and then copy that into the target, which bought nothing -- the copy was itself a plain append, so a crash mid-write could truncate the target either way -- and cost purity: mkstemp creates its file unconditionally, so under --dry-run the recorded cleanup never ran and the preview left a stray .tmp in .rlsbl/changes/.

#_warn_stale_entries

python
def _warn_stale_entries(src: str, tag_glob: str) -> None

Warn on stderr for entries in unreleased.jsonl referencing out-of-range commits.

In monorepo mode, an entry whose commits all sit before the project's last tag is stale — typically left over from a sibling project's release. We emit a warning per stale entry but do not strip them (warn-only).

#finalize_version

python
def finalize_version(changes_dir: str, version: str, tag_glob: str | None=None) -> None

Rename unreleased.jsonl to x.y.z.jsonl and create a fresh unreleased.jsonl.

Sets the versioned file read-only (chmod 0o444). Raises FileNotFoundError if unreleased.jsonl doesn't exist.

When tag_glob is provided (monorepo mode), inspects each entry in unreleased.jsonl before the rename and warns on stderr for any whose commits fall outside the current project's unreleased range. Warn-only: the stale entries are not stripped.

#unfinalize_version

python
def unfinalize_version(changes_dir: str, version: str) -> list[str]

Reverse a finalize_version: restore x.y.z.jsonl back to unreleased.jsonl.

  1. Makes the versioned file writable.
  2. Renames it to unreleased.jsonl.
  3. Deletes the per-version .md file if present.
  4. Returns the list of changed file paths (for committing).

Returns an empty list if the versioned file doesn't exist.

#is_read_only

python
def is_read_only(path: str) -> bool

Check if a file has no write permissions (for any user class).

#writable_jsonl

python
def writable_jsonl(path)

Context manager that temporarily makes a read-only JSONL file writable.

If the file is already writable, yields without changing permissions. On exit (even after exceptions), restores original read-only state.

#remap_jsonl_hashes

python
def remap_jsonl_hashes(changes_dir, sha_map) -> RemapReport

Replace commit hashes in all JSONL files using a rewrites mapping.

Scans unreleased.jsonl and all versioned *.jsonl files in changes_dir. Hashes are matched exactly against the (full-SHA) keys of sha_map; abbreviated hashes are matched by unique prefix. Only files containing matching hashes are modified. Uses writable_jsonl to handle read-only versioned files.

Returns a RemapReport: modified files plus, per file, the hashes that could not be mapped (no key match, or ambiguous abbreviated prefix). Returns an empty report if changes_dir does not exist.

#load_filter_repo_commit_map

python
def load_filter_repo_commit_map(path: str) -> 'tuple[dict[str, str], list[str]]'

Load a git-filter-repo commit-map into a clean {old: new} dict.

git-filter-repo writes .git/filter-repo/commit-map with two quirks that make it unsafe to feed straight into :func:remap_jsonl_hashes:

  • A header row of the literal tokens old and new (whitespace

padded). Ingested naively it becomes a junk {"old": "new"} entry.

  • Pruned commits map to the all-zeros :data:NULL_SHA. Ingested naively

they would rewrite surviving real hashes to nothing, corrupting the changelog. This is the actual corruption vector.

Returns (sha_map, pruned) where sha_map maps surviving old SHAs to their new SHAs (header and null-target rows excluded) and pruned is the list of old SHAs whose commits were dropped (null target), so callers can log how many entries reference now-deleted commits.

#read_changelog_format_version_enforced

python
def read_changelog_format_version_enforced(config: dict) -> 'tuple[bool, bool]'

Read the changelog_format_version_enforced flag from a config dict.

Returns (enforced, key_present):

  • key ABSENT -> (False, False): legacy mode. There is no enforced

default; the absence is surfaced by the changelog-format-version warn check ("enforcement not yet enabled").

  • key present and boolean -> (value, True).

A present-but-non-boolean value is a hard error (:class:ConfigError) -- invalid config is never silently coerced.

Search