Skip to content

refactor: slim RhizaTemplate to a pure data class#427

Merged
tschm merged 3 commits intomainfrom
copilot/refactor-rhizatemplate-class
Mar 9, 2026
Merged

refactor: slim RhizaTemplate to a pure data class#427
tschm merged 3 commits intomainfrom
copilot/refactor-rhizatemplate-class

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 9, 2026

RhizaTemplate had grown to 597 lines mixing data modelling, git operations, file-tree utilities, workflow orchestration, and command-layer logic. This PR redistributes those responsibilities to their natural homes.

Changes

src/rhiza/models/template.py — 213 → 53 statements

  • Stripped to data fields + from_config, config, git_url, resolve_include_paths
  • Removed _files/files mutation pattern (was a surprising side-effect of clone())
  • Removed deferred from rhiza.commands.validate import validate circular-import workaround

src/rhiza/models/_git_utils.py

  • Module-level functions: _expand_paths, _excluded_set, _prepare_snapshot (were @staticmethod on RhizaTemplate with no self usage)
  • GitContext instance methods: update_sparse_checkout, get_head_sha, clone_repository (from _clone_template_repository), clone_at_sha (from _clone_at_sha) — all now take git_url: str explicitly instead of reading self.git_url
  • _merge_with_base updated to call self.clone_at_sha(template.git_url, ...) and module-level _prepare_snapshot(...) directly

src/rhiza/commands/sync.py

  • _load_template_from_project(target) — extracted from RhizaTemplate.from_project
  • _clone_template(template, git_ctx, branch) — extracted from RhizaTemplate.clone; returns (upstream_dir, upstream_sha, resolved_include) without mutating the template object
  • Snapshot logic inlined via _excluded_set + _prepare_snapshot
  • dataclasses.replace used to pass a bundle-resolved template view to sync_merge/_merge_with_base
# Before — mutation-based, opaque side-effect
upstream_dir, upstream_sha = template.clone(git_ctx, branch=branch)
# template.include is now silently rewritten to resolved bundle paths

# After — explicit, no mutation
upstream_dir, upstream_sha, resolved_include = _clone_template(template, git_ctx, branch=branch)
resolved_template = dataclasses.replace(template, include=resolved_include, templates=[])

Tests

Mock paths updated throughout:

  • RhizaTemplate._clone_at_shaGitContext.clone_at_sha
  • RhizaTemplate._clone_template_repositoryGitContext.clone_repository
  • RhizaTemplate._get_head_shaGitContext.get_head_sha
  • RhizaTemplate._prepare_snapshotrhiza.models._git_utils._prepare_snapshot
  • populate_base(sha, dest, include_paths, git_ctx_)populate_base(git_url, sha, dest, include_paths) to match the new GitContext.clone_at_sha signature
  • patch.object(RhizaTemplate, "clone", ...)patch("rhiza.commands.sync._clone_template", ...) with 3-tuple return
Original prompt

Goal

RhizaTemplate has grown into a god class — it is both a data model and a git operations engine. This refactor slims it down to a clean data class by redistributing its non-data responsibilities to better homes.


Current state of src/rhiza/models/template.py

RhizaTemplate currently has 597 lines and contains:

  1. Data fields + serialisation (from_config, config, git_url) — ✅ belongs here
  2. Pure file-tree utilities (_expand_paths, _excluded_set, _prepare_snapshot) — all @staticmethod, no self usage
  3. Generic git operations (_update_sparse_checkout, _get_head_sha) — all @staticmethod, operate on GitContext
  4. Git clone procedures (_clone_template_repository, _clone_at_sha) — instance methods that only use self.git_url
  5. Command-layer logic (from_project) — calls validate() via a deferred circular import, logs, raises RuntimeError
  6. Workflow orchestration (clone, snapshot) — clone() mutates self.include as a side-effect
  7. Bundle resolution (resolve_include_paths) — uses self.templates and self.include, reasonable to keep

What to do

1. Promote _expand_paths, _excluded_set, _prepare_snapshot to module-level functions in _git_utils.py

These are pure @staticmethod utilities with no dependency on RhizaTemplate. Move them to src/rhiza/models/_git_utils.py as module-level functions (not on a class).

Note: _git_utils.py already calls RhizaTemplate._prepare_snapshot(...) in _merge_with_base — once moved to module-level, that call simplifies to just _prepare_snapshot(...).

2. Move _update_sparse_checkout and _get_head_sha onto GitContext

Both are @staticmethod on RhizaTemplate but only interact with git via GitContext. Move them as regular instance methods on GitContext in _git_utils.py.

Update the one call site in clone() accordingly.

3. Move _clone_template_repository and _clone_at_sha onto GitContext

These are instance methods that only touch self.git_url. Refactor them to GitContext methods that accept git_url: str as an explicit parameter instead.

Update all call sites (clone() in template.py, _merge_with_base in _git_utils.py).

4. Move from_project to src/rhiza/commands/sync.py

from_project does not belong on a model — it calls validate() (which requires a deferred import to avoid a circular dependency), performs logging, and raises RuntimeError. Extract it as a standalone function _load_template_from_project(target, branch) in sync.py (or a small helper module), and update sync() to call it directly.

Remove the deferred from rhiza.commands.validate import validate import from template.py.

5. Inline or remove clone and snapshot from RhizaTemplate

  • snapshot is a 2-line wrapper around _excluded_set + _prepare_snapshot. Once those are module-level, inline the two calls directly into sync.py where template.snapshot(...) is currently called.
  • clone mutates self.include as a side-effect (line 564: self.include = resolved_paths), which is surprising on a data class. Extract clone as a standalone function _clone_template(template, git_ctx, branch) in sync.py that returns (upstream_dir, upstream_sha, resolved_include) without mutating the template object. Update sync.py to use this function.

6. Keep on RhizaTemplate

  • All data fields
  • from_config classmethod
  • config property
  • git_url property
  • resolve_include_paths method (pure logic on self.templates/self.include)

Files affected

  • src/rhiza/models/template.py — shrinks to ~80 lines
  • src/rhiza/models/_git_utils.py — gains _expand_paths, _excluded_set, _prepare_snapshot as module-level functions; GitContext gains _update_sparse_checkout, _get_head_sha, clone_repository (from _clone_template_repository), clone_at_sha (from _clone_at_sha)
  • src/rhiza/commands/sync.py — gains _load_template_from_project, inlines snapshot logic, gains _clone_template function
  • Update TYPE_CHECKING imports in _git_utils.py accordingly (remove RhizaTemplate import once no longer needed there)

Constraints

  • All existing tests must continue to pass without modification (the public API of sync(), RhizaTemplate, GitContext must remain compatible)
  • If tests mock RhizaTemplate._clone_template_repository, RhizaTemplate._get_head_sha, RhizaTemplate._prepare_snapshot, or RhizaTemplate._clone_at_sha, update those mocks to point at the new locations
  • Do not change any behaviour — this is a pure structural refactor

This pull request was created from Copilot chat.


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

@tschm tschm marked this pull request as ready for review March 9, 2026 10:20
Copilot AI and others added 2 commits March 9, 2026 10:28
…lass

- Move _expand_paths, _excluded_set, _prepare_snapshot to module-level functions in _git_utils.py
- Add GitContext.update_sparse_checkout, get_head_sha, clone_repository, clone_at_sha instance methods
- Update _merge_with_base to use new GitContext methods and module-level _prepare_snapshot
- Move from_project to _load_template_from_project in sync.py
- Extract clone as _clone_template(template, git_ctx, branch) in sync.py returning (upstream_dir, upstream_sha, resolved_include)
- Inline snapshot logic in sync.py
- Add resolve_include_paths method to RhizaTemplate
- Slim template.py to ~53 statements (data fields, from_config, config, git_url, resolve_include_paths)
- Update all test mocks to point to new locations (GitContext.clone_at_sha, etc.)
- All 491 tests pass with 100% coverage

Co-authored-by: tschm <2046079+tschm@users.noreply.github.com>
… functions

Remove _logger optional parameter from _expand_paths, _load_template_from_project,
and _clone_template; use module-level loguru logger directly for consistency
with the rest of the codebase.

Co-authored-by: tschm <2046079+tschm@users.noreply.github.com>
Copilot AI changed the title [WIP] Refactor RhizaTemplate to separate data and operations refactor: slim RhizaTemplate to a pure data class Mar 9, 2026
Copilot finished work on behalf of tschm March 9, 2026 10:32
@tschm tschm merged commit cfb87c5 into main Mar 9, 2026
15 checks passed
@tschm tschm deleted the copilot/refactor-rhizatemplate-class branch March 9, 2026 10:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants