Module: Carson::Runtime::Local
- Included in:
- Carson::Runtime
- Defined in:
- lib/carson/runtime/local/sync.rb,
lib/carson/runtime/local/hooks.rb,
lib/carson/runtime/local/prune.rb,
lib/carson/runtime/local/onboard.rb,
lib/carson/runtime/local/template.rb,
lib/carson/runtime/local/worktree.rb
Constant Summary collapse
- TEMPLATE_SYNC_BRANCH =
"carson/template-sync".freeze
- SUPERSEDED =
[ ".github/carson-instructions.md", ".github/workflows/carson-lint.yml", ".github/.mega-linter.yml" ].freeze
- AGENT_WORKTREE_DIRS =
Agent directory names whose worktrees Carson may sweep.
%w[ .claude .codex ].freeze
Instance Method Summary collapse
-
#offboard! ⇒ Object
Removes Carson-managed repository integration so a host repository can retire Carson cleanly.
-
#onboard! ⇒ Object
One-command onboarding for new repositories: detect remote, install hooks, apply templates, and run initial audit.
- #prune!(json_output: false) ⇒ Object
- #prune_all! ⇒ Object
-
#refresh! ⇒ Object
Re-applies hooks, templates, and audit after upgrading Carson.
-
#refresh_all! ⇒ Object
Re-applies hooks, templates, and audit across all governed repositories.
-
#sweep_stale_worktrees! ⇒ Object
Removes agent-owned worktrees whose branch content is already on main.
- #sync!(json_output: false) ⇒ Object
-
#sync_all! ⇒ Object
Syncs main branch across all governed repositories.
-
#template_apply!(push_prep: false) ⇒ Object
Applies managed template files as full-file writes from Carson sources.
-
#template_check! ⇒ Object
Read-only template drift check; returns block when managed files are out of sync.
-
#template_check_all! ⇒ Object
Read-only template drift check across all governed repositories.
-
#worktree_create!(name:, json_output: false) ⇒ Object
Creates a new worktree under .claude/worktrees/<name> with a fresh branch.
-
#worktree_remove!(worktree_path:, force: false, json_output: false) ⇒ Object
Removes a worktree: directory, git registration, and branch.
Instance Method Details
#offboard! ⇒ Object
Removes Carson-managed repository integration so a host repository can retire Carson cleanly.
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 239 240 241 |
# File 'lib/carson/runtime/local/onboard.rb', line 195 def offboard! puts_verbose "" puts_verbose "[Offboard]" unless inside_git_work_tree? puts_line "ERROR: #{repo_root} is not a git repository." return EXIT_ERROR end if self.in.respond_to?( :tty? ) && self.in.tty? puts_line "" puts_line "This will remove Carson hooks, managed .github/ files," puts_line "and deregister this repository from portfolio governance." puts_line "Continue?" unless prompt_yes_no( default: false ) puts_line "Offboard cancelled." return EXIT_OK end end hooks_status = disable_carson_hooks_path! return hooks_status unless hooks_status == EXIT_OK removed_count = 0 missing_count = 0 offboard_cleanup_targets.each do |relative| absolute = resolve_repo_path!( relative_path: relative, label: "offboard target #{relative}" ) if File.exist?( absolute ) FileUtils.rm_rf( absolute ) puts_verbose "removed_path: #{relative}" removed_count += 1 else puts_verbose "skip_missing_path: #{relative}" missing_count += 1 end end remove_empty_offboard_directories! remove_govern_repo!( repo_path: File.( repo_root ) ) puts_verbose "govern_deregistered: #{File.( repo_root )}" puts_verbose "offboard_summary: removed=#{removed_count} missing=#{missing_count}" if verbose? puts_line "OK: Carson offboard completed for #{repo_root}." else puts_line "Removed #{removed_count} file#{plural_suffix( count: removed_count )}. Offboard complete." end puts_line "" puts_line "Next: commit the removals and push to finalise offboarding." EXIT_OK end |
#onboard! ⇒ Object
One-command onboarding for new repositories: detect remote, install hooks, apply templates, and run initial audit.
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
# File 'lib/carson/runtime/local/onboard.rb', line 10 def onboard! fingerprint_status = block_if_outsider_fingerprints! return fingerprint_status unless fingerprint_status.nil? unless inside_git_work_tree? puts_line "ERROR: #{repo_root} is not a git repository." return EXIT_ERROR end repo_name = File.basename( repo_root ) puts_line "" puts_line "Onboarding #{repo_name}..." if !global_config_exists? || !git_remote_exists?( remote_name: config.git_remote ) if self.in.respond_to?( :tty? ) && self.in.tty? setup_status = setup! return setup_status unless setup_status == EXIT_OK else silent_setup! end end onboard_apply! end |
#prune!(json_output: false) ⇒ Object
7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
# File 'lib/carson/runtime/local/prune.rb', line 7 def prune!( json_output: false ) fingerprint_status = block_if_outsider_fingerprints! unless fingerprint_status.nil? if json_output out.puts JSON.pretty_generate( { command: "prune", status: "block", error: "Carson-owned artefacts detected in host repository", recovery: "remove Carson-owned files (.carson.yml, bin/carson, .tools/carson) then retry", exit_code: EXIT_BLOCK } ) end return fingerprint_status end prune_git!( "fetch", config.git_remote, "--prune", json_output: json_output ) # Clean stale worktree entries whose directories no longer exist. # Unblocks branch deletion for branches held by dead worktrees. git_run( "worktree", "prune" ) active_branch = current_branch cwd_branch = cwd_worktree_branch counters = { deleted: 0, skipped: 0 } branches = [] stale_branches = stale_local_branches prune_stale_branch_entries( stale_branches: stale_branches, active_branch: active_branch, cwd_branch: cwd_branch, counters: counters, branches: branches ) orphan_branches = orphan_local_branches( active_branch: active_branch, cwd_branch: cwd_branch ) prune_orphan_branch_entries( orphan_branches: orphan_branches, counters: counters, branches: branches ) absorbed_branches = absorbed_local_branches( active_branch: active_branch, cwd_branch: cwd_branch ) prune_absorbed_branch_entries( absorbed_branches: absorbed_branches, counters: counters, branches: branches ) prune_finish( result: { command: "prune", status: "ok", branches: branches, deleted: counters.fetch( :deleted ), skipped: counters.fetch( :skipped ) }, exit_code: EXIT_OK, json_output: json_output, counters: counters ) end |
#prune_all! ⇒ Object
144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 |
# File 'lib/carson/runtime/local/onboard.rb', line 144 def prune_all! repos = config.govern_repos if repos.empty? puts_line "No governed repositories configured." puts_line " Run carson onboard in each repo to register." return EXIT_ERROR end puts_line "" puts_line "Prune all (#{repos.length} repo#{plural_suffix( count: repos.length )})" succeeded = 0 failed = 0 repos.each do |repo_path| repo_name = File.basename( repo_path ) unless Dir.exist?( repo_path ) puts_line "#{repo_name}: FAIL (path not found)" record_batch_skip( command: "prune", repo_path: repo_path, reason: "path not found" ) failed += 1 next end begin buf = verbose? ? out : StringIO.new err_buf = verbose? ? err : StringIO.new rt = Runtime.new( repo_root: repo_path, tool_root: tool_root, out: buf, err: err_buf, verbose: verbose? ) status = rt.prune! unless verbose? summary = buf.string.lines.last.to_s.strip puts_line "#{repo_name}: #{summary.empty? ? 'OK' : summary}" end if status == EXIT_ERROR record_batch_skip( command: "prune", repo_path: repo_path, reason: "prune failed" ) failed += 1 else clear_batch_success( command: "prune", repo_path: repo_path ) succeeded += 1 end rescue StandardError => e puts_line "#{repo_name}: FAIL (#{e.})" record_batch_skip( command: "prune", repo_path: repo_path, reason: e. ) failed += 1 end end puts_line "" puts_line "Prune all complete: #{succeeded} pruned, #{failed} failed." failed.zero? ? EXIT_OK : EXIT_ERROR end |
#refresh! ⇒ Object
Re-applies hooks, templates, and audit after upgrading Carson.
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 75 76 77 78 79 80 81 82 83 84 85 |
# File 'lib/carson/runtime/local/onboard.rb', line 36 def refresh! fingerprint_status = block_if_outsider_fingerprints! return fingerprint_status unless fingerprint_status.nil? unless inside_git_work_tree? puts_line "ERROR: #{repo_root} is not a git repository." return EXIT_ERROR end if verbose? puts_verbose "" puts_verbose "[Refresh]" hook_status = prepare! return hook_status unless hook_status == EXIT_OK drift_count = template_results.count { it.fetch( :status ) != "ok" } template_status = template_apply! return template_status unless template_status == EXIT_OK @template_sync_result = template_propagate!( drift_count: drift_count ) audit_status = audit! if audit_status == EXIT_OK puts_line "OK: Carson refresh completed for #{repo_root}." elsif audit_status == EXIT_BLOCK puts_line "BLOCK: Carson refresh completed with policy blocks; resolve and rerun carson audit." end return audit_status end puts_line "Refresh" hook_status = with_captured_output { prepare! } return hook_status unless hook_status == EXIT_OK puts_line "Hooks installed (#{config.managed_hooks.count} hooks)." template_drift_count = template_results.count { it.fetch( :status ) != "ok" } template_status = with_captured_output { template_apply! } return template_status unless template_status == EXIT_OK if template_drift_count.positive? puts_line "Templates applied (#{template_drift_count} updated)." else puts_line "Templates in sync." end @template_sync_result = template_propagate!( drift_count: template_drift_count ) audit_status = audit! puts_line "Refresh complete." audit_status end |
#refresh_all! ⇒ Object
Re-applies hooks, templates, and audit across all governed repositories. Checks each repo for safety (active worktrees, uncommitted changes) and marks unsafe repos as pending to avoid disrupting active work.
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 |
# File 'lib/carson/runtime/local/onboard.rb', line 90 def refresh_all! repos = config.govern_repos if repos.empty? puts_line "No governed repositories configured." puts_line " Run carson onboard in each repo to register." return EXIT_ERROR end pending_before = pending_repos_for( command: "refresh" ) if pending_before.any? puts_line "#{pending_before.length} repo#{plural_suffix( count: pending_before.length )} pending from previous run" end puts_line "" puts_line "Refresh all (#{repos.length} repo#{plural_suffix( count: repos.length )})" refreshed = 0 pending = 0 failed = 0 repos.each do |repo_path| repo_name = File.basename( repo_path ) unless Dir.exist?( repo_path ) puts_line "#{repo_name}: FAIL (path not found)" record_batch_skip( command: "refresh", repo_path: repo_path, reason: "path not found" ) failed += 1 next end safety = portfolio_repo_safety( repo_path: repo_path ) unless safety.fetch( :safe ) reason = safety.fetch( :reasons ).join( ", " ) puts_line "#{repo_name}: PENDING (#{reason})" record_batch_skip( command: "refresh", repo_path: repo_path, reason: reason ) pending += 1 next end status = refresh_single_repo( repo_path: repo_path, repo_name: repo_name ) if status == EXIT_ERROR failed += 1 else clear_batch_success( command: "refresh", repo_path: repo_path ) refreshed += 1 end end puts_line "" parts = [ "#{refreshed} refreshed" ] parts << "#{pending} still pending (will retry on next run)" if pending.positive? parts << "#{failed} failed" if failed.positive? puts_line "Refresh all complete: #{parts.join( ', ' )}." failed.zero? && pending.zero? ? EXIT_OK : EXIT_ERROR end |
#sweep_stale_worktrees! ⇒ Object
Removes agent-owned worktrees whose branch content is already on main. Scans AGENT_WORKTREE_DIRS (e.g. .claude/worktrees/, .codex/worktrees/) under the main repo root. Safe: skips detached HEADs, the caller’s CWD, and dirty working trees (git worktree remove refuses without –force).
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 |
# File 'lib/carson/runtime/local/worktree.rb', line 182 def sweep_stale_worktrees! main_root = main_worktree_root worktrees = worktree_list agent_prefixes = AGENT_WORKTREE_DIRS.filter_map do |dir| full = File.join( main_root, dir, "worktrees" ) File.join( realpath_safe( full ), "" ) if Dir.exist?( full ) end return if agent_prefixes.empty? worktrees.each do |wt| path = wt.fetch( :path ) branch = wt.fetch( :branch, nil ) next unless branch next unless agent_prefixes.any? { |prefix| path.start_with?( prefix ) } next if cwd_inside_worktree?( worktree_path: path ) next unless branch_absorbed_into_main?( branch: branch ) # Remove the worktree (no --force: refuses if dirty working tree). _, _, rm_success, = git_run( "worktree", "remove", path ) next unless rm_success puts_verbose "swept stale worktree: #{File.basename( path )} (branch: #{branch})" # Delete the local branch now that no worktree holds it. if !config.protected_branches.include?( branch ) git_run( "branch", "-D", branch ) puts_verbose "deleted branch: #{branch}" end end end |
#sync!(json_output: false) ⇒ Object
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
# File 'lib/carson/runtime/local/sync.rb', line 6 def sync!( json_output: false ) fingerprint_status = block_if_outsider_fingerprints! return fingerprint_status unless fingerprint_status.nil? unless working_tree_clean? return sync_finish( result: { command: "sync", status: "block", error: "working tree is dirty", recovery: "git add -A && git commit, then carson sync" }, exit_code: EXIT_BLOCK, json_output: json_output ) end start_branch = current_branch switched = false sync_git!( "fetch", config.git_remote, "--prune", json_output: json_output ) if start_branch != config.main_branch sync_git!( "switch", config.main_branch, json_output: json_output ) switched = true end sync_git!( "pull", "--ff-only", config.git_remote, config.main_branch, json_output: json_output ) ahead_count, behind_count, error_text = main_sync_counts if error_text return sync_finish( result: { command: "sync", status: "block", error: "unable to verify main sync state (#{error_text})" }, exit_code: EXIT_BLOCK, json_output: json_output ) end if ahead_count.zero? && behind_count.zero? return sync_finish( result: { command: "sync", status: "ok", ahead: 0, behind: 0, main_branch: config.main_branch, remote: config.git_remote }, exit_code: EXIT_OK, json_output: json_output ) end sync_finish( result: { command: "sync", status: "block", ahead: ahead_count, behind: behind_count, main_branch: config.main_branch, remote: config.git_remote, error: "local #{config.main_branch} still diverges" }, exit_code: EXIT_BLOCK, json_output: json_output ) ensure git_system!( "switch", start_branch ) if switched && branch_exists?( branch_name: start_branch ) end |
#sync_all! ⇒ Object
Syncs main branch across all governed repositories.
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 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
# File 'lib/carson/runtime/local/sync.rb', line 46 def sync_all! repos = config.govern_repos if repos.empty? puts_line "No governed repositories configured." puts_line " Run carson onboard in each repo to register." return EXIT_ERROR end puts_line "" puts_line "Sync all (#{repos.length} repo#{plural_suffix( count: repos.length )})" synced = 0 failed = 0 repos.each do |repo_path| repo_name = File.basename( repo_path ) unless Dir.exist?( repo_path ) puts_line "#{repo_name}: FAIL (path not found)" record_batch_skip( command: "sync", repo_path: repo_path, reason: "path not found" ) failed += 1 next end begin rt = build_scoped_runtime( repo_path: repo_path ) status = rt.sync! if status == EXIT_OK puts_line "#{repo_name}: ok" unless verbose? clear_batch_success( command: "sync", repo_path: repo_path ) synced += 1 else puts_line "#{repo_name}: FAIL" unless verbose? record_batch_skip( command: "sync", repo_path: repo_path, reason: "sync failed" ) failed += 1 end rescue StandardError => e puts_line "#{repo_name}: FAIL (#{e.})" record_batch_skip( command: "sync", repo_path: repo_path, reason: e. ) failed += 1 end end puts_line "" puts_line "Sync all complete: #{synced} synced, #{failed} failed." failed.zero? ? EXIT_OK : EXIT_ERROR end |
#template_apply!(push_prep: false) ⇒ Object
Applies managed template files as full-file writes from Carson sources. Also removes superseded files that are no longer part of the managed set.
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 |
# File 'lib/carson/runtime/local/template.rb', line 95 def template_apply!( push_prep: false ) fingerprint_status = block_if_outsider_fingerprints! return fingerprint_status unless fingerprint_status.nil? puts_verbose "" puts_verbose "[Template Sync Apply]" results = template_results stale = template_superseded_present applied = 0 results.each do |entry| if entry.fetch( :status ) == "error" puts_verbose "template_file: #{entry.fetch( :file )} status=error reason=#{entry.fetch( :reason )}" next end file_path = File.join( repo_root, entry.fetch( :file ) ) if entry.fetch( :status ) == "ok" puts_verbose "template_file: #{entry.fetch( :file )} status=ok reason=in_sync" next end FileUtils.mkdir_p( File.dirname( file_path ) ) File.write( file_path, entry.fetch( :applied_content ) ) puts_verbose "template_file: #{entry.fetch( :file )} status=updated reason=#{entry.fetch( :reason )}" applied += 1 end removed = 0 stale.each do |file| file_path = resolve_repo_path!( relative_path: file, label: "superseded file #{file}" ) File.delete( file_path ) puts_verbose "template_file: #{file} status=removed reason=superseded" removed += 1 end error_count = results.count { it.fetch( :status ) == "error" } puts_verbose "template_apply_summary: updated=#{applied} removed=#{removed} error=#{error_count}" unless verbose? if applied.positive? || removed.positive? summary_parts = [] summary_parts << "#{applied} updated" if applied.positive? summary_parts << "#{removed} removed" if removed.positive? puts_line "Templates applied (#{summary_parts.join( ", " )})." else puts_line "Templates in sync." end end return EXIT_ERROR if error_count.positive? return EXIT_BLOCK if push_prep && push_prep_commit! EXIT_OK end |
#template_check! ⇒ Object
Read-only template drift check; returns block when managed files are out of sync.
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
# File 'lib/carson/runtime/local/template.rb', line 13 def template_check! fingerprint_status = block_if_outsider_fingerprints! return fingerprint_status unless fingerprint_status.nil? puts_verbose "" puts_verbose "[Template Sync Check]" results = template_results stale = template_superseded_present drift_count = results.count { it.fetch( :status ) == "drift" } error_count = results.count { it.fetch( :status ) == "error" } stale_count = stale.count results.each do |entry| puts_verbose "template_file: #{entry.fetch( :file )} status=#{entry.fetch( :status )} reason=#{entry.fetch( :reason )}" end stale.each { |file| puts_verbose "template_file: #{file} status=stale reason=superseded" } puts_verbose "template_summary: total=#{results.count} drift=#{drift_count} stale=#{stale_count} error=#{error_count}" unless verbose? if ( drift_count + stale_count ).positive? summary_parts = [] summary_parts << "#{drift_count} of #{results.count} drifted" if drift_count.positive? summary_parts << "#{stale_count} stale" if stale_count.positive? puts_line "Templates: #{summary_parts.join( ", " )}" results.select { it.fetch( :status ) == "drift" }.each { |entry| puts_line " #{entry.fetch( :file )}" } stale.each { |file| puts_line " #{file} — superseded" } else puts_line "Templates: #{results.count} files in sync" end end return EXIT_ERROR if error_count.positive? ( drift_count + stale_count ).positive? ? EXIT_BLOCK : EXIT_OK end |
#template_check_all! ⇒ Object
Read-only template drift check across all governed repositories.
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 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
# File 'lib/carson/runtime/local/template.rb', line 47 def template_check_all! repos = config.govern_repos if repos.empty? puts_line "No governed repositories configured." puts_line " Run carson onboard in each repo to register." return EXIT_ERROR end puts_line "" puts_line "Template check all (#{repos.length} repo#{plural_suffix( count: repos.length )})" in_sync = 0 drifted = 0 failed = 0 repos.each do |repo_path| repo_name = File.basename( repo_path ) unless Dir.exist?( repo_path ) puts_line "#{repo_name}: FAIL (path not found)" record_batch_skip( command: "template_check", repo_path: repo_path, reason: "path not found" ) failed += 1 next end begin rt = build_scoped_runtime( repo_path: repo_path ) status = rt.template_check! if status == EXIT_OK puts_line "#{repo_name}: in sync" unless verbose? clear_batch_success( command: "template_check", repo_path: repo_path ) in_sync += 1 else puts_line "#{repo_name}: DRIFT" unless verbose? drifted += 1 end rescue StandardError => e puts_line "#{repo_name}: FAIL (#{e.})" record_batch_skip( command: "template_check", repo_path: repo_path, reason: e. ) failed += 1 end end puts_line "" puts_line "Template check complete: #{in_sync} in sync, #{drifted} drifted, #{failed} failed." drifted.zero? && failed.zero? ? EXIT_OK : EXIT_BLOCK end |
#worktree_create!(name:, json_output: false) ⇒ Object
Creates a new worktree under .claude/worktrees/<name> with a fresh branch. Uses main_worktree_root so this works even when called from inside a worktree.
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
# File 'lib/carson/runtime/local/worktree.rb', line 15 def worktree_create!( name:, json_output: false ) worktrees_dir = File.join( main_worktree_root, ".claude", "worktrees" ) wt_path = File.join( worktrees_dir, name ) if Dir.exist?( wt_path ) return worktree_finish( result: { command: "worktree create", status: "error", name: name, path: wt_path, error: "worktree already exists: #{name}", recovery: "carson worktree remove #{name}, then retry" }, exit_code: EXIT_ERROR, json_output: json_output ) end # Determine the base branch (main branch from config). base = config.main_branch # Sync main from remote before branching so the worktree starts # from the latest code. Prevents stale-base merge conflicts later. # Best-effort — if pull fails (non-ff, offline), continue anyway. main_root = main_worktree_root _, _, pull_ok, = Open3.capture3( "git", "-C", main_root, "pull", "--ff-only", config.git_remote, base ) puts_verbose pull_ok.success? ? "synced #{base} before branching" : "sync skipped — continuing from local #{base}" # Ensure .claude/ is excluded from git status in the host repository. # Uses .git/info/exclude (local-only, never committed) to respect the outsider boundary. ensure_claude_dir_excluded! # Create the worktree with a new branch based on the main branch. FileUtils.mkdir_p( worktrees_dir ) _, wt_stderr, wt_success, = git_run( "worktree", "add", wt_path, "-b", name, base ) unless wt_success error_text = wt_stderr.to_s.strip error_text = "unable to create worktree" if error_text.empty? return worktree_finish( result: { command: "worktree create", status: "error", name: name, error: error_text }, exit_code: EXIT_ERROR, json_output: json_output ) end worktree_finish( result: { command: "worktree create", status: "ok", name: name, path: wt_path, branch: name }, exit_code: EXIT_OK, json_output: json_output ) end |
#worktree_remove!(worktree_path:, force: false, json_output: false) ⇒ Object
Removes a worktree: directory, git registration, and branch. Never forces removal — if the worktree has uncommitted changes, refuses unless the user explicitly passes force: true via CLI –force flag.
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 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 166 167 168 169 170 171 172 173 174 175 176 |
# File 'lib/carson/runtime/local/worktree.rb', line 64 def worktree_remove!( worktree_path:, force: false, json_output: false ) fingerprint_status = block_if_outsider_fingerprints! unless fingerprint_status.nil? if json_output out.puts JSON.pretty_generate( { command: "worktree remove", status: "block", error: "Carson-owned artefacts detected in host repository", recovery: "remove Carson-owned files (.carson.yml, bin/carson, .tools/carson) then retry", exit_code: EXIT_BLOCK } ) end return fingerprint_status end resolved_path = resolve_worktree_path( worktree_path: worktree_path ) # Missing directory: worktree was destroyed externally (e.g. gh pr merge # --delete-branch). Clean up the stale git registration and delete the branch. if !Dir.exist?( resolved_path ) && worktree_registered?( path: resolved_path ) return worktree_remove_missing!( resolved_path: resolved_path, json_output: json_output ) end unless worktree_registered?( path: resolved_path ) return worktree_finish( result: { command: "worktree remove", status: "error", name: File.basename( resolved_path ), error: "#{resolved_path} is not a registered worktree", recovery: "git worktree list" }, exit_code: EXIT_ERROR, json_output: json_output ) end # Safety: refuse if the caller's shell CWD is inside the worktree. # Removing a directory while a shell is inside it kills the shell permanently. if cwd_inside_worktree?( worktree_path: resolved_path ) safe_root = main_worktree_root return worktree_finish( result: { command: "worktree remove", status: "block", name: File.basename( resolved_path ), error: "current working directory is inside this worktree", recovery: "cd #{safe_root} && carson worktree remove #{File.basename( resolved_path )}" }, exit_code: EXIT_BLOCK, json_output: json_output ) end branch = worktree_branch( path: resolved_path ) puts_verbose "worktree_remove: path=#{resolved_path} branch=#{branch} force=#{force}" # Safety: refuse if the branch has unpushed commits (unless --force). # Prevents accidental destruction of work that exists only locally. unless force unpushed = check_unpushed_commits( branch: branch, worktree_path: resolved_path ) if unpushed return worktree_finish( result: { command: "worktree remove", status: "block", name: File.basename( resolved_path ), branch: branch, error: unpushed[ :error ], recovery: unpushed[ :recovery ] }, exit_code: EXIT_BLOCK, json_output: json_output ) end end # Step 1: remove the worktree (directory + git registration). rm_args = [ "worktree", "remove" ] rm_args << "--force" if force rm_args << resolved_path rm_stdout, rm_stderr, rm_success, = git_run( *rm_args ) unless rm_success error_text = rm_stderr.to_s.strip error_text = "unable to remove worktree" if error_text.empty? if !force && ( error_text.downcase.include?( "untracked" ) || error_text.downcase.include?( "modified" ) ) return worktree_finish( result: { command: "worktree remove", status: "error", name: File.basename( resolved_path ), error: "worktree has uncommitted changes", recovery: "commit or discard changes first, or use --force to override" }, exit_code: EXIT_ERROR, json_output: json_output ) end return worktree_finish( result: { command: "worktree remove", status: "error", name: File.basename( resolved_path ), error: error_text }, exit_code: EXIT_ERROR, json_output: json_output ) end puts_verbose "worktree_removed: #{resolved_path}" # Step 2: delete the local branch. branch_deleted = false if branch && !config.protected_branches.include?( branch ) _, del_stderr, del_success, = git_run( "branch", "-D", branch ) if del_success puts_verbose "branch_deleted: #{branch}" branch_deleted = true else puts_verbose "branch_delete_skipped: #{branch} reason=#{del_stderr.to_s.strip}" end end # Step 3: delete the remote branch (best-effort). remote_deleted = false if branch && !config.protected_branches.include?( branch ) remote_branch = branch _, _, rd_success, = git_run( "push", config.git_remote, "--delete", remote_branch ) if rd_success puts_verbose "remote_branch_deleted: #{config.git_remote}/#{remote_branch}" remote_deleted = true end end worktree_finish( result: { command: "worktree remove", status: "ok", name: File.basename( resolved_path ), branch: branch, branch_deleted: branch_deleted, remote_deleted: remote_deleted }, exit_code: EXIT_OK, json_output: json_output ) end |