Skip to content

feat(workspace): add cleanup lifecycle (clean, prune, protect, unprotect) - #24

Merged
reckerp merged 6 commits into
mainfrom
feat/workspace-clean-lifecycle
Jun 29, 2026
Merged

feat(workspace): add cleanup lifecycle (clean, prune, protect, unprotect)#24
reckerp merged 6 commits into
mainfrom
feat/workspace-clean-lifecycle

Conversation

@reckerp

@reckerp reckerp commented Jun 29, 2026

Copy link
Copy Markdown
Owner

What

Adds the Workspace Cleanup Lifecycle so worktree-heavy development stays maintainable over time. Workspaces accumulate quickly across reviews, fixes, experiments, and agent tasks, and removing them by hand requires inspecting each one. This lets gx answer which workspaces are old, which are unsafe (local changes), which branches no longer have a worktree, which must never be removed, and shows exactly what will be deleted before anything destructive happens.

New gx workspace subcommands:

  • clean — interactive multi-section picker (workspaces, local branches without workspaces, orphan branches whose remote tracking branch is gone). Protected, main, current, and locked rows are shown but disabled with a tag explaining why, so a skipped row never looks like a scanner miss.
  • clean --auto — removes every workspace that passes the safety checks, with a single final confirmation. --use-threshold limits it to workspaces older than threshold_days; --dry-run reports without deleting; --force bypasses the dirty/untracked/unpushed checks but never the main, current, locked, or protected guards.
  • prune [--dry-run --no-branches] — runs git worktree prune, then deletes safe orphan branches after confirmation.
  • protect [branch] / unprotect [branch] — manage the configured protected-branch list, defaulting to the current branch and erroring on detached HEAD.

Safety model

A workspace is safe to remove only when it is not the main worktree, not the current worktree, not locked, not on a protected branch, and has no uncommitted, untracked, or unpushed work. Branches that are always protected regardless of config: the configured default branch, main, master, the current branch, and any branch checked out in an active worktree. Orphan branches with no upstream are treated as unsafe because their commits exist only locally.

Age uses a conservative calculation: the smaller of days since the branch's last commit and days since the workspace .git pointer was last modified, so a fresh workspace created off an old branch is treated as fresh.

Config

[workspace]
protected_branches = ["staging", "release"]

[workspace.clean]
threshold_days = 7
auto = false

All fields default via serde, so existing config files keep loading unchanged.

Implementation

  • src/args.rs — new subcommands and flags.
  • src/git/worktree.rs — branch listing, unpushed/upstream checks, conservative age calculation, orphan and gone-branch detection, metadata prune.
  • src/commands/workspace_clean.rs — command handlers; pure safety and prune-eligibility logic.
  • src/ui/clean_picker.rs — multi-section picker reusing the background summary lookup and stderr terminal setup.
  • src/config.rs[workspace.clean] table, protected_branches, and config::store.

Reuses ui::confirm, the existing summary lookup, terminal helpers, and the config loader. All human-readable output goes to stderr; stdout stays reserved for shell navigation (the clean handler emits a navigation target only when the current workspace is removed).

Testing

cargo build and cargo test both pass (162 tests). New unit tests cover: main worktree never removed (even with --force), current worktree never removed, dirty/untracked/unpushed skipped without --force, locked skipped, protected branch skipped, orphan branch deletion gated on safety + confirmation, orphan with no upstream treated as unsafe, fresh workspace off an old branch not stale, gone-branch parsing, and the picker's row/section/selection logic.

reckerp added 6 commits June 29, 2026 22:03
Extend WorkspaceConfig with a [workspace.clean] table (threshold_days,
auto) and a [workspace] protected_branches list. Add config::store so the
protect/unprotect commands can persist the protected-branch list back to
the global gx config. All fields default via serde so existing config
files keep loading unchanged.
Add the git helpers the cleanup lifecycle needs:
- list_local_branches and branches_without_worktree for orphan detection
- has_upstream/unpushed_count/has_unpushed for safety checks
- orphan_branches annotating each branch with upstream/unpushed state
- remote_gone_branches (parses for-each-ref upstream:track == [gone])
- workspace_age_days using the smaller of branch-tip age and the .git
  pointer mtime, so a fresh workspace off an old branch reads as fresh
- prune_metadata wrapping git worktree prune from the main root

Pure helpers (age_days_from, branches_without_worktree, parse_gone_branches)
are extracted and unit-tested.
Wire the cleanup-lifecycle CLI surface under 'gx workspace':
- clean [--auto --dry-run --use-threshold --force]
- prune [--dry-run --no-branches]
- protect [branch]
- unprotect [branch]

Each subcommand dispatches to the new workspace_clean handler.
Add the interactive cleaner TUI with three sections: workspaces, local
branches without workspaces, and orphan branches whose upstream is gone.
Protected, main, current, and locked rows are shown but disabled with a
tag explaining why, so a skipped row never looks like a scanner miss.
Reuses the background summary lookup and stderr terminal setup. Row
building, sectioning, safety tagging, and selection-to-action resolution
are pure and unit-tested.
Add the workspace_clean command module:
- clean: interactive multi-section picker, or --auto to remove every
  workspace passing the safety checks (final confirmation still required).
  --use-threshold limits --auto to workspaces older than threshold_days;
  --force bypasses dirty/untracked/unpushed but never the main, current,
  locked, or protected guards.
- prune: git worktree prune, then delete safe orphan branches after
  confirmation; --no-branches stops after metadata cleanup; --dry-run
  reports without deleting.
- protect/unprotect: manage the configured list, defaulting to the current
  branch and erroring on detached HEAD; advise when a branch stays
  implicitly protected.

Reuse ui::confirm and keep all human output on stderr; expose
main_worktree_root, delete_local_branch, and print_go_path from the
existing workspace module. delete_local_branch now treats an
already-deleted branch as success during multi-step cleanup. Safety and
prune-eligibility logic is pure and unit-tested.
…lifecycle

# Conflicts:
#	src/args.rs
#	src/git/worktree.rs
@reckerp
reckerp merged commit 7c7c9b8 into main Jun 29, 2026
6 checks passed
@reckerp
reckerp deleted the feat/workspace-clean-lifecycle branch June 29, 2026 20:15
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.

1 participant