carson 2.8.0 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/RELEASE.md +73 -0
- data/VERSION +1 -1
- data/lib/carson/cli.rb +6 -3
- data/lib/carson/runtime/audit.rb +121 -95
- data/lib/carson/runtime/govern.rb +9 -4
- data/lib/carson/runtime/lint.rb +9 -8
- data/lib/carson/runtime/local.rb +172 -70
- data/lib/carson/runtime/review/gate_support.rb +6 -6
- data/lib/carson/runtime/review/sweep_support.rb +3 -3
- data/lib/carson/runtime/review.rb +17 -9
- data/lib/carson/runtime/setup.rb +9 -8
- data/lib/carson/runtime.rb +24 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 45443353a34297748fec8177c152d898dabb8cf7f1641d7870b7aff59d9a2f6c
|
|
4
|
+
data.tar.gz: 627bdc81dcf78ad41998840796a3427512c39b6b2e80e2b961895bdf4423a456
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 96a9e858b5d934d5e5570443e25b79e28255cfe54279623be8d44474e55d592a7c9cd3161da8611d84fd24141cd10a98b6922fb364609630d843f651858bf78c
|
|
7
|
+
data.tar.gz: ee85efae91e54e07d872c560ad7305553b3d45246f9ec502b6fef5b7d1e6e843c8a03787629adb55190541cf5101be76a3f3ee625ec31540876b718905013ba4
|
data/RELEASE.md
CHANGED
|
@@ -5,6 +5,79 @@ Release-note scope rule:
|
|
|
5
5
|
- `RELEASE.md` records only version deltas, breaking changes, and migration actions.
|
|
6
6
|
- Operational usage guides live in `MANUAL.md` and `API.md`.
|
|
7
7
|
|
|
8
|
+
## 2.9.0 — Concise UX for All Commands
|
|
9
|
+
|
|
10
|
+
### What changed
|
|
11
|
+
|
|
12
|
+
- **Concise output by default.** Every Carson command now prints clean, minimal output — what happened, what needs attention, what to do next. Diagnostic key-value lines are suppressed unless `--verbose` is passed.
|
|
13
|
+
- **`--verbose` flag.** Global flag available on all commands. Restores full diagnostic output (same as pre-2.9.0 behaviour). The pre-commit hook runs `carson audit` (no flags) so it automatically gets clean output.
|
|
14
|
+
- **Audit concise output.** A healthy audit prints one line (`Audit: ok`). Problems print only actionable summaries (e.g. `Hooks: mismatch — run carson prepare.`).
|
|
15
|
+
- **Refresh concise output.** Prints ~5 lines: hooks installed, templates in sync, audit result, done.
|
|
16
|
+
- **All other commands.** `prepare`, `inspect`, `offboard`, `template check/apply`, `prune`, `review gate/sweep`, `govern`, `lint setup`, `setup`, and `housekeep` all follow the same concise/verbose pattern.
|
|
17
|
+
|
|
18
|
+
### What users must do now
|
|
19
|
+
|
|
20
|
+
1. Upgrade Carson to `2.9.0`.
|
|
21
|
+
2. Use `--verbose` when you need full diagnostics (debugging, CI troubleshooting).
|
|
22
|
+
|
|
23
|
+
### Breaking or removed behaviour
|
|
24
|
+
|
|
25
|
+
- Default output is now concise. Scripts that parse Carson's key-value diagnostic lines must add `--verbose`.
|
|
26
|
+
- Removed `@concise` internal flag (replaced by `--verbose` opt-in pattern).
|
|
27
|
+
|
|
28
|
+
### Upgrade steps
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
cd ~/Dev/carson
|
|
32
|
+
git pull
|
|
33
|
+
bash install.sh
|
|
34
|
+
carson version
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 2.8.1 — Onboard UX and Install Cleanup
|
|
38
|
+
|
|
39
|
+
### What changed
|
|
40
|
+
|
|
41
|
+
- **Concise onboard output.** `carson onboard` now prints a clean 8-line summary instead of verbose internal state (hook paths, template statuses, config lines). Tells users what happened, what needs attention, and what to do next.
|
|
42
|
+
- **Graceful handling of fresh repos.** Onboard no longer fails with a fatal error on repositories with no commits yet.
|
|
43
|
+
- **Suppressed RubyGems PATH warning.** The misleading `WARNING: You don't have ... in your PATH, gem executables will not run` message from `gem install --user-install` is now suppressed during installation. Carson symlinks the executable to `~/.carson/bin`, making the gem bin directory irrelevant.
|
|
44
|
+
- **Default workflow style changed from `trunk` to `branch`.** All governed repositories now enforce PR-only merges by default. Direct commits, merge commits, and pushes to protected branches (`main`/`master`) are blocked by hooks unless explicitly opted out.
|
|
45
|
+
|
|
46
|
+
### What users must do now
|
|
47
|
+
|
|
48
|
+
1. Upgrade Carson to `2.8.1`.
|
|
49
|
+
2. If you rely on direct commits to main, re-run `carson setup` and choose `trunk`, or set `CARSON_WORKFLOW_STYLE=trunk` in your environment.
|
|
50
|
+
|
|
51
|
+
### Breaking or removed behaviour
|
|
52
|
+
|
|
53
|
+
- Default `workflow.style` changed from `trunk` to `branch`. Repositories that previously relied on the implicit `trunk` default will now block direct commits to protected branches. Escape hatches: run `carson setup` to choose `trunk`, or set `CARSON_WORKFLOW_STYLE=trunk`.
|
|
54
|
+
|
|
55
|
+
### Upgrade steps
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
cd ~/Dev/carson
|
|
59
|
+
git pull
|
|
60
|
+
bash install.sh
|
|
61
|
+
carson version
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Engineering Appendix
|
|
65
|
+
|
|
66
|
+
#### Modified components
|
|
67
|
+
|
|
68
|
+
- `lib/carson/runtime.rb` — added `concise?` flag and `with_captured_output` helper for suppressing sub-command detail during onboard.
|
|
69
|
+
- `lib/carson/runtime/local.rb` — rewrote `onboard!` to use concise orchestration (`onboard_apply!`), added `onboard_report_remote!` and `onboard_run_audit!` helpers, added `unless concise?` guards to `prepare!` and `template_apply!`.
|
|
70
|
+
- `install.sh` — capture `gem install` stderr and filter out RubyGems PATH warning.
|
|
71
|
+
- `script/install_global_carson.sh` — same PATH warning suppression.
|
|
72
|
+
- `test/runtime_govern_test.rb` — updated onboard output assertions.
|
|
73
|
+
|
|
74
|
+
#### Public interface and config changes
|
|
75
|
+
|
|
76
|
+
- No new CLI commands or config keys.
|
|
77
|
+
- Exit status contract unchanged.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
8
81
|
## 2.8.0 — Interactive Setup and Remote Detection
|
|
9
82
|
|
|
10
83
|
### What changed
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.
|
|
1
|
+
2.9.0
|
data/lib/carson/cli.rb
CHANGED
|
@@ -19,7 +19,8 @@ module Carson
|
|
|
19
19
|
return Runtime::EXIT_ERROR
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
verbose = parsed.fetch( :verbose, false )
|
|
23
|
+
runtime = Runtime.new( repo_root: target_repo_root, tool_root: tool_root, out: out, err: err, verbose: verbose )
|
|
23
24
|
dispatch( parsed: parsed, runtime: runtime )
|
|
24
25
|
rescue ConfigError => e
|
|
25
26
|
err.puts "#{BADGE} CONFIG ERROR: #{e.message}"
|
|
@@ -30,12 +31,14 @@ module Carson
|
|
|
30
31
|
end
|
|
31
32
|
|
|
32
33
|
def self.parse_args( argv:, out:, err: )
|
|
34
|
+
verbose = argv.delete( "--verbose" ) ? true : false
|
|
33
35
|
parser = build_parser
|
|
34
36
|
preset = parse_preset_command( argv: argv, out: out, parser: parser )
|
|
35
|
-
return preset unless preset.nil?
|
|
37
|
+
return preset.merge( verbose: verbose ) unless preset.nil?
|
|
36
38
|
|
|
37
39
|
command = argv.shift
|
|
38
|
-
parse_command( command: command, argv: argv, parser: parser, err: err )
|
|
40
|
+
result = parse_command( command: command, argv: argv, parser: parser, err: err )
|
|
41
|
+
result.merge( verbose: verbose )
|
|
39
42
|
rescue OptionParser::ParseError => e
|
|
40
43
|
err.puts "#{BADGE} #{e.message}"
|
|
41
44
|
err.puts parser
|
data/lib/carson/runtime/audit.rb
CHANGED
|
@@ -8,42 +8,62 @@ module Carson
|
|
|
8
8
|
fingerprint_status = block_if_outsider_fingerprints!
|
|
9
9
|
return fingerprint_status unless fingerprint_status.nil?
|
|
10
10
|
audit_state = "ok"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
audit_concise_problems = []
|
|
12
|
+
puts_verbose ""
|
|
13
|
+
puts_verbose "[Repository]"
|
|
14
|
+
puts_verbose "root: #{repo_root}"
|
|
15
|
+
puts_verbose "current_branch: #{current_branch}"
|
|
16
|
+
puts_verbose ""
|
|
17
|
+
puts_verbose "[Working Tree]"
|
|
18
|
+
puts_verbose git_capture!( "status", "--short", "--branch" ).strip
|
|
19
|
+
puts_verbose ""
|
|
20
|
+
puts_verbose "[Hooks]"
|
|
17
21
|
hooks_ok = hooks_health_report
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
unless hooks_ok
|
|
23
|
+
audit_state = "block"
|
|
24
|
+
audit_concise_problems << "Hooks: mismatch — run carson prepare."
|
|
25
|
+
end
|
|
26
|
+
puts_verbose ""
|
|
27
|
+
puts_verbose "[Local Lint Quality]"
|
|
20
28
|
local_lint_quality = local_lint_quality_report
|
|
21
|
-
|
|
22
|
-
|
|
29
|
+
if local_lint_quality.fetch( :status ) == "block"
|
|
30
|
+
audit_state = "block"
|
|
31
|
+
blocking_langs = local_lint_quality.fetch( :languages ).select { |l| l.fetch( :status ) == "block" }
|
|
32
|
+
blocking_langs.each do |lang|
|
|
33
|
+
exit_code = lang.fetch( :exit_code, 1 )
|
|
34
|
+
audit_concise_problems << "Lint: #{lang.fetch( :language )} failed (exit #{exit_code})."
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
puts_verbose ""
|
|
38
|
+
puts_verbose "[Main Sync Status]"
|
|
23
39
|
ahead_count, behind_count, main_error = main_sync_counts
|
|
24
40
|
if main_error
|
|
25
|
-
|
|
26
|
-
|
|
41
|
+
puts_verbose "main_vs_remote_main: unknown"
|
|
42
|
+
puts_verbose "WARN: unable to calculate main sync status (#{main_error})."
|
|
27
43
|
audit_state = "attention" if audit_state == "ok"
|
|
28
44
|
elsif ahead_count.positive?
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
45
|
+
puts_verbose "main_vs_remote_main_ahead: #{ahead_count}"
|
|
46
|
+
puts_verbose "main_vs_remote_main_behind: #{behind_count}"
|
|
47
|
+
puts_verbose "ACTION: local #{config.main_branch} is ahead of #{config.git_remote}/#{config.main_branch} by #{ahead_count} commit#{plural_suffix( count: ahead_count )}; reset local drift before commit/push workflows."
|
|
32
48
|
audit_state = "block"
|
|
49
|
+
audit_concise_problems << "Main sync: ahead by #{ahead_count} — reset local drift."
|
|
33
50
|
elsif behind_count.positive?
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
51
|
+
puts_verbose "main_vs_remote_main_ahead: #{ahead_count}"
|
|
52
|
+
puts_verbose "main_vs_remote_main_behind: #{behind_count}"
|
|
53
|
+
puts_verbose "ACTION: local #{config.main_branch} is behind #{config.git_remote}/#{config.main_branch} by #{behind_count} commit#{plural_suffix( count: behind_count )}; run carson sync."
|
|
37
54
|
audit_state = "attention" if audit_state == "ok"
|
|
55
|
+
audit_concise_problems << "Main sync: behind by #{behind_count} — run carson sync."
|
|
38
56
|
else
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
57
|
+
puts_verbose "main_vs_remote_main_ahead: 0"
|
|
58
|
+
puts_verbose "main_vs_remote_main_behind: 0"
|
|
59
|
+
puts_verbose "ACTION: local #{config.main_branch} is in sync with #{config.git_remote}/#{config.main_branch}."
|
|
42
60
|
end
|
|
43
|
-
|
|
61
|
+
puts_verbose ""
|
|
62
|
+
puts_verbose "[PR and Required Checks (gh)]"
|
|
44
63
|
monitor_report = pr_and_check_report
|
|
45
64
|
audit_state = "attention" if audit_state == "ok" && monitor_report.fetch( :status ) != "ok"
|
|
46
|
-
|
|
65
|
+
puts_verbose ""
|
|
66
|
+
puts_verbose "[Default Branch CI Baseline (gh)]"
|
|
47
67
|
default_branch_baseline = default_branch_ci_baseline_report
|
|
48
68
|
audit_state = "block" if default_branch_baseline.fetch( :status ) == "block"
|
|
49
69
|
audit_state = "attention" if audit_state == "ok" && default_branch_baseline.fetch( :status ) != "ok"
|
|
@@ -56,9 +76,14 @@ module Carson
|
|
|
56
76
|
audit_status: audit_state
|
|
57
77
|
)
|
|
58
78
|
)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
79
|
+
puts_verbose ""
|
|
80
|
+
puts_verbose "[Audit Result]"
|
|
81
|
+
puts_verbose "status: #{audit_state}"
|
|
82
|
+
puts_verbose( audit_state == "block" ? "ACTION: local policy block must be resolved before commit/push." : "ACTION: no local hard block detected." )
|
|
83
|
+
unless verbose?
|
|
84
|
+
audit_concise_problems.each { |problem| puts_line problem }
|
|
85
|
+
puts_line "Audit: #{audit_state}"
|
|
86
|
+
end
|
|
62
87
|
audit_state == "block" ? EXIT_BLOCK : EXIT_OK
|
|
63
88
|
end
|
|
64
89
|
|
|
@@ -83,7 +108,7 @@ module Carson
|
|
|
83
108
|
unless gh_available?
|
|
84
109
|
report[ :status ] = "skipped"
|
|
85
110
|
report[ :skip_reason ] = "gh CLI not available in PATH"
|
|
86
|
-
|
|
111
|
+
puts_verbose "SKIP: #{report.fetch( :skip_reason )}"
|
|
87
112
|
return report
|
|
88
113
|
end
|
|
89
114
|
pr_stdout, pr_stderr, pr_success, = gh_run( "pr", "view", current_branch, "--json", "number,title,url,state,reviewDecision" )
|
|
@@ -91,7 +116,7 @@ module Carson
|
|
|
91
116
|
error_text = gh_error_text( stdout_text: pr_stdout, stderr_text: pr_stderr, fallback: "unable to read PR for branch #{current_branch}" )
|
|
92
117
|
report[ :status ] = "skipped"
|
|
93
118
|
report[ :skip_reason ] = error_text
|
|
94
|
-
|
|
119
|
+
puts_verbose "SKIP: #{error_text}"
|
|
95
120
|
return report
|
|
96
121
|
end
|
|
97
122
|
pr_data = JSON.parse( pr_stdout )
|
|
@@ -102,16 +127,16 @@ module Carson
|
|
|
102
127
|
state: pr_data[ "state" ].to_s,
|
|
103
128
|
review_decision: blank_to( value: pr_data[ "reviewDecision" ], default: "NONE" )
|
|
104
129
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
130
|
+
puts_verbose "pr: ##{report.dig( :pr, :number )} #{report.dig( :pr, :title )}"
|
|
131
|
+
puts_verbose "url: #{report.dig( :pr, :url )}"
|
|
132
|
+
puts_verbose "review_decision: #{report.dig( :pr, :review_decision )}"
|
|
108
133
|
checks_stdout, checks_stderr, checks_success, checks_exit = gh_run( "pr", "checks", report.dig( :pr, :number ).to_s, "--required", "--json", "name,state,bucket,workflow,link" )
|
|
109
134
|
if checks_stdout.to_s.strip.empty?
|
|
110
135
|
error_text = gh_error_text( stdout_text: checks_stdout, stderr_text: checks_stderr, fallback: "required checks unavailable" )
|
|
111
136
|
report[ :checks ][ :status ] = "skipped"
|
|
112
137
|
report[ :checks ][ :skip_reason ] = error_text
|
|
113
138
|
report[ :status ] = "attention"
|
|
114
|
-
|
|
139
|
+
puts_verbose "checks: SKIP (#{error_text})"
|
|
115
140
|
return report
|
|
116
141
|
end
|
|
117
142
|
checks_data = JSON.parse( checks_stdout )
|
|
@@ -123,17 +148,17 @@ module Carson
|
|
|
123
148
|
report[ :checks ][ :pending_count ] = pending.count
|
|
124
149
|
report[ :checks ][ :failing ] = normalise_check_entries( entries: failing )
|
|
125
150
|
report[ :checks ][ :pending ] = normalise_check_entries( entries: pending )
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
report.dig( :checks, :failing ).each { |entry|
|
|
130
|
-
report.dig( :checks, :pending ).each { |entry|
|
|
151
|
+
puts_verbose "required_checks_total: #{report.dig( :checks, :required_total )}"
|
|
152
|
+
puts_verbose "required_checks_failing: #{report.dig( :checks, :failing_count )}"
|
|
153
|
+
puts_verbose "required_checks_pending: #{report.dig( :checks, :pending_count )}"
|
|
154
|
+
report.dig( :checks, :failing ).each { |entry| puts_verbose "check_fail: #{entry.fetch( :workflow )} / #{entry.fetch( :name )} #{entry.fetch( :link )}".strip }
|
|
155
|
+
report.dig( :checks, :pending ).each { |entry| puts_verbose "check_pending: #{entry.fetch( :workflow )} / #{entry.fetch( :name )} #{entry.fetch( :link )}".strip }
|
|
131
156
|
report[ :status ] = "attention" if report.dig( :checks, :failing_count ).positive? || report.dig( :checks, :pending_count ).positive?
|
|
132
157
|
report
|
|
133
158
|
rescue JSON::ParserError => e
|
|
134
159
|
report[ :status ] = "skipped"
|
|
135
160
|
report[ :skip_reason ] = "invalid gh JSON response (#{e.message})"
|
|
136
|
-
|
|
161
|
+
puts_verbose "SKIP: #{report.fetch( :skip_reason )}"
|
|
137
162
|
report
|
|
138
163
|
end
|
|
139
164
|
|
|
@@ -148,8 +173,8 @@ module Carson
|
|
|
148
173
|
blocking_languages: 0,
|
|
149
174
|
languages: []
|
|
150
175
|
}
|
|
151
|
-
|
|
152
|
-
|
|
176
|
+
puts_verbose "lint_target_source: #{target_source}"
|
|
177
|
+
puts_verbose "lint_target_files_total: #{target_files.count}"
|
|
153
178
|
config.lint_languages.each do |language, entry|
|
|
154
179
|
language_report = lint_language_report(
|
|
155
180
|
language: language,
|
|
@@ -162,7 +187,7 @@ module Carson
|
|
|
162
187
|
report[ :status ] = "block"
|
|
163
188
|
report[ :blocking_languages ] += 1
|
|
164
189
|
end
|
|
165
|
-
|
|
190
|
+
puts_verbose "lint_blocking_languages: #{report.fetch( :blocking_languages )}"
|
|
166
191
|
report
|
|
167
192
|
rescue StandardError => e
|
|
168
193
|
report ||= {
|
|
@@ -191,7 +216,7 @@ module Carson
|
|
|
191
216
|
if github_pull_request_event?
|
|
192
217
|
files = lint_target_files_for_pull_request
|
|
193
218
|
return [ files, "github_pull_request" ] unless files.nil?
|
|
194
|
-
|
|
219
|
+
puts_verbose "WARN: unable to resolve pull request changed files; falling back to full repository files."
|
|
195
220
|
end
|
|
196
221
|
|
|
197
222
|
if github_actions_environment?
|
|
@@ -265,16 +290,16 @@ module Carson
|
|
|
265
290
|
config_files: entry.fetch( :config_files ),
|
|
266
291
|
exit_code: 0
|
|
267
292
|
}
|
|
268
|
-
|
|
293
|
+
puts_verbose "lint_language: #{language} enabled=#{report.fetch( :enabled )} files=#{report.fetch( :file_count )}"
|
|
269
294
|
if language == "ruby" && outsider_mode?
|
|
270
295
|
local_rubocop_path = File.join( repo_root, ".rubocop.yml" )
|
|
271
296
|
if File.file?( local_rubocop_path )
|
|
272
297
|
report[ :status ] = "block"
|
|
273
298
|
report[ :reason ] = "repo-local RuboCop config is forbidden: #{relative_path( local_rubocop_path )}; remove it and use ~/.carson/lint/rubocop.yml."
|
|
274
299
|
report[ :exit_code ] = EXIT_BLOCK
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
300
|
+
puts_verbose "lint_#{language}_status: block"
|
|
301
|
+
puts_verbose "lint_#{language}_reason: #{report.fetch( :reason )}"
|
|
302
|
+
puts_verbose "ACTION: remove .rubocop.yml from this repository and run carson lint setup --source <path-or-git-url>."
|
|
278
303
|
return report
|
|
279
304
|
end
|
|
280
305
|
end
|
|
@@ -286,9 +311,9 @@ module Carson
|
|
|
286
311
|
report[ :status ] = "block"
|
|
287
312
|
report[ :reason ] = "missing config files: #{missing_config_files.join( ', ' )}"
|
|
288
313
|
report[ :exit_code ] = EXIT_BLOCK
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
314
|
+
puts_verbose "lint_#{language}_status: block"
|
|
315
|
+
puts_verbose "lint_#{language}_reason: #{report.fetch( :reason )}"
|
|
316
|
+
puts_verbose "ACTION: run carson lint setup --source <path-or-git-url> to prepare ~/.carson/lint policy files."
|
|
292
317
|
return report
|
|
293
318
|
end
|
|
294
319
|
|
|
@@ -298,16 +323,16 @@ module Carson
|
|
|
298
323
|
report[ :status ] = "block"
|
|
299
324
|
report[ :reason ] = "missing lint command"
|
|
300
325
|
report[ :exit_code ] = EXIT_BLOCK
|
|
301
|
-
|
|
302
|
-
|
|
326
|
+
puts_verbose "lint_#{language}_status: block"
|
|
327
|
+
puts_verbose "lint_#{language}_reason: #{report.fetch( :reason )}"
|
|
303
328
|
return report
|
|
304
329
|
end
|
|
305
330
|
unless command_available_for_lint?( command_name: command_name )
|
|
306
331
|
report[ :status ] = "block"
|
|
307
332
|
report[ :reason ] = "command not available: #{command_name}"
|
|
308
333
|
report[ :exit_code ] = EXIT_BLOCK
|
|
309
|
-
|
|
310
|
-
|
|
334
|
+
puts_verbose "lint_#{language}_status: block"
|
|
335
|
+
puts_verbose "lint_#{language}_reason: #{report.fetch( :reason )}"
|
|
311
336
|
return report
|
|
312
337
|
end
|
|
313
338
|
|
|
@@ -322,9 +347,9 @@ module Carson
|
|
|
322
347
|
fallback: "lint command failed for #{language}"
|
|
323
348
|
)
|
|
324
349
|
end
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
350
|
+
puts_verbose "lint_#{language}_status: #{report.fetch( :status )}"
|
|
351
|
+
puts_verbose "lint_#{language}_exit: #{report.fetch( :exit_code )}"
|
|
352
|
+
puts_verbose "lint_#{language}_reason: #{report.fetch( :reason )}" unless report.fetch( :reason ).nil?
|
|
328
353
|
report
|
|
329
354
|
end
|
|
330
355
|
|
|
@@ -405,7 +430,7 @@ module Carson
|
|
|
405
430
|
unless gh_available?
|
|
406
431
|
report[ :status ] = "skipped"
|
|
407
432
|
report[ :skip_reason ] = "gh CLI not available in PATH"
|
|
408
|
-
|
|
433
|
+
puts_verbose "baseline: SKIP (#{report.fetch( :skip_reason )})"
|
|
409
434
|
return report
|
|
410
435
|
end
|
|
411
436
|
owner, repo = repository_coordinates
|
|
@@ -455,32 +480,32 @@ module Carson
|
|
|
455
480
|
report[ :status ] = "block" if report.fetch( :pending_count ).positive?
|
|
456
481
|
report[ :status ] = "block" if report.fetch( :no_check_evidence )
|
|
457
482
|
report[ :status ] = "attention" if report.fetch( :status ) == "ok" && ( report.fetch( :advisory_failing_count ).positive? || report.fetch( :advisory_pending_count ).positive? )
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
report.fetch( :failing ).each { |entry|
|
|
468
|
-
report.fetch( :pending ).each { |entry|
|
|
469
|
-
report.fetch( :advisory_failing ).each { |entry|
|
|
470
|
-
report.fetch( :advisory_pending ).each { |entry|
|
|
483
|
+
puts_verbose "default_branch_repository: #{report.fetch( :repository )}"
|
|
484
|
+
puts_verbose "default_branch_name: #{report.fetch( :default_branch )}"
|
|
485
|
+
puts_verbose "default_branch_head_sha: #{report.fetch( :head_sha )}"
|
|
486
|
+
puts_verbose "default_branch_workflows_total: #{report.fetch( :workflows_total )}"
|
|
487
|
+
puts_verbose "default_branch_check_runs_total: #{report.fetch( :check_runs_total )}"
|
|
488
|
+
puts_verbose "default_branch_failing: #{report.fetch( :failing_count )}"
|
|
489
|
+
puts_verbose "default_branch_pending: #{report.fetch( :pending_count )}"
|
|
490
|
+
puts_verbose "default_branch_advisory_failing: #{report.fetch( :advisory_failing_count )}"
|
|
491
|
+
puts_verbose "default_branch_advisory_pending: #{report.fetch( :advisory_pending_count )}"
|
|
492
|
+
report.fetch( :failing ).each { |entry| puts_verbose "default_branch_check_fail: #{entry.fetch( :workflow )} / #{entry.fetch( :name )} #{entry.fetch( :link )}".strip }
|
|
493
|
+
report.fetch( :pending ).each { |entry| puts_verbose "default_branch_check_pending: #{entry.fetch( :workflow )} / #{entry.fetch( :name )} #{entry.fetch( :link )}".strip }
|
|
494
|
+
report.fetch( :advisory_failing ).each { |entry| puts_verbose "default_branch_check_advisory_fail: #{entry.fetch( :workflow )} / #{entry.fetch( :name )} (advisory) #{entry.fetch( :link )}".strip }
|
|
495
|
+
report.fetch( :advisory_pending ).each { |entry| puts_verbose "default_branch_check_advisory_pending: #{entry.fetch( :workflow )} / #{entry.fetch( :name )} (advisory) #{entry.fetch( :link )}".strip }
|
|
471
496
|
if report.fetch( :no_check_evidence )
|
|
472
|
-
|
|
497
|
+
puts_verbose "ACTION: default branch has workflow files but no check-runs; align workflow triggers and branch protection check names."
|
|
473
498
|
end
|
|
474
499
|
report
|
|
475
500
|
rescue JSON::ParserError => e
|
|
476
501
|
report[ :status ] = "skipped"
|
|
477
502
|
report[ :skip_reason ] = "invalid gh JSON response (#{e.message})"
|
|
478
|
-
|
|
503
|
+
puts_verbose "baseline: SKIP (#{report.fetch( :skip_reason )})"
|
|
479
504
|
report
|
|
480
505
|
rescue StandardError => e
|
|
481
506
|
report[ :status ] = "skipped"
|
|
482
507
|
report[ :skip_reason ] = e.message
|
|
483
|
-
|
|
508
|
+
puts_verbose "baseline: SKIP (#{report.fetch( :skip_reason )})"
|
|
484
509
|
report
|
|
485
510
|
end
|
|
486
511
|
|
|
@@ -577,10 +602,10 @@ module Carson
|
|
|
577
602
|
# Writes monitor report artefacts and prints their locations.
|
|
578
603
|
def write_and_print_pr_monitor_report( report: )
|
|
579
604
|
markdown_path, json_path = write_pr_monitor_report( report: report )
|
|
580
|
-
|
|
581
|
-
|
|
605
|
+
puts_verbose "report_markdown: #{markdown_path}"
|
|
606
|
+
puts_verbose "report_json: #{json_path}"
|
|
582
607
|
rescue StandardError => e
|
|
583
|
-
|
|
608
|
+
puts_verbose "report_write: SKIP (#{e.message})"
|
|
584
609
|
end
|
|
585
610
|
|
|
586
611
|
# Persists report in both machine-readable JSON and human-readable Markdown.
|
|
@@ -703,28 +728,29 @@ module Carson
|
|
|
703
728
|
return { status: "ok", split_required: false } if files.empty?
|
|
704
729
|
|
|
705
730
|
scope = scope_integrity_status( files: files, branch: current_branch )
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
scope.fetch( :unmatched_paths ).
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
731
|
+
puts_verbose ""
|
|
732
|
+
puts_verbose "[Scope Integrity Guard]"
|
|
733
|
+
puts_verbose "scope_file_source: #{files_source}"
|
|
734
|
+
puts_verbose "scope_file_count: #{files.count}"
|
|
735
|
+
puts_verbose "branch: #{scope.fetch( :branch )}"
|
|
736
|
+
puts_verbose "scope_basis: changed_paths_only"
|
|
737
|
+
puts_verbose "detected_groups: #{scope.fetch( :detected_groups ).sort.join( ', ' )}"
|
|
738
|
+
puts_verbose "core_groups: #{scope.fetch( :core_groups ).empty? ? 'none' : scope.fetch( :core_groups ).sort.join( ', ' )}"
|
|
739
|
+
puts_verbose "non_doc_groups: #{scope.fetch( :non_doc_groups ).empty? ? 'none' : scope.fetch( :non_doc_groups ).sort.join( ', ' )}"
|
|
740
|
+
puts_verbose "docs_only_changes: #{scope.fetch( :docs_only )}"
|
|
741
|
+
puts_verbose "unmatched_paths_count: #{scope.fetch( :unmatched_paths ).count}"
|
|
742
|
+
scope.fetch( :unmatched_paths ).each { |path| puts_verbose "unmatched_path: #{path}" }
|
|
743
|
+
puts_verbose "violating_files_count: #{scope.fetch( :violating_files ).count}"
|
|
744
|
+
scope.fetch( :violating_files ).each { |path| puts_verbose "violating_file: #{path} (group=#{scope.fetch( :grouped_paths ).fetch( path )})" }
|
|
745
|
+
puts_verbose "checklist_single_business_intent: pass"
|
|
746
|
+
puts_verbose "checklist_single_scope_group: #{scope.fetch( :split_required ) ? 'advisory' : 'pass'}"
|
|
747
|
+
puts_verbose "checklist_cross_boundary_changes_justified: #{( scope.fetch( :split_required ) || scope.fetch( :misc_present ) ) ? 'advisory' : 'pass'}"
|
|
722
748
|
if scope.fetch( :split_required )
|
|
723
|
-
|
|
749
|
+
puts_verbose "ACTION: multiple module groups detected (informational only)."
|
|
724
750
|
elsif scope.fetch( :misc_present )
|
|
725
|
-
|
|
751
|
+
puts_verbose "ACTION: unmatched paths detected; classify via scope.path_groups for stricter module checks."
|
|
726
752
|
else
|
|
727
|
-
|
|
753
|
+
puts_verbose "ACTION: scope integrity is within commit policy."
|
|
728
754
|
end
|
|
729
755
|
{ status: scope.fetch( :status ), split_required: scope.fetch( :split_required ) }
|
|
730
756
|
end
|
|
@@ -83,7 +83,8 @@ module Carson
|
|
|
83
83
|
|
|
84
84
|
# Standalone housekeep: sync + prune.
|
|
85
85
|
def housekeep!
|
|
86
|
-
|
|
86
|
+
puts_verbose ""
|
|
87
|
+
puts_verbose "[Housekeep]"
|
|
87
88
|
sync_status = sync!
|
|
88
89
|
if sync_status != EXIT_OK
|
|
89
90
|
puts_line "housekeep: sync returned #{sync_status}; skipping prune."
|
|
@@ -571,8 +572,8 @@ module Carson
|
|
|
571
572
|
md_path = File.join( report_dir, GOVERN_REPORT_MD )
|
|
572
573
|
File.write( json_path, JSON.pretty_generate( report ) )
|
|
573
574
|
File.write( md_path, render_govern_markdown( report: report ) )
|
|
574
|
-
|
|
575
|
-
|
|
575
|
+
puts_verbose "report_json: #{json_path}"
|
|
576
|
+
puts_verbose "report_markdown: #{md_path}"
|
|
576
577
|
end
|
|
577
578
|
|
|
578
579
|
def render_govern_markdown( report: )
|
|
@@ -632,7 +633,11 @@ module Carson
|
|
|
632
633
|
end
|
|
633
634
|
|
|
634
635
|
repos_count = Array( report[ :repos ] ).length
|
|
635
|
-
|
|
636
|
+
if verbose?
|
|
637
|
+
puts_line "govern_summary: repos=#{repos_count} prs=#{total_prs} ready=#{ready_count} blocked=#{blocked_count}"
|
|
638
|
+
else
|
|
639
|
+
puts_line "Govern: #{repos_count} repo#{plural_suffix( count: repos_count )}, #{total_prs} PR#{plural_suffix( count: total_prs )} (#{ready_count} ready, #{blocked_count} blocked)"
|
|
640
|
+
end
|
|
636
641
|
end
|
|
637
642
|
end
|
|
638
643
|
|
data/lib/carson/runtime/lint.rb
CHANGED
|
@@ -7,7 +7,8 @@ module Carson
|
|
|
7
7
|
module Lint
|
|
8
8
|
# Prepares canonical lint policy files under ~/.carson/lint from an explicit source.
|
|
9
9
|
def lint_setup!( source:, ref: "main", force: false )
|
|
10
|
-
|
|
10
|
+
puts_verbose ""
|
|
11
|
+
puts_verbose "[Lint Setup]"
|
|
11
12
|
source_text = source.to_s.strip
|
|
12
13
|
if source_text.empty?
|
|
13
14
|
puts_line "ERROR: lint setup requires --source <path-or-git-url>."
|
|
@@ -29,12 +30,12 @@ module Carson
|
|
|
29
30
|
target_coding_dir: target_coding_dir,
|
|
30
31
|
force: force
|
|
31
32
|
)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
puts_verbose "lint_setup_source: #{source_text}"
|
|
34
|
+
puts_verbose "lint_setup_ref: #{ref_text}" if lint_source_git_url?( source: source_text )
|
|
35
|
+
puts_verbose "lint_setup_target: #{target_coding_dir}"
|
|
36
|
+
puts_verbose "lint_setup_created: #{copy_result.fetch( :created )}"
|
|
37
|
+
puts_verbose "lint_setup_updated: #{copy_result.fetch( :updated )}"
|
|
38
|
+
puts_verbose "lint_setup_skipped: #{copy_result.fetch( :skipped )}"
|
|
38
39
|
|
|
39
40
|
missing_policy = missing_lint_policy_files
|
|
40
41
|
if missing_policy.empty?
|
|
@@ -43,7 +44,7 @@ module Carson
|
|
|
43
44
|
end
|
|
44
45
|
|
|
45
46
|
missing_policy.each do |entry|
|
|
46
|
-
|
|
47
|
+
puts_verbose "missing_lint_policy_file: language=#{entry.fetch( :language )} path=#{entry.fetch( :path )}"
|
|
47
48
|
end
|
|
48
49
|
puts_line "ACTION: update source CODING policy files, rerun carson lint setup, then rerun carson audit."
|
|
49
50
|
EXIT_ERROR
|
data/lib/carson/runtime/local.rb
CHANGED
|
@@ -43,12 +43,19 @@ module Carson
|
|
|
43
43
|
return prune_no_stale_branches if stale_branches.empty?
|
|
44
44
|
|
|
45
45
|
counters = prune_stale_branch_entries( stale_branches: stale_branches, active_branch: active_branch )
|
|
46
|
-
|
|
46
|
+
puts_verbose "prune_summary: deleted=#{counters.fetch( :deleted )} skipped=#{counters.fetch( :skipped )}"
|
|
47
|
+
unless verbose?
|
|
48
|
+
puts_line "Pruned #{counters.fetch( :deleted )} stale branch#{plural_suffix( count: counters.fetch( :deleted ) )}."
|
|
49
|
+
end
|
|
47
50
|
EXIT_OK
|
|
48
51
|
end
|
|
49
52
|
|
|
50
53
|
def prune_no_stale_branches
|
|
51
|
-
|
|
54
|
+
if verbose?
|
|
55
|
+
puts_line "OK: no stale local branches tracking deleted #{config.git_remote} branches."
|
|
56
|
+
else
|
|
57
|
+
puts_line "No stale branches."
|
|
58
|
+
end
|
|
52
59
|
EXIT_OK
|
|
53
60
|
end
|
|
54
61
|
|
|
@@ -72,7 +79,7 @@ module Carson
|
|
|
72
79
|
|
|
73
80
|
def prune_skip_stale_branch( type:, branch:, upstream: )
|
|
74
81
|
status = type == :protected ? "skip_protected_branch" : "skip_current_branch"
|
|
75
|
-
|
|
82
|
+
puts_verbose "#{status}: #{branch} (upstream=#{upstream})"
|
|
76
83
|
:skipped
|
|
77
84
|
end
|
|
78
85
|
|
|
@@ -89,8 +96,8 @@ module Carson
|
|
|
89
96
|
end
|
|
90
97
|
|
|
91
98
|
def prune_safe_delete_success( branch:, upstream:, stdout_text: )
|
|
92
|
-
out.print stdout_text
|
|
93
|
-
|
|
99
|
+
out.print stdout_text if verbose? && !stdout_text.empty?
|
|
100
|
+
puts_verbose "deleted_local_branch: #{branch} (upstream=#{upstream})"
|
|
94
101
|
:deleted
|
|
95
102
|
end
|
|
96
103
|
|
|
@@ -108,20 +115,20 @@ module Carson
|
|
|
108
115
|
end
|
|
109
116
|
|
|
110
117
|
def prune_force_delete_success( branch:, upstream:, merged_pr:, force_stdout: )
|
|
111
|
-
out.print force_stdout
|
|
112
|
-
|
|
118
|
+
out.print force_stdout if verbose? && !force_stdout.empty?
|
|
119
|
+
puts_verbose "deleted_local_branch_force: #{branch} (upstream=#{upstream}) merged_pr=#{merged_pr.fetch( :url )}"
|
|
113
120
|
:deleted
|
|
114
121
|
end
|
|
115
122
|
|
|
116
123
|
def prune_force_delete_failed( branch:, upstream:, force_stderr: )
|
|
117
124
|
force_error_text = normalise_branch_delete_error( error_text: force_stderr )
|
|
118
|
-
|
|
125
|
+
puts_verbose "fail_force_delete_branch: #{branch} (upstream=#{upstream}) reason=#{force_error_text}"
|
|
119
126
|
:skipped
|
|
120
127
|
end
|
|
121
128
|
|
|
122
129
|
def prune_force_delete_skipped( branch:, upstream:, delete_error_text:, force_error: )
|
|
123
|
-
|
|
124
|
-
|
|
130
|
+
puts_verbose "skip_delete_branch: #{branch} (upstream=#{upstream}) reason=#{delete_error_text}"
|
|
131
|
+
puts_verbose "skip_force_delete_branch: #{branch} (upstream=#{upstream}) reason=#{force_error}" unless force_error.to_s.strip.empty?
|
|
125
132
|
:skipped
|
|
126
133
|
end
|
|
127
134
|
|
|
@@ -153,26 +160,34 @@ module Carson
|
|
|
153
160
|
target_path = File.join( hooks_dir, hook_name )
|
|
154
161
|
FileUtils.cp( source_path, target_path )
|
|
155
162
|
FileUtils.chmod( 0o755, target_path )
|
|
156
|
-
|
|
163
|
+
puts_verbose "hook_written: #{relative_path( target_path )}"
|
|
157
164
|
end
|
|
158
165
|
git_system!( "config", "core.hooksPath", hooks_dir )
|
|
159
166
|
File.write( File.join( hooks_dir, "workflow_style" ), config.workflow_style )
|
|
160
|
-
|
|
167
|
+
puts_verbose "configured_hooks_path: #{hooks_dir}"
|
|
168
|
+
unless verbose?
|
|
169
|
+
puts_line "Hooks installed (#{config.required_hooks.count} hooks)."
|
|
170
|
+
return EXIT_OK
|
|
171
|
+
end
|
|
172
|
+
|
|
161
173
|
inspect!
|
|
162
174
|
end
|
|
163
175
|
|
|
164
176
|
# One-command onboarding for new repositories: detect remote, install hooks,
|
|
165
|
-
# apply templates, and
|
|
177
|
+
# apply templates, and run initial audit.
|
|
166
178
|
def onboard!
|
|
167
179
|
fingerprint_status = block_if_outsider_fingerprints!
|
|
168
180
|
return fingerprint_status unless fingerprint_status.nil?
|
|
169
181
|
|
|
170
|
-
print_header "Onboard"
|
|
171
182
|
unless inside_git_work_tree?
|
|
172
183
|
puts_line "ERROR: #{repo_root} is not a git repository."
|
|
173
184
|
return EXIT_ERROR
|
|
174
185
|
end
|
|
175
186
|
|
|
187
|
+
repo_name = File.basename( repo_root )
|
|
188
|
+
puts_line ""
|
|
189
|
+
puts_line "Onboarding #{repo_name}..."
|
|
190
|
+
|
|
176
191
|
unless global_config_exists?
|
|
177
192
|
if self.in.respond_to?( :tty? ) && self.in.tty?
|
|
178
193
|
setup_status = setup!
|
|
@@ -182,21 +197,7 @@ module Carson
|
|
|
182
197
|
end
|
|
183
198
|
end
|
|
184
199
|
|
|
185
|
-
|
|
186
|
-
hook_status = prepare!
|
|
187
|
-
return hook_status unless hook_status == EXIT_OK
|
|
188
|
-
|
|
189
|
-
template_status = template_apply!
|
|
190
|
-
return template_status unless template_status == EXIT_OK
|
|
191
|
-
|
|
192
|
-
audit_status = audit!
|
|
193
|
-
if audit_status == EXIT_OK
|
|
194
|
-
puts_line "OK: Carson onboard completed for #{repo_root}."
|
|
195
|
-
elsif audit_status == EXIT_BLOCK
|
|
196
|
-
puts_line "BLOCK: Carson onboard completed with policy blocks; resolve and rerun carson audit."
|
|
197
|
-
end
|
|
198
|
-
print_onboarding_guidance
|
|
199
|
-
audit_status
|
|
200
|
+
onboard_apply!
|
|
200
201
|
end
|
|
201
202
|
|
|
202
203
|
# Re-applies hooks, templates, and audit after upgrading Carson.
|
|
@@ -204,29 +205,52 @@ module Carson
|
|
|
204
205
|
fingerprint_status = block_if_outsider_fingerprints!
|
|
205
206
|
return fingerprint_status unless fingerprint_status.nil?
|
|
206
207
|
|
|
207
|
-
print_header "Refresh"
|
|
208
208
|
unless inside_git_work_tree?
|
|
209
209
|
puts_line "ERROR: #{repo_root} is not a git repository."
|
|
210
210
|
return EXIT_ERROR
|
|
211
211
|
end
|
|
212
|
-
|
|
212
|
+
|
|
213
|
+
if verbose?
|
|
214
|
+
puts_verbose ""
|
|
215
|
+
puts_verbose "[Refresh]"
|
|
216
|
+
hook_status = prepare!
|
|
217
|
+
return hook_status unless hook_status == EXIT_OK
|
|
218
|
+
|
|
219
|
+
template_status = template_apply!
|
|
220
|
+
return template_status unless template_status == EXIT_OK
|
|
221
|
+
|
|
222
|
+
audit_status = audit!
|
|
223
|
+
if audit_status == EXIT_OK
|
|
224
|
+
puts_line "OK: Carson refresh completed for #{repo_root}."
|
|
225
|
+
elsif audit_status == EXIT_BLOCK
|
|
226
|
+
puts_line "BLOCK: Carson refresh completed with policy blocks; resolve and rerun carson audit."
|
|
227
|
+
end
|
|
228
|
+
return audit_status
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
puts_line "Refresh"
|
|
232
|
+
hook_status = with_captured_output { prepare! }
|
|
213
233
|
return hook_status unless hook_status == EXIT_OK
|
|
234
|
+
puts_line "Hooks installed (#{config.required_hooks.count} hooks)."
|
|
214
235
|
|
|
215
|
-
|
|
236
|
+
template_drift_count = template_results.count { |entry| entry.fetch( :status ) != "ok" }
|
|
237
|
+
template_status = with_captured_output { template_apply! }
|
|
216
238
|
return template_status unless template_status == EXIT_OK
|
|
239
|
+
if template_drift_count.positive?
|
|
240
|
+
puts_line "Templates applied (#{template_drift_count} updated)."
|
|
241
|
+
else
|
|
242
|
+
puts_line "Templates in sync."
|
|
243
|
+
end
|
|
217
244
|
|
|
218
245
|
audit_status = audit!
|
|
219
|
-
|
|
220
|
-
puts_line "OK: Carson refresh completed for #{repo_root}."
|
|
221
|
-
elsif audit_status == EXIT_BLOCK
|
|
222
|
-
puts_line "BLOCK: Carson refresh completed with policy blocks; resolve and rerun carson audit."
|
|
223
|
-
end
|
|
246
|
+
puts_line "Refresh complete."
|
|
224
247
|
audit_status
|
|
225
248
|
end
|
|
226
249
|
|
|
227
250
|
# Removes Carson-managed repository integration so a host repository can retire Carson cleanly.
|
|
228
251
|
def offboard!
|
|
229
|
-
|
|
252
|
+
puts_verbose ""
|
|
253
|
+
puts_verbose "[Offboard]"
|
|
230
254
|
unless inside_git_work_tree?
|
|
231
255
|
puts_line "ERROR: #{repo_root} is not a git repository."
|
|
232
256
|
return EXIT_ERROR
|
|
@@ -240,16 +264,20 @@ module Carson
|
|
|
240
264
|
absolute = resolve_repo_path!( relative_path: relative, label: "offboard target #{relative}" )
|
|
241
265
|
if File.exist?( absolute )
|
|
242
266
|
FileUtils.rm_rf( absolute )
|
|
243
|
-
|
|
267
|
+
puts_verbose "removed_path: #{relative}"
|
|
244
268
|
removed_count += 1
|
|
245
269
|
else
|
|
246
|
-
|
|
270
|
+
puts_verbose "skip_missing_path: #{relative}"
|
|
247
271
|
missing_count += 1
|
|
248
272
|
end
|
|
249
273
|
end
|
|
250
274
|
remove_empty_offboard_directories!
|
|
251
|
-
|
|
252
|
-
|
|
275
|
+
puts_verbose "offboard_summary: removed=#{removed_count} missing=#{missing_count}"
|
|
276
|
+
if verbose?
|
|
277
|
+
puts_line "OK: Carson offboard completed for #{repo_root}."
|
|
278
|
+
else
|
|
279
|
+
puts_line "Removed #{removed_count} file#{plural_suffix( count: removed_count )}. Offboard complete."
|
|
280
|
+
end
|
|
253
281
|
EXIT_OK
|
|
254
282
|
end
|
|
255
283
|
|
|
@@ -258,9 +286,13 @@ module Carson
|
|
|
258
286
|
fingerprint_status = block_if_outsider_fingerprints!
|
|
259
287
|
return fingerprint_status unless fingerprint_status.nil?
|
|
260
288
|
|
|
261
|
-
|
|
289
|
+
puts_verbose ""
|
|
290
|
+
puts_verbose "[Inspect]"
|
|
262
291
|
ok = hooks_health_report( strict: true )
|
|
263
|
-
|
|
292
|
+
puts_verbose( ok ? "status: ok" : "status: block" )
|
|
293
|
+
unless verbose?
|
|
294
|
+
puts_line( ok ? "Hooks: ok" : "Hooks: block" )
|
|
295
|
+
end
|
|
264
296
|
ok ? EXIT_OK : EXIT_BLOCK
|
|
265
297
|
end
|
|
266
298
|
|
|
@@ -269,14 +301,24 @@ module Carson
|
|
|
269
301
|
fingerprint_status = block_if_outsider_fingerprints!
|
|
270
302
|
return fingerprint_status unless fingerprint_status.nil?
|
|
271
303
|
|
|
272
|
-
|
|
304
|
+
puts_verbose ""
|
|
305
|
+
puts_verbose "[Template Sync Check]"
|
|
273
306
|
results = template_results
|
|
274
307
|
drift_count = results.count { |entry| entry.fetch( :status ) == "drift" }
|
|
275
308
|
error_count = results.count { |entry| entry.fetch( :status ) == "error" }
|
|
276
309
|
results.each do |entry|
|
|
277
|
-
|
|
310
|
+
puts_verbose "template_file: #{entry.fetch( :file )} status=#{entry.fetch( :status )} reason=#{entry.fetch( :reason )}"
|
|
311
|
+
end
|
|
312
|
+
puts_verbose "template_summary: total=#{results.count} drift=#{drift_count} error=#{error_count}"
|
|
313
|
+
unless verbose?
|
|
314
|
+
if drift_count.positive?
|
|
315
|
+
drift_files = results.select { |entry| entry.fetch( :status ) == "drift" }.map { |entry| entry.fetch( :file ) }
|
|
316
|
+
puts_line "Templates: #{drift_count} of #{results.count} drifted"
|
|
317
|
+
drift_files.each { |file| puts_line " #{file}" }
|
|
318
|
+
else
|
|
319
|
+
puts_line "Templates: #{results.count} files in sync"
|
|
320
|
+
end
|
|
278
321
|
end
|
|
279
|
-
puts_line "template_summary: total=#{results.count} drift=#{drift_count} error=#{error_count}"
|
|
280
322
|
return EXIT_ERROR if error_count.positive?
|
|
281
323
|
|
|
282
324
|
drift_count.positive? ? EXIT_BLOCK : EXIT_OK
|
|
@@ -287,29 +329,37 @@ module Carson
|
|
|
287
329
|
fingerprint_status = block_if_outsider_fingerprints!
|
|
288
330
|
return fingerprint_status unless fingerprint_status.nil?
|
|
289
331
|
|
|
290
|
-
|
|
332
|
+
puts_verbose ""
|
|
333
|
+
puts_verbose "[Template Sync Apply]"
|
|
291
334
|
results = template_results
|
|
292
335
|
applied = 0
|
|
293
336
|
results.each do |entry|
|
|
294
337
|
if entry.fetch( :status ) == "error"
|
|
295
|
-
|
|
338
|
+
puts_verbose "template_file: #{entry.fetch( :file )} status=error reason=#{entry.fetch( :reason )}"
|
|
296
339
|
next
|
|
297
340
|
end
|
|
298
341
|
|
|
299
342
|
file_path = File.join( repo_root, entry.fetch( :file ) )
|
|
300
343
|
if entry.fetch( :status ) == "ok"
|
|
301
|
-
|
|
344
|
+
puts_verbose "template_file: #{entry.fetch( :file )} status=ok reason=in_sync"
|
|
302
345
|
next
|
|
303
346
|
end
|
|
304
347
|
|
|
305
348
|
FileUtils.mkdir_p( File.dirname( file_path ) )
|
|
306
349
|
File.write( file_path, entry.fetch( :applied_content ) )
|
|
307
|
-
|
|
350
|
+
puts_verbose "template_file: #{entry.fetch( :file )} status=updated reason=#{entry.fetch( :reason )}"
|
|
308
351
|
applied += 1
|
|
309
352
|
end
|
|
310
353
|
|
|
311
354
|
error_count = results.count { |entry| entry.fetch( :status ) == "error" }
|
|
312
|
-
|
|
355
|
+
puts_verbose "template_apply_summary: updated=#{applied} error=#{error_count}"
|
|
356
|
+
unless verbose?
|
|
357
|
+
if applied.positive?
|
|
358
|
+
puts_line "Templates applied (#{applied} updated)."
|
|
359
|
+
else
|
|
360
|
+
puts_line "Templates in sync."
|
|
361
|
+
end
|
|
362
|
+
end
|
|
313
363
|
error_count.positive? ? EXIT_ERROR : EXIT_OK
|
|
314
364
|
end
|
|
315
365
|
|
|
@@ -370,9 +420,9 @@ module Carson
|
|
|
370
420
|
def print_hooks_path_status( configured:, expected: )
|
|
371
421
|
configured_abs = configured.nil? ? nil : File.expand_path( configured )
|
|
372
422
|
hooks_path_ok = configured_abs == expected
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
423
|
+
puts_verbose "hooks_path: #{configured || '(unset)'}"
|
|
424
|
+
puts_verbose "hooks_path_expected: #{expected}"
|
|
425
|
+
puts_verbose( hooks_path_ok ? "hooks_path_status: ok" : "hooks_path_status: attention" )
|
|
376
426
|
hooks_path_ok
|
|
377
427
|
end
|
|
378
428
|
|
|
@@ -381,7 +431,7 @@ module Carson
|
|
|
381
431
|
exists = File.file?( path )
|
|
382
432
|
symlink = File.symlink?( path )
|
|
383
433
|
executable = exists && !symlink && File.executable?( path )
|
|
384
|
-
|
|
434
|
+
puts_verbose "hook_file: #{relative_path( path )} exists=#{exists} symlink=#{symlink} executable=#{executable}"
|
|
385
435
|
end
|
|
386
436
|
end
|
|
387
437
|
|
|
@@ -406,13 +456,13 @@ module Carson
|
|
|
406
456
|
if strict && !hooks_path_ok
|
|
407
457
|
configured_text = configured.to_s.strip
|
|
408
458
|
if configured_text.empty?
|
|
409
|
-
|
|
459
|
+
puts_verbose "ACTION: hooks path is unset (expected=#{expected})."
|
|
410
460
|
else
|
|
411
|
-
|
|
461
|
+
puts_verbose "ACTION: hooks path mismatch (configured=#{configured_text}, expected=#{expected})."
|
|
412
462
|
end
|
|
413
463
|
end
|
|
414
464
|
message = strict ? "ACTION: run carson prepare to align hooks with Carson #{Carson::VERSION}." : "ACTION: run carson prepare to enforce local main protections."
|
|
415
|
-
|
|
465
|
+
puts_verbose message
|
|
416
466
|
end
|
|
417
467
|
|
|
418
468
|
# Returns ahead/behind counts for local main versus configured remote main.
|
|
@@ -611,17 +661,17 @@ module Carson
|
|
|
611
661
|
def disable_carson_hooks_path!
|
|
612
662
|
configured = configured_hooks_path
|
|
613
663
|
if configured.nil?
|
|
614
|
-
|
|
664
|
+
puts_verbose "hooks_path: (unset)"
|
|
615
665
|
return EXIT_OK
|
|
616
666
|
end
|
|
617
|
-
|
|
667
|
+
puts_verbose "hooks_path: #{configured}"
|
|
618
668
|
configured_abs = File.expand_path( configured, repo_root )
|
|
619
669
|
unless carson_managed_hooks_path?( configured_abs: configured_abs )
|
|
620
|
-
|
|
670
|
+
puts_verbose "hooks_path_kept: #{configured} (not Carson-managed)"
|
|
621
671
|
return EXIT_OK
|
|
622
672
|
end
|
|
623
673
|
git_system!( "config", "--unset", "core.hooksPath" )
|
|
624
|
-
|
|
674
|
+
puts_verbose "hooks_path_unset: core.hooksPath"
|
|
625
675
|
EXIT_OK
|
|
626
676
|
rescue StandardError => e
|
|
627
677
|
puts_line "ERROR: unable to update core.hooksPath (#{e.message})"
|
|
@@ -667,25 +717,77 @@ module Carson
|
|
|
667
717
|
next unless Dir.empty?( absolute )
|
|
668
718
|
|
|
669
719
|
Dir.rmdir( absolute )
|
|
670
|
-
|
|
720
|
+
puts_verbose "removed_empty_dir: #{relative}"
|
|
671
721
|
end
|
|
672
722
|
end
|
|
673
723
|
|
|
674
724
|
# Verifies configured remote exists and logs status without mutating remotes.
|
|
675
725
|
def report_detected_remote!
|
|
676
726
|
if git_remote_exists?( remote_name: config.git_remote )
|
|
677
|
-
|
|
727
|
+
puts_verbose "remote_ok: #{config.git_remote}"
|
|
678
728
|
else
|
|
679
729
|
puts_line "WARN: remote '#{config.git_remote}' not found; run carson setup to configure."
|
|
680
730
|
end
|
|
681
731
|
end
|
|
682
732
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
733
|
+
# Concise onboard orchestration: hooks, templates, remote, audit, guidance.
|
|
734
|
+
def onboard_apply!
|
|
735
|
+
hook_status = with_captured_output { prepare! }
|
|
736
|
+
return hook_status unless hook_status == EXIT_OK
|
|
737
|
+
puts_line "Hooks installed (#{config.required_hooks.count} hooks)."
|
|
738
|
+
|
|
739
|
+
template_drift_count = template_results.count { |entry| entry.fetch( :status ) != "ok" }
|
|
740
|
+
template_status = with_captured_output { template_apply! }
|
|
741
|
+
return template_status unless template_status == EXIT_OK
|
|
742
|
+
if template_drift_count.positive?
|
|
743
|
+
puts_line "Templates synced (#{template_drift_count} file#{plural_suffix( count: template_drift_count )} updated)."
|
|
744
|
+
else
|
|
745
|
+
puts_line "Templates in sync."
|
|
746
|
+
end
|
|
747
|
+
|
|
748
|
+
onboard_report_remote!
|
|
749
|
+
audit_status = onboard_run_audit!
|
|
750
|
+
|
|
686
751
|
puts_line ""
|
|
687
|
-
puts_line "
|
|
688
|
-
puts_line "
|
|
752
|
+
puts_line "Carson is ready. Workflow: #{config.workflow_style}"
|
|
753
|
+
puts_line "Reconfigure anytime: carson setup"
|
|
754
|
+
audit_status
|
|
755
|
+
end
|
|
756
|
+
|
|
757
|
+
# Friendly remote status for onboard output.
|
|
758
|
+
def onboard_report_remote!
|
|
759
|
+
if git_remote_exists?( remote_name: config.git_remote )
|
|
760
|
+
puts_line "Remote: #{config.git_remote} (connected)."
|
|
761
|
+
else
|
|
762
|
+
puts_line "Remote not configured yet — carson setup will walk you through it."
|
|
763
|
+
end
|
|
764
|
+
end
|
|
765
|
+
|
|
766
|
+
# Runs audit with captured output; reports summary instead of full detail.
|
|
767
|
+
def onboard_run_audit!
|
|
768
|
+
audit_error = nil
|
|
769
|
+
audit_status = with_captured_output { audit! }
|
|
770
|
+
rescue StandardError => e
|
|
771
|
+
audit_error = e
|
|
772
|
+
audit_status = EXIT_OK
|
|
773
|
+
ensure
|
|
774
|
+
return onboard_print_audit_result( status: audit_status, error: audit_error )
|
|
775
|
+
end
|
|
776
|
+
|
|
777
|
+
def onboard_print_audit_result( status:, error: )
|
|
778
|
+
if error
|
|
779
|
+
if error.message.to_s.match?( /HEAD|rev-parse/ )
|
|
780
|
+
puts_line "No commits yet — run carson audit after your first commit."
|
|
781
|
+
else
|
|
782
|
+
puts_line "Audit skipped — run carson audit for details."
|
|
783
|
+
end
|
|
784
|
+
return EXIT_OK
|
|
785
|
+
end
|
|
786
|
+
|
|
787
|
+
if status == EXIT_BLOCK
|
|
788
|
+
puts_line "Some checks need attention — run carson audit for details."
|
|
789
|
+
end
|
|
790
|
+
status
|
|
689
791
|
end
|
|
690
792
|
|
|
691
793
|
# Uses `git remote get-url` as existence check to avoid parsing remote lists.
|
|
@@ -8,10 +8,10 @@ module Carson
|
|
|
8
8
|
return unless config.review_wait_seconds.positive?
|
|
9
9
|
quick = review_gate_snapshot( owner: owner, repo: repo, pr_number: pr_number )
|
|
10
10
|
if quick[ :unresolved_threads ].empty? && quick[ :unacknowledged_actionable ].empty?
|
|
11
|
-
|
|
11
|
+
puts_verbose "warmup_skip: all threads resolved"
|
|
12
12
|
return quick
|
|
13
13
|
end
|
|
14
|
-
|
|
14
|
+
puts_verbose "warmup_wait_seconds: #{config.review_wait_seconds}"
|
|
15
15
|
sleep config.review_wait_seconds
|
|
16
16
|
nil
|
|
17
17
|
end
|
|
@@ -19,7 +19,7 @@ module Carson
|
|
|
19
19
|
# Poll delay between consecutive snapshot reads during convergence checks.
|
|
20
20
|
def wait_for_review_poll
|
|
21
21
|
return unless config.review_poll_seconds.positive?
|
|
22
|
-
|
|
22
|
+
puts_verbose "poll_wait_seconds: #{config.review_poll_seconds}"
|
|
23
23
|
sleep config.review_poll_seconds
|
|
24
24
|
end
|
|
25
25
|
|
|
@@ -173,10 +173,10 @@ module Carson
|
|
|
173
173
|
json_name: REVIEW_GATE_REPORT_JSON,
|
|
174
174
|
renderer: method( :render_review_gate_markdown )
|
|
175
175
|
)
|
|
176
|
-
|
|
177
|
-
|
|
176
|
+
puts_verbose "review_gate_report_markdown: #{markdown_path}"
|
|
177
|
+
puts_verbose "review_gate_report_json: #{json_path}"
|
|
178
178
|
rescue StandardError => e
|
|
179
|
-
|
|
179
|
+
puts_verbose "review_gate_report_write: SKIP (#{e.message})"
|
|
180
180
|
end
|
|
181
181
|
|
|
182
182
|
# Human-readable review gate report for merge-readiness evidence.
|
|
@@ -202,10 +202,10 @@ module Carson
|
|
|
202
202
|
json_name: REVIEW_SWEEP_REPORT_JSON,
|
|
203
203
|
renderer: method( :render_review_sweep_markdown )
|
|
204
204
|
)
|
|
205
|
-
|
|
206
|
-
|
|
205
|
+
puts_verbose "review_sweep_report_markdown: #{markdown_path}"
|
|
206
|
+
puts_verbose "review_sweep_report_json: #{json_path}"
|
|
207
207
|
rescue StandardError => e
|
|
208
|
-
|
|
208
|
+
puts_verbose "review_sweep_report_write: SKIP (#{e.message})"
|
|
209
209
|
end
|
|
210
210
|
|
|
211
211
|
# Human-readable scheduled sweep report.
|
|
@@ -16,7 +16,11 @@ module Carson
|
|
|
16
16
|
def review_gate!
|
|
17
17
|
fingerprint_status = block_if_outsider_fingerprints!
|
|
18
18
|
return fingerprint_status unless fingerprint_status.nil?
|
|
19
|
-
|
|
19
|
+
puts_verbose ""
|
|
20
|
+
puts_verbose "[Review Gate]"
|
|
21
|
+
unless verbose?
|
|
22
|
+
puts_line "Review Gate"
|
|
23
|
+
end
|
|
20
24
|
unless gh_available?
|
|
21
25
|
puts_line "ERROR: gh CLI not available in PATH."
|
|
22
26
|
return EXIT_ERROR
|
|
@@ -67,13 +71,13 @@ module Carson
|
|
|
67
71
|
snapshot = review_gate_snapshot( owner: owner, repo: repo, pr_number: pr_summary.fetch( :number ) )
|
|
68
72
|
last_snapshot = snapshot
|
|
69
73
|
signature = review_gate_signature( snapshot: snapshot )
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
puts_verbose "poll_attempt: #{poll_attempts}/#{config.review_max_polls}"
|
|
75
|
+
puts_verbose "latest_activity: #{snapshot.fetch( :latest_activity ) || 'unknown'}"
|
|
76
|
+
puts_verbose "unresolved_threads: #{snapshot.fetch( :unresolved_threads ).count}"
|
|
77
|
+
puts_verbose "unacknowledged_actionable: #{snapshot.fetch( :unacknowledged_actionable ).count}"
|
|
74
78
|
if !last_signature.nil? && signature == last_signature
|
|
75
79
|
converged = true
|
|
76
|
-
|
|
80
|
+
puts_verbose "convergence: stable"
|
|
77
81
|
break
|
|
78
82
|
end
|
|
79
83
|
last_signature = signature
|
|
@@ -110,6 +114,9 @@ module Carson
|
|
|
110
114
|
unacknowledged_actionable: last_snapshot.fetch( :unacknowledged_actionable )
|
|
111
115
|
}
|
|
112
116
|
write_review_gate_report( report: report )
|
|
117
|
+
unless verbose?
|
|
118
|
+
puts_line "Polling... (converged after #{poll_attempts} attempt#{plural_suffix( count: poll_attempts )})"
|
|
119
|
+
end
|
|
113
120
|
if block_reasons.empty?
|
|
114
121
|
puts_line "OK: review gate passed."
|
|
115
122
|
return EXIT_OK
|
|
@@ -128,7 +135,8 @@ module Carson
|
|
|
128
135
|
def review_sweep!
|
|
129
136
|
fingerprint_status = block_if_outsider_fingerprints!
|
|
130
137
|
return fingerprint_status unless fingerprint_status.nil?
|
|
131
|
-
|
|
138
|
+
puts_verbose ""
|
|
139
|
+
puts_verbose "[Review Sweep]"
|
|
132
140
|
unless gh_available?
|
|
133
141
|
puts_line "ERROR: gh CLI not available in PATH."
|
|
134
142
|
return EXIT_ERROR
|
|
@@ -137,8 +145,8 @@ module Carson
|
|
|
137
145
|
owner, repo = repository_coordinates
|
|
138
146
|
cutoff_time = Time.now.utc - ( config.review_sweep_window_days * 86_400 )
|
|
139
147
|
pull_requests = recent_pull_requests_for_sweep( owner: owner, repo: repo, cutoff_time: cutoff_time )
|
|
140
|
-
|
|
141
|
-
|
|
148
|
+
puts_verbose "window_days: #{config.review_sweep_window_days}"
|
|
149
|
+
puts_verbose "candidate_prs: #{pull_requests.count}"
|
|
142
150
|
findings = []
|
|
143
151
|
|
|
144
152
|
pull_requests.each do |entry|
|
data/lib/carson/runtime/setup.rb
CHANGED
|
@@ -4,7 +4,8 @@ module Carson
|
|
|
4
4
|
WELL_KNOWN_REMOTES = %w[origin github upstream].freeze
|
|
5
5
|
|
|
6
6
|
def setup!
|
|
7
|
-
|
|
7
|
+
puts_verbose ""
|
|
8
|
+
puts_verbose "[Setup]"
|
|
8
9
|
|
|
9
10
|
unless inside_git_work_tree?
|
|
10
11
|
puts_line "WARN: not a git repository. Skipping remote and branch detection."
|
|
@@ -43,19 +44,19 @@ module Carson
|
|
|
43
44
|
choices = {}
|
|
44
45
|
if detected && detected != config.git_remote
|
|
45
46
|
choices[ "git.remote" ] = detected
|
|
46
|
-
|
|
47
|
+
puts_verbose "detected_remote: #{detected}"
|
|
47
48
|
elsif detected
|
|
48
|
-
|
|
49
|
+
puts_verbose "detected_remote: #{detected}"
|
|
49
50
|
else
|
|
50
|
-
|
|
51
|
+
puts_verbose "detected_remote: none"
|
|
51
52
|
end
|
|
52
53
|
|
|
53
54
|
branch = detect_main_branch
|
|
54
55
|
if branch && branch != config.main_branch
|
|
55
56
|
choices[ "git.main_branch" ] = branch
|
|
56
|
-
|
|
57
|
+
puts_verbose "detected_main_branch: #{branch}"
|
|
57
58
|
elsif branch
|
|
58
|
-
|
|
59
|
+
puts_verbose "detected_main_branch: #{branch}"
|
|
59
60
|
end
|
|
60
61
|
|
|
61
62
|
write_setup_config( choices: choices )
|
|
@@ -103,8 +104,8 @@ module Carson
|
|
|
103
104
|
puts_line ""
|
|
104
105
|
puts_line "Workflow style"
|
|
105
106
|
options = [
|
|
106
|
-
{ label: "
|
|
107
|
-
{ label: "
|
|
107
|
+
{ label: "branch — enforce PR-only merges (default)", value: "branch" },
|
|
108
|
+
{ label: "trunk — commit directly to main", value: "trunk" }
|
|
108
109
|
]
|
|
109
110
|
prompt_choice( options: options, default: 0 )
|
|
110
111
|
end
|
data/lib/carson/runtime.rb
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
require "fileutils"
|
|
5
5
|
require "json"
|
|
6
6
|
require "open3"
|
|
7
|
+
require "stringio"
|
|
7
8
|
require "time"
|
|
8
9
|
|
|
9
10
|
module Carson
|
|
@@ -22,12 +23,13 @@ module Carson
|
|
|
22
23
|
DISPOSITION_TOKENS = %w[accepted rejected deferred].freeze
|
|
23
24
|
|
|
24
25
|
# Runtime wiring for repository context, tool paths, and output streams.
|
|
25
|
-
def initialize( repo_root:, tool_root:, out:, err:, in_stream: $stdin )
|
|
26
|
+
def initialize( repo_root:, tool_root:, out:, err:, in_stream: $stdin, verbose: false )
|
|
26
27
|
@repo_root = repo_root
|
|
27
28
|
@tool_root = tool_root
|
|
28
29
|
@out = out
|
|
29
30
|
@err = err
|
|
30
31
|
@in = in_stream
|
|
32
|
+
@verbose = verbose
|
|
31
33
|
@config = Config.load( repo_root: repo_root )
|
|
32
34
|
@git_adapter = Adapters::Git.new( repo_root: repo_root )
|
|
33
35
|
@github_adapter = Adapters::GitHub.new( repo_root: repo_root )
|
|
@@ -37,6 +39,27 @@ module Carson
|
|
|
37
39
|
|
|
38
40
|
attr_reader :repo_root, :tool_root, :out, :err, :in, :config, :git_adapter, :github_adapter
|
|
39
41
|
|
|
42
|
+
# Returns true when full diagnostic output is enabled via --verbose.
|
|
43
|
+
def verbose?
|
|
44
|
+
@verbose
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Prints a line only when verbose mode is active.
|
|
48
|
+
def puts_verbose( message )
|
|
49
|
+
puts_line( message ) if verbose?
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Runs a block with all output captured (suppressed from the user).
|
|
53
|
+
# Returns the block's return value; output is silently discarded.
|
|
54
|
+
def with_captured_output
|
|
55
|
+
saved_out, saved_err = @out, @err
|
|
56
|
+
@out = StringIO.new
|
|
57
|
+
@err = StringIO.new
|
|
58
|
+
yield
|
|
59
|
+
ensure
|
|
60
|
+
@out, @err = saved_out, saved_err
|
|
61
|
+
end
|
|
62
|
+
|
|
40
63
|
# Current local branch name.
|
|
41
64
|
def current_branch
|
|
42
65
|
git_capture!( "rev-parse", "--abbrev-ref", "HEAD" ).strip
|