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.



250
251
252
253
254
255
256
257
258
# File 'lib/carson/runtime/local.rb', line 250

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

	print_header "Inspect"
	ok = hooks_health_report( strict: true )
	puts_line( ok ? "status: ok" : "status: block" )
	ok ? EXIT_OK : EXIT_BLOCK
end

#normalise_branch_delete_error(error_text:) ⇒ Object



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

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.



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

def offboard!
	print_header "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_line "removed_path: #{relative}"
			removed_count += 1
		else
			puts_line "skip_missing_path: #{relative}"
			missing_count += 1
		end
	end
	remove_empty_offboard_directories!
	puts_line "offboard_summary: removed=#{removed_count} missing=#{missing_count}"
	puts_line "OK: Carson offboard completed for #{repo_root}."
	EXIT_OK
end

#onboard!Object

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



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

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}..."

	unless global_config_exists?
		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!
ensure
	@concise = false
end

#prepare!Object

Installs required hook files and enforces repository hook path.



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

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_line "hook_written: #{relative_path( target_path )}" unless concise?
	end
	git_system!( "config", "core.hooksPath", hooks_dir )
	File.write( File.join( hooks_dir, "workflow_style" ), config.workflow_style )
	puts_line "configured_hooks_path: #{hooks_dir}" unless concise?
	return EXIT_OK if concise?

	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
# 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_line "prune_summary: deleted=#{counters.fetch( :deleted )} skipped=#{counters.fetch( :skipped )}"
	EXIT_OK
end

#prune_delete_stale_branch(branch:, upstream:) ⇒ Object



79
80
81
82
83
84
85
86
87
88
89
# File 'lib/carson/runtime/local.rb', line 79

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



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

def prune_force_delete_failed( branch:, upstream:, force_stderr: )
	force_error_text = normalise_branch_delete_error( error_text: force_stderr )
	puts_line "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



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

def prune_force_delete_skipped( branch:, upstream:, delete_error_text:, force_error: )
	puts_line "skip_delete_branch: #{branch} (upstream=#{upstream}) reason=#{delete_error_text}"
	puts_line "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



97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/carson/runtime/local.rb', line 97

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



110
111
112
113
114
# File 'lib/carson/runtime/local.rb', line 110

def prune_force_delete_success( branch:, upstream:, merged_pr:, force_stdout: )
	out.print force_stdout unless force_stdout.empty?
	puts_line "deleted_local_branch_force: #{branch} (upstream=#{upstream}) merged_pr=#{merged_pr.fetch( :url )}"
	:deleted
end

#prune_no_stale_branchesObject



50
51
52
53
# File 'lib/carson/runtime/local.rb', line 50

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

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



91
92
93
94
95
# File 'lib/carson/runtime/local.rb', line 91

def prune_safe_delete_success( branch:, upstream:, stdout_text: )
	out.print stdout_text unless stdout_text.empty?
	puts_line "deleted_local_branch: #{branch} (upstream=#{upstream})"
	:deleted
end

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



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

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

#prune_stale_branch_entries(stale_branches:, active_branch:) ⇒ Object



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

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



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

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.



196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/carson/runtime/local.rb', line 196

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

	print_header "Refresh"
	unless inside_git_work_tree?
		puts_line "ERROR: #{repo_root} is not a git repository."
		return EXIT_ERROR
	end
	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
	audit_status
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.



279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
# File 'lib/carson/runtime/local.rb', line 279

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

	print_header "Template Sync Apply" unless concise?
	results = template_results
	applied = 0
	results.each do |entry|
		if entry.fetch( :status ) == "error"
			puts_line "template_file: #{entry.fetch( :file )} status=error reason=#{entry.fetch( :reason )}" unless concise?
			next
		end

		file_path = File.join( repo_root, entry.fetch( :file ) )
		if entry.fetch( :status ) == "ok"
			puts_line "template_file: #{entry.fetch( :file )} status=ok reason=in_sync" unless concise?
			next
		end

		FileUtils.mkdir_p( File.dirname( file_path ) )
		File.write( file_path, entry.fetch( :applied_content ) )
		puts_line "template_file: #{entry.fetch( :file )} status=updated reason=#{entry.fetch( :reason )}" unless concise?
		applied += 1
	end

	error_count = results.count { |entry| entry.fetch( :status ) == "error" }
	puts_line "template_apply_summary: updated=#{applied} error=#{error_count}" unless concise?
	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.



261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
# File 'lib/carson/runtime/local.rb', line 261

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

	print_header "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_line "template_file: #{entry.fetch( :file )} status=#{entry.fetch( :status )} reason=#{entry.fetch( :reason )}"
	end
	puts_line "template_summary: total=#{results.count} drift=#{drift_count} error=#{error_count}"
	return EXIT_ERROR if error_count.positive?

	drift_count.positive? ? EXIT_BLOCK : EXIT_OK
end