rlsbl v0.113.0 /rlsbl.commands.monorepo.extract
On this page

Extract and absorb operations for moving packages in and out of monorepos, including history migration, changelog transfer, and workspace.toml updates.

#rlsbl.commands.monorepo.extract

#rlsbl.commands.monorepo.extract

Extract and absorb operations for moving packages in and out of monorepos, including history migration, changelog transfer, and workspace.toml updates.

Provides:

  • require_filter_repo(): dependency check for git-filter-repo
  • cmd_extract(): extract a package from a monorepo into a new repository
  • cmd_absorb(): absorb an external repository as a package in the monorepo
  • cmd_extract_releasable(): extract all member packages of a releasable

#ExtractError

Error during extract or absorb operations.

#require_filter_repo

python
def require_filter_repo()

Raise if git-filter-repo is not installed.

Checks that the git-filter-repo command is available on PATH. Raises ExtractError with install instructions if missing.

#_run_git

python
def _run_git(cwd, *args)

Run a git command and return stdout. Raises subprocess.CalledProcessError on failure.

#_run_filter_repo

python
def _run_filter_repo(cwd, *args)

Run git-filter-repo in cwd, wrapping failures in ExtractError.

Raw subprocess.CalledProcessError from a filter-repo run is an opaque stack trace to the caller; wrap it so the extract flow reports a clean, actionable error including filter-repo's own stderr.

#_ensure_git_identity

python
def _ensure_git_identity(clone_path, source_path)

Copy the source repo's committer identity into a fresh clone.

git clone does not carry over the source's local user.name / user.email, so a commit inside the clone can fail with "please tell me who you are" in environments without a global identity. We read the source's effective identity (local or global) and set it locally in the clone. If the source has none configured, the clone inherits whatever global identity exists (unchanged).

#_git_tag_list

python
def _git_tag_list(repo, pattern=None)

Return the list of tags in repo, optionally filtered by a glob.

#_commit_resolves

python
def _commit_resolves(repo, commit_hash)

Whether commit_hash resolves to an existing commit object in repo.

#_translate_extract_tags

python
def _translate_extract_tags(repo, own_glob, foreign_globs, *, keep_own)

Translate/prune tags in a freshly extracted repo.

A clone (then filter-repo) carries EVERY tag the monorepo had: the extracted package's own monorepo-scheme tags, foreign packages' scheme tags, and any pre-existing standalone v* tags -- all pointing at rewritten commits.

  • Tags matching own_glob are the extracted package's own tags. When

keep_own is False they are retagged as v{version} at the same commit and the monorepo-scheme original is deleted; when True (a multi-member releasable, whose releasable scheme stays valid) they are left untouched.

  • A tag matching one of foreign_globs (another CURRENT workspace

member's/releasable's resolved glob) is a genuine foreign artifact and is deleted.

  • A tag that parses as a monorepo/path-scheme version tag but matches NO

current member glob is KEPT with a log line. Such a tag is most likely this package's OWN historical release under an old prefix (e.g. after a releasable rename, oldname@vX). Deleting it would destroy the package's own release history, so the conservative rule keeps it.

  • Tags that do not parse as a version tag under any scheme (and standalone

v* tags that are not translation targets) are left in place, with a log line for genuinely unrecognized tags.

Collision: when keep_own is False and a translated v{version} name already exists (a pre-existing standalone tag), raise ExtractError naming both tags -- never silently clobber.

Returns (translated_or_kept, deleted, left) tag-name lists.

#_prune_dangling_entries

python
def _prune_dangling_entries(changes_dir, repo_root)

Drop changelog entries whose commits no longer resolve after a rewrite.

Runs AFTER :func:remap_jsonl_hashes has mapped every survivable hash to its post-rewrite SHA. Any commit that still fails to resolve in repo_root was pruned by the filter (or was never mappable):

  • An entry with at least one surviving commit is kept, narrowed to just the

resolving hashes (a partial survival, logged).

  • An entry whose EVERY commit fails to resolve is DROPPED entirely, with a

loud log line -- never left dangling with a null/stale hash.

Returns the number of entries dropped.

#_remap_and_prune

python
def _remap_and_prune(changes_dir, sha_map, repo_root)

Remap migrated JSONL hashes to post-rewrite SHAs and drop dangling entries.

Combines :func:remap_jsonl_hashes (map old monorepo SHAs to the extracted repo's new SHAs) with :func:_prune_dangling_entries (drop entries whose commits were pruned). Used by both extract commands so remap + prune stay identical across them.

#_commit_extracted_state

python
def _commit_extracted_state(repo)

Commit the migrated/remapped/retagged .rlsbl state in an extracted repo.

The extracted repo is a fresh, non-shared clone, so a plain committed snapshot is safe. Carries the Autogenerated trailer so the structural migration commit is exempt from changelog coverage checks in the new repo. Captures every working-tree change (workspace.toml/config edits, remapped JSONL, migrated changelog files). No-op when the tree is already clean.

#_resolve_own_tag_glob

python
def _resolve_own_tag_glob(workspace_root, project, projects)

Resolve the extracted package's own monorepo tag glob.

Uses the project's releasable tag_format when the workspace is in explicit mode and the project belongs to a releasable; otherwise the project's first detected target's monorepo glob.

Assumes the package's target config was already validated up front by :func:validate_extract_preconditions. A broken target declaration is a hard error THERE (before any history rewrite), never a silent fallback here -- so this helper never has to swallow a ConfigError.

#_other_member_globs

python
def _other_member_globs(workspace_root, projects, releasables, *, exclude_project_names, exclude_releasable_names=frozenset())

Resolve the tag globs of every workspace member/releasable NOT extracted.

Returns a set of git tag globs (e.g. {"pkgB@v*", "core@v*"}) covering every OTHER live member and releasable in the workspace. These globs identify which scheme-parsing tags in a freshly extracted repo are genuinely foreign (another live member's release history -- safe to prune) versus orphan tags matching no current member. An orphan is most likely the extracted package's OWN pre-rename history under an old prefix, which must be kept.

Must be called against the SOURCE workspace BEFORE the extracted project is removed from workspace.toml (target detection reads the still-present member dirs).

#_assert_extract_target_config_valid

python
def _assert_extract_target_config_valid(workspace_root, project, projects)

Hard-error UP FRONT if the package's target declaration is broken.

A broken declaration is a .rlsbl/config.json that exists but has no targets key (detect_targets raises ConfigError). A package with NO config file at all is fine -- targets auto-detect. Releasable members derive their tag scheme from the releasable tag_format and skip target detection entirely, so they are never broken here.

Raising here (in the precondition gate, before the clone/filter-repo runs) is what replaces the old silent fallback to the {name}@v* scheme.

#_find_project

python
def _find_project(projects, package_name)

Find a project by name in the workspace project list.

Returns the WorkspaceProject or raises ExtractError.

#_get_default_branch

python
def _get_default_branch(cwd)

Detect the default branch name (main or master) of a repo.

#_filter_changelog_entries

python
def _filter_changelog_entries(entries, package_path, repo_root)

Filter changelog entries to those touching a specific package path.

An entry matches if:

  • It has a packages field containing the package name, OR
  • Any of its commits touch files under the package path (checked via

git diff-tree if repo_root is provided and the commit exists).

When we cannot determine relevance (e.g. no packages field and commits are not resolvable), the entry is included (conservative approach).

#_migrate_changelog_to_new_repo

python
def _migrate_changelog_to_new_repo(source_changes_dir, target_changes_dir, package_path, repo_root)

Migrate changelog entries relevant to a package into a new repo's changes dir.

Reads unreleased.jsonl and all versioned JSONL files from source_changes_dir, filters entries to those relevant to the package, and writes them into target_changes_dir.

Returns (files_written, entries_migrated) tuple.

#_create_rlsbl_config

python
def _create_rlsbl_config(target_path, source_config_path=None)

Create a .rlsbl/ config in the target repo.

If source_config_path is provided and exists, copies relevant config. Otherwise creates a minimal config.

#_remove_project_from_workspace

python
def _remove_project_from_workspace(workspace_root, package_name, projects)

Remove a project from workspace.toml by name.

Returns the updated project list.

#validate_extract_preconditions

python
def validate_extract_preconditions(workspace_root, package_name, target_repo_path)

Validate that extraction can proceed.

Checks:

  • Package exists in workspace.toml
  • Target path does not already exist
  • git-filter-repo is installed

Returns (projects, project) tuple.

#cmd_extract

python
def cmd_extract(workspace_root, package_name, target_repo_path, *, dry_run=False)

Extract a package from the monorepo into a new repository.

Steps:

  1. Validate package exists in workspace.toml
  2. Clone the monorepo to target_repo_path
  3. Run git filter-repo --path <pkg-dir> on the clone to keep only

that package's history

  1. Migrate changelog: filter JSONL entries to those touching the

extracted package

  1. Create .rlsbl/ config in the new repo
  2. Update source monorepo: remove project from workspace.toml

Args:

  • workspace_root: path to the monorepo root.
  • package_name: name of the package to extract.
  • target_repo_path: path where the new repo will be created.
  • dry_run: if True, validate but do not perform the extraction.

Returns:

  • A dict with extraction details: package_name, target_path,
  • entries_migrated, files_written.

#validate_absorb_preconditions

python
def validate_absorb_preconditions(workspace_root, source_repo_path, dest_path, name)

Validate that absorption can proceed.

Checks (mirrors monorepo add for uniqueness):

  • git-filter-repo is installed (the history rewrite depends on it)
  • Source repo exists and is a git repo
  • Source working tree is CLEAN -- the history filter only captures

committed state, so uncommitted changes would be silently dropped

  • Destination path is not already registered in workspace.toml
  • Package name is not already registered in workspace.toml

Returns projects list.

#_assert_absorb_source_config_valid

python
def _assert_absorb_source_config_valid(source_repo_path)

Hard-error UP FRONT (before any history rewrite) if the source repo's target declaration is broken.

The source's .rlsbl/config.json becomes the absorbed package's config, so a broken declaration (config file present but no targets key) would otherwise surface only LATER -- after the merge landed -- as a silently mis-schemed tag import. A source with NO config file at all is the legitimate auto-detect path and passes.

Raising here replaces the old silent fallback to the {name}@v{version} scheme in :func:_resolve_monorepo_tag_target.

#_collect_source_tags

python
def _collect_source_tags(source_repo_path)

Return [(tag_name, commit_sha), ...] for every tag in the source repo.

commit_sha is the (full) commit the tag resolves to -- annotated tags are dereferenced to their target commit via git rev-list.

#_resolve_monorepo_tag_target

python
def _resolve_monorepo_tag_target(workspace_root, dest_path, name)

Return a callable version -> monorepo tag string for the absorbed pkg.

The tag scheme is resolved from the absorbed package's first detected target (Go uses path-style {path}/v{version}; others use {name}@v{version}). Uses the default {name}@v{version} scheme when no target is detected (a source with no config at all).

Assumes the source's target config was validated up front by :func:validate_absorb_preconditions; a broken declaration is a hard error there (before the merge), so this helper never swallows a ConfigError.

#cmd_absorb

python
def cmd_absorb(workspace_root, source_repo_path, dest_path, *, name=None, registry_name='', releasable_name=None, dry_run=False)

Absorb an external repository as a package in the monorepo.

Rewrites the source's history to live under dest_path (rather than a verbatim subtree add), preserving its full commit history with rewritten paths, importing its version tags under the monorepo tag scheme, and remapping its JSONL changelog hashes to the new (post-rewrite) commits.

Steps:

  1. Validate preconditions (filter-repo present, source clean, uniqueness).
  2. Temp-clone the source and run ``git-filter-repo

--to-subdirectory-filter `` to relocate all files/history.

  1. Fetch + merge (--allow-unrelated-histories) the rewritten history

into the monorepo -- this is the first commit.

  1. Delete the bare v* tags the merge auto-followed in, and re-create

version tags under the monorepo scheme at the mapped commits.

  1. Remap the arriving JSONL changelog hashes to the new commits. In

releasable mode, move the package's changes into the releasable's changes dir and remove the per-package residue via saferm.

  1. Register the project in workspace.toml and commit the follow-up

(workspace.toml + remap edits + residue removals).

Args:

  • workspace_root: path to the monorepo root.
  • source_repo_path: path to the external repository.
  • dest_path: directory prefix (and workspace path) for the absorbed pkg.
  • name: workspace project name (default: basename of dest_path).
  • registry_name: optional registry identity recorded in workspace.toml.
  • releasable_name: optional releasable to assign the package to.
  • dry_run: if True, validate and report but do not perform the absorption.

Returns:

  • A dict with absorption details.

#_count_jsonl_entries

python
def _count_jsonl_entries(changes_dir)

Total number of changelog entries across all JSONL files in a dir.

#_merge_changes_into_releasable

python
def _merge_changes_into_releasable(pkg_changes_dir, rel_changes_dir)

Move a package's arriving JSONL changes into a releasable's changes dir.

Unreleased entries are appended to the releasable's unreleased.jsonl. Versioned files are copied over (skipped if a same-version file already exists in the releasable dir, which would indicate a real collision the operator must resolve). Returns the number of entries moved.

#_commit_absorb_followup

python
def _commit_absorb_followup(workspace_root, name)

Commit every working-tree change left after the absorb merge.

Captures the workspace.toml entry, remapped JSONL edits, moved changelog files, and saferm'd residue deletions in a single follow-up commit. Uses the Autogenerated trailer so the structural commit is exempt from changelog coverage checks.

#cmd_extract_releasable

python
def cmd_extract_releasable(workspace_root, releasable_name, target_repo_path, *, dry_run=False)

Extract all member packages of a releasable into a new repository.

If the releasable has one member, the result is a single-project repo. If it has multiple members, the result is a monorepo with workspace.toml.

Steps:

  1. Resolve releasable members from workspace.toml
  2. Clone the monorepo
  3. Run git filter-repo --path <dir1> --path <dir2> ... for all

member paths

  1. Migrate changelog for each member
  2. Create appropriate config (single-project or monorepo)
  3. Update source monorepo: remove all member projects from workspace.toml

Args:

  • workspace_root: path to the monorepo root.
  • releasable_name: name of the releasable to extract.
  • target_repo_path: path where the new repo will be created.
  • dry_run: if True, validate but do not perform the extraction.

Returns:

  • A dict with extraction details.
Search