Skip to content

refactor: separate core logic from UI layer#249

Merged
avihut merged 26 commits intomasterfrom
refactor
Feb 21, 2026
Merged

refactor: separate core logic from UI layer#249
avihut merged 26 commits intomasterfrom
refactor

Conversation

@avihut
Copy link
Owner

@avihut avihut commented Feb 21, 2026

Summary

  • Extract business logic from all command files into src/core/ modules with the Output trait abstraction
  • Split monolithic files (git.rs, hooks.rs, completions.rs, yaml_executor.rs, hook_progress.rs) into focused submodules
  • Convert all remaining command files from raw println!/eprintln! to the Output trait, completing the UI/logic separation

Phase breakdown (25 commits):

  1. Split monolithic modules into focused submodules (git, hooks, completions, yaml_executor, hook_progress)
  2. Define core abstractions (ProgressSink, HookRunner, OutputSink)
  3. Extract business logic from 10 worktree commands into src/core/worktree/
  4. Move shared modules (config, settings, remote, multi_remote) into src/core/
  5. Consolidate lib.rs utility functions
  6. Convert remaining 12 command files (hooks/*, multi_remote, shortcuts, setup, doctor) to Output trait

Design decisions

  • Interactive prompts (print! + stdin) stay outside the Output trait
  • Functional stdout (shell_init, completions, docs) is exempt
  • Doctor retains its own quiet/verbose filtering; Output is just the print sink
  • shortcuts::enable_style() API changed from verbose: bool to output: &mut dyn Output

Test plan

  • mise run fmt - formatting clean
  • mise run clippy - zero warnings
  • mise run test:unit - all 364 tests pass
  • Pre-commit hooks pass on all 6 commits (fmt-check, clippy, verify-man, verify-cli-docs)
  • Grep audit: only approved println!/eprintln! remain in src/commands/

🤖 Generated with Claude Code

avihut and others added 26 commits February 21, 2026 15:36
The post-clone hook was installing lefthook directly via Homebrew, but
mise.toml already manages lefthook. Instead, install mise itself
(via Homebrew on macOS, via install script on Linux) and let `mise
install` provision all tools including lefthook.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Split the 1,132-line git.rs into a git/ module with focused submodules:
- mod.rs: GitCommand struct, constructor, gitoxide discovery
- config.rs: config_get, config_set, config_unset, config_get_global
- refs.rs: show_ref_exists, for_each_ref, symbolic_ref, rev_parse, rev_list, merge_base, cherry
- remote.rs: fetch, push, pull, ls_remote, remote_list, remote_get_url
- worktree.rs: worktree_add/remove/list/move, find_worktree_for_branch, resolve_worktree_path
- branch.rs: branch_delete, branch_list_verbose, checkout
- stash.rs: has_uncommitted_changes, stash_push/pop/apply/drop
- clone.rs: clone_bare, init_bare, setup_fetch_refspec
- oxide.rs: moved from git_oxide.rs (gitoxide backend implementations)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Split the monolithic hook_progress.rs (1,075 lines) into focused submodules:
- formatting.rs: color constants, duration formatting, header/summary box drawing
- interactive.rs: HookProgressRenderer (indicatif-based rich terminal output)
- plain.rs: PlainHookRenderer (CI/pipe-friendly plain text output)
- mod.rs: shared types (JobOutcome, JobResultEntry), HookRenderer enum, standalone functions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Introduce src/core/ module with traits that allow core business logic to
report progress and trigger hooks without depending on Output or HookExecutor:
- ProgressSink trait with NullSink (no-op) implementation
- OutputSink adapter bridging ProgressSink to the Output trait
- HookRunner trait with NoopHookRunner implementation
- HookOutcome simplified result type for core operations

These abstractions will be used in Phase 3 when extracting command business
logic into core::worktree modules.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move carry command's business logic into core::worktree::carry module:
- CarryParams/CarryResult structs define the operation interface
- execute() function uses ProgressSink instead of direct println!
- Command layer becomes thin orchestrator: parse args, call core, render

This is the template for extracting all remaining commands.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move init command's business logic into core::worktree::init module:
- InitParams/InitResult structs define the operation interface
- execute() function uses ProgressSink instead of direct Output calls
- Command layer orchestrates: parse args, call core, render, run exec, cd_path
- All existing tests continue to pass via run_with_output

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Introduces CommandBridge adapter that implements both ProgressSink and
HookRunner, allowing core operations to trigger hooks without depending
on concrete hook or output implementations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Hooks remain in the command layer due to clone-specific trust semantics
(--trust-hooks, --no-hooks, check_hooks_notice).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move eject logic (worktree parsing, target resolution, dirty checks,
file staging, index initialization) into src/core/worktree/flow_eject.rs.
Command file becomes a thin orchestrator using CommandBridge for
per-worktree pre/post-remove hook execution.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move prune logic (gone branch detection, worktree removal with dirty
checks, deferred branch handling, CD target resolution, empty parent
cleanup) into src/core/worktree/prune.rs. Command file becomes a thin
orchestrator using CommandBridge for per-worktree removal hooks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move all branch-delete business logic (argument resolution, validation,
merge checking, deletion execution, hook invocation) from
commands/branch_delete.rs into core/worktree/branch_delete.rs. The
command file becomes a thin orchestrator that parses args, creates a
CommandBridge, calls core::execute(), and renders the structured result.

Public types: BranchDeleteParams, BranchDeleteResult, DeletionResult,
ValidationError. The core uses ProgressSink for progress messages and
HookRunner for pre/post-remove hooks with RemovalReason::Manual.

All 11 unit tests moved to the core module alongside the types they test.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move settings, remote, config, and multi_remote modules from src/ into
src/core/ where they belong as business logic. Use re-exports in lib.rs
to maintain backward-compatible import paths.

Refactor multi_remote/migration.rs to use ProgressSink instead of Output
trait, completing the core/UI separation for that module.

Modules moved:
- settings.rs → core/settings.rs
- remote.rs → core/remote.rs
- config.rs → core/config.rs
- multi_remote/ → core/multi_remote/

Skipped hints.rs and update_check.rs (UI-layer dependencies).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move all repository query functions, URL parsing, and dependency checking
from src/lib.rs into src/core/repo.rs. Move WorktreeConfig to
src/core/worktree/mod.rs. Re-export everything from lib.rs for backward
compatibility.

lib.rs is now a thin layer containing only: VERSION/CD_FILE_ENV constants,
get_clap_args() (CLI-specific), module declarations, and re-exports.

Removed unused functions (ensure_directory_exists, cleanup_on_error,
change_to_original_dir) that duplicated src/utils.rs functionality.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rait

Add `output: &mut dyn Output` parameter to cmd_dump, cmd_validate,
cmd_install, and cmd_migrate. Wire up CliOutput in hooks/mod.rs run()
and pass it to all subcommand handlers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add `output: &mut dyn Output` parameter to cmd_run, cmd_status, and
all trust subcommands. Interactive prompts remain as print! + stdin.
Pager output path preserved as-is with output.raw() for non-pager path.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Create CliOutput in cmd_status and cmd_set_default instead of using
raw println. Use detail(), list_item(), and result() for structured
output. cmd_enable and cmd_disable already used the Output trait.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ilent

Replace `verbose: bool` parameter on enable_style() with
`output: &mut dyn Output`. Remove create_symlink_silent() - callers
that want silent behavior pass a quiet CliOutput instead.

All internal functions (create_symlink, remove_symlink, cmd_status,
cmd_list, cmd_enable, cmd_disable, cmd_only) now accept output parameter.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Create CliOutput in run() and use output methods for all messages.
Pass quiet CliOutput to enable_style() so shortcut creation is silent
during setup (setup prints its own summary). Interactive prompt stays
as print! + stdin.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Doctor retains its own quiet/verbose filtering logic - Output is just
the print sink with quiet=false. All println!/print! calls in
print_results, print_summary, preview_fixes, and apply_fixes now go
through output.info() and output.warning().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@avihut avihut added this to the v1.1.0 milestone Feb 21, 2026
@avihut avihut added the refactor Code refactoring label Feb 21, 2026
@avihut avihut self-assigned this Feb 21, 2026
@avihut avihut merged commit cd8755e into master Feb 21, 2026
6 checks passed
@avihut avihut deleted the refactor branch February 21, 2026 16:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

refactor Code refactoring

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant