Module: Carson::Runtime::Housekeep
- Included in:
- Carson::Runtime
- Defined in:
- lib/carson/runtime/housekeep.rb
Instance Method Summary collapse
-
#housekeep!(json_output: false, dry_run: false) ⇒ Object
Serves the current repo: sync + prune.
-
#housekeep_all!(json_output: false, dry_run: false) ⇒ Object
Knocks each governed repo’s gate in turn.
-
#housekeep_one_dry_run ⇒ Object
Prints a dry-run plan for this repo without making any changes.
-
#housekeep_target!(target:, json_output: false, dry_run: false) ⇒ Object
Resolves a target name to a governed repo, then serves it.
-
#reap_dead_worktrees! ⇒ Object
Removes dead worktrees — those whose content is on main, with merged PR evidence, or with closed-unmerged PR evidence and no open PR.
-
#reap_dead_worktrees_plan ⇒ Object
Returns a plan array describing what reap_dead_worktrees! would do for each non-main worktree, without executing any mutations.
Instance Method Details
#housekeep!(json_output: false, dry_run: false) ⇒ Object
Serves the current repo: sync + prune.
13 14 15 16 17 |
# File 'lib/carson/runtime/housekeep.rb', line 13 def housekeep!( json_output: false, dry_run: false ) return housekeep_one_dry_run if dry_run housekeep_one( repo_path: repo_root, json_output: json_output ) end |
#housekeep_all!(json_output: false, dry_run: false) ⇒ Object
Knocks each governed repo’s gate in turn.
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
# File 'lib/carson/runtime/housekeep.rb', line 36 def housekeep_all!( json_output: false, dry_run: false ) repos = config.govern_repos if repos.empty? result = { command: "housekeep", status: "error", error: "No governed repositories configured.", recovery: "Run carson onboard in each repo to register." } return housekeep_finish( result: result, exit_code: EXIT_ERROR, json_output: json_output ) end if dry_run repos.each_with_index do |repo_path, idx| puts_line "" if idx > 0 unless Dir.exist?( repo_path ) puts_line "#{File.basename( repo_path )}: SKIP (path not found)" next end scoped = Runtime.new( repo_root: repo_path, tool_root: tool_root, output: output, error: error, verbose: verbose? ) scoped.housekeep_one_dry_run end total = repos.size puts_line "" puts_line "#{total} repo#{plural_suffix( count: total )} surveyed. Run without --dry-run to apply." return EXIT_OK end results = [] repos.each do |repo_path| entry = housekeep_one_entry( repo_path: repo_path, silent: json_output ) if entry[ :status ] == "ok" clear_batch_success( command: "housekeep", repo_path: repo_path ) else record_batch_skip( command: "housekeep", repo_path: repo_path, reason: entry[ :error ] || "housekeep failed" ) end results << entry end succeeded = results.count { |entry| entry[ :status ] == "ok" } failed = results.count { |entry| entry[ :status ] != "ok" } result = { command: "housekeep", status: failed.zero? ? "ok" : "partial", repos: results, succeeded: succeeded, failed: failed } housekeep_finish( result: result, exit_code: failed.zero? ? EXIT_OK : EXIT_ERROR, json_output: json_output, results: results, succeeded: succeeded, failed: failed ) end |
#housekeep_one_dry_run ⇒ Object
Prints a dry-run plan for this repo without making any changes. Calls reap_dead_worktrees_plan and prune_plan on self (already scoped to the repo).
78 79 80 81 82 83 84 |
# File 'lib/carson/runtime/housekeep.rb', line 78 def housekeep_one_dry_run repo_name = File.basename( repo_root ) worktree_plan = reap_dead_worktrees_plan branch_plan = prune_plan( dry_run: true ) print_housekeep_dry_run( repo_name: repo_name, worktree_plan: worktree_plan, branch_plan: branch_plan ) EXIT_OK end |
#housekeep_target!(target:, json_output: false, dry_run: false) ⇒ Object
Resolves a target name to a governed repo, then serves it.
20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
# File 'lib/carson/runtime/housekeep.rb', line 20 def housekeep_target!( target:, json_output: false, dry_run: false ) repo_path = resolve_governed_repo( target: target ) unless repo_path result = { command: "housekeep", status: "error", error: "Not a governed repository: #{target}", recovery: "Run carson repos to see governed repositories." } return housekeep_finish( result: result, exit_code: EXIT_ERROR, json_output: json_output ) end if dry_run scoped = Runtime.new( repo_root: repo_path, tool_root: tool_root, output: output, error: error, verbose: verbose? ) return scoped.housekeep_one_dry_run end housekeep_one( repo_path: repo_path, json_output: json_output ) end |
#reap_dead_worktrees! ⇒ Object
Removes dead worktrees — those whose content is on main, with merged PR evidence, or with closed-unmerged PR evidence and no open PR. Unblocks prune for the branches they hold. Three-layer dead check:
1. Content-absorbed: delegates to sweep_stale_worktrees! (shared, no gh needed).
2. Merged PR evidence: covers rebase/squash where main has since evolved
the same files (requires gh).
3. Abandoned PR evidence: closed-but-unmerged PR on the exact branch tip,
but only when no open PR still exists for the branch.
176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 |
# File 'lib/carson/runtime/housekeep.rb', line 176 def reap_dead_worktrees! # Layer 1: sweep agent-owned worktrees whose content is on main. sweep_stale_worktrees! # Layers 2 and 3: PR evidence for remaining worktrees. return unless gh_available? main_root = main_worktree_root worktree_list.each do |worktree| next if worktree.path == main_root next unless worktree.branch next if worktree.holds_cwd? next if worktree.held_by_other_process? # Missing directory: worktree was destroyed externally. # Prune the stale entry and delete the branch immediately. unless Dir.exist?( worktree.path ) git_run( "worktree", "prune" ) puts_verbose "reaped stale worktree entry: #{File.basename( worktree.path )} (branch: #{worktree.branch})" if !config.protected_branches.include?( worktree.branch ) git_run( "branch", "-D", worktree.branch ) puts_verbose "deleted branch: #{worktree.branch}" end next end tip_sha = git_capture!( "rev-parse", "--verify", worktree.branch ).strip rescue nil next unless tip_sha merged_pr, = merged_pr_for_branch( branch: worktree.branch, branch_tip_sha: tip_sha ) if !merged_pr.nil? # Remove the worktree (no --force: refuses if dirty working tree). _, _, rm_success, = git_run( "worktree", "remove", worktree.path ) next unless rm_success puts_verbose "reaped dead worktree: #{File.basename( worktree.path )} (branch: #{worktree.branch})" # Delete the local branch now that no worktree holds it. if !config.protected_branches.include?( worktree.branch ) git_run( "branch", "-D", worktree.branch ) puts_verbose "deleted branch: #{worktree.branch}" end next end next if branch_has_open_pr?( branch: worktree.branch ) abandoned_pr, = abandoned_pr_for_branch( branch: worktree.branch, branch_tip_sha: tip_sha ) next if abandoned_pr.nil? # Remove the worktree (no --force: refuses if dirty working tree). _, _, rm_success, = git_run( "worktree", "remove", worktree.path ) next unless rm_success puts_verbose "reaped abandoned worktree: #{File.basename( worktree.path )} (branch: #{worktree.branch}, closed PR: #{abandoned_pr.fetch( :url )})" # Delete the local branch now that no worktree holds it. if !config.protected_branches.include?( worktree.branch ) git_run( "branch", "-D", worktree.branch ) puts_verbose "deleted branch: #{worktree.branch}" end end end |
#reap_dead_worktrees_plan ⇒ Object
Returns a plan array describing what reap_dead_worktrees! would do for each non-main worktree, without executing any mutations. Each item: { name:, branch:, action: :reap|:skip, reason: }
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 |
# File 'lib/carson/runtime/housekeep.rb', line 89 def reap_dead_worktrees_plan main_root = main_worktree_root items = [] agent_prefixes = Worktree::AGENT_DIRS.map do |dir| full = File.join( main_root, dir, "worktrees" ) File.join( realpath_safe( full ), "" ) if Dir.exist?( full ) end.compact worktree_list.each do |worktree| next if worktree.path == main_root next unless worktree.branch item = { name: File.basename( worktree.path ), branch: worktree.branch } if worktree.holds_cwd? items << item.merge( action: :skip, reason: "held by current shell" ) next end if worktree.held_by_other_process? items << item.merge( action: :skip, reason: "held by another process" ) next end # Missing directory — would be reaped by worktree prune + branch delete. unless Dir.exist?( worktree.path ) items << item.merge( action: :reap, reason: "directory missing (destroyed externally)" ) next end # Layer 1: agent-owned + content absorbed into main (no gh needed). if agent_prefixes.any? { |prefix| worktree.path.start_with?( prefix ) } && branch_absorbed_into_main?( branch: worktree.branch ) items << item.merge( action: :reap, reason: "content absorbed into main" ) next end # Layers 2 + 3: PR evidence — requires gh CLI. unless gh_available? items << item.merge( action: :skip, reason: "gh CLI not available for PR check" ) next end tip_sha = begin git_capture!( "rev-parse", "--verify", worktree.branch ).strip rescue StandardError nil end unless tip_sha items << item.merge( action: :skip, reason: "cannot read branch tip SHA" ) next end merged_pr, = merged_pr_for_branch( branch: worktree.branch, branch_tip_sha: tip_sha ) if merged_pr items << item.merge( action: :reap, reason: "merged #{pr_short_ref( merged_pr[ :url ] )}" ) next end if branch_has_open_pr?( branch: worktree.branch ) items << item.merge( action: :skip, reason: "open PR exists" ) next end abandoned_pr, = abandoned_pr_for_branch( branch: worktree.branch, branch_tip_sha: tip_sha ) if abandoned_pr items << item.merge( action: :reap, reason: "closed abandoned #{pr_short_ref( abandoned_pr[ :url ] )}" ) next end items << item.merge( action: :skip, reason: "no evidence to reap" ) end items end |