Module: Carson::Runtime::Local

Included in:
Carson::Runtime
Defined in:
lib/carson/runtime/local.rb

Instance Method Summary collapse

Instance Method Details

#inspect!Object

Strict hook health check used by humans, hooks, and CI paths.



320
321
322
323
324
325
326
327
328
329
330
331
332
# File 'lib/carson/runtime/local.rb', line 320

def inspect!
	fingerprint_status = block_if_outsider_fingerprints!
	return fingerprint_status unless fingerprint_status.nil?

	puts_verbose ""
	puts_verbose "[Inspect]"
	ok = hooks_health_report( strict: true )
	puts_verbose( ok ? "status: ok" : "status: block" )
	unless verbose?
		puts_line( ok ? "Hooks: ok" : "Hooks: block" )
	end
	ok ? EXIT_OK : EXIT_BLOCK
end

#normalise_branch_delete_error(error_text:) ⇒ Object



135
136
137
138
# File 'lib/carson/runtime/local.rb', line 135

def normalise_branch_delete_error( error_text: )
	text = error_text.to_s.strip
	text.empty? ? "unknown error" : text
end

#offboard!Object

Removes Carson-managed repository integration so a host repository can retire Carson cleanly.



286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
# File 'lib/carson/runtime/local.rb', line 286

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
	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!
	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
	EXIT_OK
end

#onboard!Object

One-command onboarding for new repositories: detect remote, install hooks, apply templates, and run initial audit.



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/carson/runtime/local.rb', line 178

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

#prepare!Object

Installs required hook files and enforces repository hook path.



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
# File 'lib/carson/runtime/local.rb', line 141

def prepare!
	fingerprint_status = block_if_outsider_fingerprints!
	return fingerprint_status unless fingerprint_status.nil?

	FileUtils.mkdir_p( hooks_dir )
	missing_templates = config.required_hooks.reject { |name| File.file?( hook_template_path( hook_name: name ) ) }
	unless missing_templates.empty?
		puts_line "BLOCK: missing hook templates in Carson: #{missing_templates.join( ', ' )}."
		return EXIT_BLOCK
	end

	symlinked = symlink_hook_files
	unless symlinked.empty?
		puts_line "BLOCK: symlink hook files are not allowed: #{symlinked.join( ', ' )}."
		return EXIT_BLOCK
	end

	config.required_hooks.each do |hook_name|
		source_path = hook_template_path( hook_name: hook_name )
		target_path = File.join( hooks_dir, hook_name )
		FileUtils.cp( source_path, target_path )
		FileUtils.chmod( 0o755, target_path )
		puts_verbose "hook_written: #{relative_path( target_path )}"
	end
	git_system!( "config", "core.hooksPath", hooks_dir )
	File.write( File.join( hooks_dir, "workflow_style" ), config.workflow_style )
	puts_verbose "configured_hooks_path: #{hooks_dir}"
	unless verbose?
		puts_line "Hooks installed (#{config.required_hooks.count} hooks)."
		return EXIT_OK
	end

	inspect!
end

#prune!Object

Removes stale local branches that track remote refs already deleted upstream.



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/carson/runtime/local.rb', line 36

def prune!
	fingerprint_status = block_if_outsider_fingerprints!
	return fingerprint_status unless fingerprint_status.nil?

	git_system!( "fetch", config.git_remote, "--prune" )
	active_branch = current_branch
	stale_branches = stale_local_branches
	return prune_no_stale_branches if stale_branches.empty?

	counters = prune_stale_branch_entries( stale_branches: stale_branches, active_branch: active_branch )
	puts_verbose "prune_summary: deleted=#{counters.fetch( :deleted )} skipped=#{counters.fetch( :skipped )}"
	unless verbose?
		puts_line "Pruned #{counters.fetch( :deleted )} stale branch#{plural_suffix( count: counters.fetch( :deleted ) )}."
	end
	EXIT_OK
end

#prune_delete_stale_branch(branch:, upstream:) ⇒ Object



86
87
88
89
90
91
92
93
94
95
96
# File 'lib/carson/runtime/local.rb', line 86

def prune_delete_stale_branch( branch:, upstream: )
	stdout_text, stderr_text, success, = git_run( "branch", "-d", branch )
	return prune_safe_delete_success( branch: branch, upstream: upstream, stdout_text: stdout_text ) if success

	delete_error_text = normalise_branch_delete_error( error_text: stderr_text )
	prune_force_delete_stale_branch(
		branch: branch,
		upstream: upstream,
		delete_error_text: delete_error_text
	)
end

#prune_force_delete_failed(branch:, upstream:, force_stderr:) ⇒ Object



123
124
125
126
127
# File 'lib/carson/runtime/local.rb', line 123

def prune_force_delete_failed( branch:, upstream:, force_stderr: )
	force_error_text = normalise_branch_delete_error( error_text: force_stderr )
	puts_verbose "fail_force_delete_branch: #{branch} (upstream=#{upstream}) reason=#{force_error_text}"
	:skipped
end

#prune_force_delete_skipped(branch:, upstream:, delete_error_text:, force_error:) ⇒ Object



129
130
131
132
133
# File 'lib/carson/runtime/local.rb', line 129

def prune_force_delete_skipped( branch:, upstream:, delete_error_text:, force_error: )
	puts_verbose "skip_delete_branch: #{branch} (upstream=#{upstream}) reason=#{delete_error_text}"
	puts_verbose "skip_force_delete_branch: #{branch} (upstream=#{upstream}) reason=#{force_error}" unless force_error.to_s.strip.empty?
	:skipped
end

#prune_force_delete_stale_branch(branch:, upstream:, delete_error_text:) ⇒ Object



104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/carson/runtime/local.rb', line 104

def prune_force_delete_stale_branch( branch:, upstream:, delete_error_text: )
	merged_pr, force_error = force_delete_evidence_for_stale_branch(
		branch: branch,
		delete_error_text: delete_error_text
	)
	return prune_force_delete_skipped( branch: branch, upstream: upstream, delete_error_text: delete_error_text, force_error: force_error ) if merged_pr.nil?

	force_stdout, force_stderr, force_success, = git_run( "branch", "-D", branch )
	return prune_force_delete_success( branch: branch, upstream: upstream, merged_pr: merged_pr, force_stdout: force_stdout ) if force_success

	prune_force_delete_failed( branch: branch, upstream: upstream, force_stderr: force_stderr )
end

#prune_force_delete_success(branch:, upstream:, merged_pr:, force_stdout:) ⇒ Object



117
118
119
120
121
# File 'lib/carson/runtime/local.rb', line 117

def prune_force_delete_success( branch:, upstream:, merged_pr:, force_stdout: )
	out.print force_stdout if verbose? && !force_stdout.empty?
	puts_verbose "deleted_local_branch_force: #{branch} (upstream=#{upstream}) merged_pr=#{merged_pr.fetch( :url )}"
	:deleted
end

#prune_no_stale_branchesObject



53
54
55
56
57
58
59
60
# File 'lib/carson/runtime/local.rb', line 53

def prune_no_stale_branches
	if verbose?
		puts_line "OK: no stale local branches tracking deleted #{config.git_remote} branches."
	else
		puts_line "No stale branches."
	end
	EXIT_OK
end

#prune_safe_delete_success(branch:, upstream:, stdout_text:) ⇒ Object



98
99
100
101
102
# File 'lib/carson/runtime/local.rb', line 98

def prune_safe_delete_success( branch:, upstream:, stdout_text: )
	out.print stdout_text if verbose? && !stdout_text.empty?
	puts_verbose "deleted_local_branch: #{branch} (upstream=#{upstream})"
	:deleted
end

#prune_skip_stale_branch(type:, branch:, upstream:) ⇒ Object



80
81
82
83
84
# File 'lib/carson/runtime/local.rb', line 80

def prune_skip_stale_branch( type:, branch:, upstream: )
	status = type == :protected ? "skip_protected_branch" : "skip_current_branch"
	puts_verbose "#{status}: #{branch} (upstream=#{upstream})"
	:skipped
end

#prune_stale_branch_entries(stale_branches:, active_branch:) ⇒ Object



62
63
64
65
66
67
68
69
# File 'lib/carson/runtime/local.rb', line 62

def prune_stale_branch_entries( stale_branches:, active_branch: )
	counters = { deleted: 0, skipped: 0 }
	stale_branches.each do |entry|
		outcome = prune_stale_branch_entry( entry: entry, active_branch: active_branch )
		counters[ outcome ] += 1
	end
	counters
end

#prune_stale_branch_entry(entry:, active_branch:) ⇒ Object



71
72
73
74
75
76
77
78
# File 'lib/carson/runtime/local.rb', line 71

def prune_stale_branch_entry( entry:, active_branch: )
	branch = entry.fetch( :branch )
	upstream = entry.fetch( :upstream )
	return prune_skip_stale_branch( type: :protected, branch: branch, upstream: upstream ) if config.protected_branches.include?( branch )
	return prune_skip_stale_branch( type: :current, branch: branch, upstream: upstream ) if branch == active_branch

	prune_delete_stale_branch( branch: branch, upstream: upstream )
end

#refresh!Object

Re-applies hooks, templates, and audit after upgrading Carson.



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
242
243
244
245
246
247
248
# File 'lib/carson/runtime/local.rb', line 204

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

		template_status = template_apply!
		return template_status unless template_status == EXIT_OK

		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.required_hooks.count} hooks)."

	template_drift_count = template_results.count { |entry| entry.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

	audit_status = audit!
	puts_line "Refresh complete."
	audit_status
end

#refresh_all!Object

Re-applies hooks, templates, and audit across all governed repositories.



251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
# File 'lib/carson/runtime/local.rb', line 251

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

	puts_line ""
	puts_line "Refresh all (#{repos.length} repo#{plural_suffix( count: repos.length )})"
	refreshed = 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)"
			failed += 1
			next
		end

		status = refresh_single_repo( repo_path: repo_path, repo_name: repo_name )
		if status == EXIT_ERROR
			failed += 1
		else
			refreshed += 1
		end
	end

	puts_line ""
	puts_line "Refresh all complete: #{refreshed} refreshed, #{failed} failed."
	failed.zero? ? EXIT_OK : EXIT_ERROR
end

#sync!Object



4
5
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
# File 'lib/carson/runtime/local.rb', line 4

def sync!
	fingerprint_status = block_if_outsider_fingerprints!
	return fingerprint_status unless fingerprint_status.nil?

	unless working_tree_clean?
		puts_line "BLOCK: working tree is dirty; commit/stash first, then run carson sync."
		return EXIT_BLOCK
	end
	start_branch = current_branch
	switched = false
	git_system!( "fetch", config.git_remote, "--prune" )
	if start_branch != config.main_branch
		git_system!( "switch", config.main_branch )
		switched = true
	end
	git_system!( "pull", "--ff-only", config.git_remote, config.main_branch )
	ahead_count, behind_count, error_text = main_sync_counts
	if error_text
		puts_line "BLOCK: unable to verify main sync state (#{error_text})."
		return EXIT_BLOCK
	end
	if ahead_count.zero? && behind_count.zero?
		puts_line "OK: local #{config.main_branch} is now in sync with #{config.git_remote}/#{config.main_branch}."
		return EXIT_OK
	end
	puts_line "BLOCK: local #{config.main_branch} still diverges (ahead=#{ahead_count}, behind=#{behind_count})."
	EXIT_BLOCK
ensure
	git_system!( "switch", start_branch ) if switched && branch_exists?( branch_name: start_branch )
end

#template_apply!Object

Applies managed template files as full-file writes from Carson sources.



363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
# File 'lib/carson/runtime/local.rb', line 363

def template_apply!
	fingerprint_status = block_if_outsider_fingerprints!
	return fingerprint_status unless fingerprint_status.nil?

	puts_verbose ""
	puts_verbose "[Template Sync Apply]"
	results = template_results
	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

	error_count = results.count { |entry| entry.fetch( :status ) == "error" }
	puts_verbose "template_apply_summary: updated=#{applied} error=#{error_count}"
	unless verbose?
		if applied.positive?
			puts_line "Templates applied (#{applied} updated)."
		else
			puts_line "Templates in sync."
		end
	end
	error_count.positive? ? EXIT_ERROR : EXIT_OK
end

#template_check!Object

Read-only template drift check; returns block when managed files are out of sync.



335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
# File 'lib/carson/runtime/local.rb', line 335

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
	drift_count = results.count { |entry| entry.fetch( :status ) == "drift" }
	error_count = results.count { |entry| entry.fetch( :status ) == "error" }
	results.each do |entry|
		puts_verbose "template_file: #{entry.fetch( :file )} status=#{entry.fetch( :status )} reason=#{entry.fetch( :reason )}"
	end
	puts_verbose "template_summary: total=#{results.count} drift=#{drift_count} error=#{error_count}"
	unless verbose?
		if drift_count.positive?
			drift_files = results.select { |entry| entry.fetch( :status ) == "drift" }.map { |entry| entry.fetch( :file ) }
			puts_line "Templates: #{drift_count} of #{results.count} drifted"
			drift_files.each { |file| puts_line "  #{file}" }
		else
			puts_line "Templates: #{results.count} files in sync"
		end
	end
	return EXIT_ERROR if error_count.positive?

	drift_count.positive? ? EXIT_BLOCK : EXIT_OK
end