carson 2.9.0 → 2.11.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 45443353a34297748fec8177c152d898dabb8cf7f1641d7870b7aff59d9a2f6c
4
- data.tar.gz: 627bdc81dcf78ad41998840796a3427512c39b6b2e80e2b961895bdf4423a456
3
+ metadata.gz: ef5d5bfa64c8a8b7f4d2b4893dd5a3cab05ab2f154bd12ab926837ae396c2fe2
4
+ data.tar.gz: 106a0dc1a54c05e59ba9fe4eb0d01f647c420ac305d92a5d70cbc3c606300006
5
5
  SHA512:
6
- metadata.gz: 96a9e858b5d934d5e5570443e25b79e28255cfe54279623be8d44474e55d592a7c9cd3161da8611d84fd24141cd10a98b6922fb364609630d843f651858bf78c
7
- data.tar.gz: ee85efae91e54e07d872c560ad7305553b3d45246f9ec502b6fef5b7d1e6e843c8a03787629adb55190541cf5101be76a3f3ee625ec31540876b718905013ba4
6
+ metadata.gz: 1c1d53528fa822f8d7e4850234a039459f56053762ed8ed1982e7a27c2b1f7fd7820f6565afc2a8cda596971ce4dfab2e2288aeabf8b92e54e2cc5e9266ab213
7
+ data.tar.gz: 04755a262052a630f8200089d03ff1f5a7c48ab8e68f50097fcdd206a8672da988d51c5f041384752a7d97712854b054aa4fd85b2b7268989190ea248d33bf1f
data/API.md CHANGED
@@ -95,6 +95,7 @@ Environment overrides:
95
95
  - `CARSON_REVIEW_DISPOSITION_PREFIX`
96
96
  - `CARSON_REVIEW_SWEEP_WINDOW_DAYS`
97
97
  - `CARSON_REVIEW_SWEEP_STATES`
98
+ - `CARSON_WORKFLOW_STYLE`
98
99
  - `CARSON_RUBY_INDENTATION`
99
100
 
100
101
  `lint.languages` schema:
data/MANUAL.md CHANGED
@@ -212,6 +212,7 @@ Common environment overrides:
212
212
  | `CARSON_REVIEW_DISPOSITION_PREFIX` | Required prefix for disposition comments. |
213
213
  | `CARSON_REVIEW_SWEEP_WINDOW_DAYS` | Lookback window for review sweep. |
214
214
  | `CARSON_REVIEW_SWEEP_STATES` | PR states to include in sweep. |
215
+ | `CARSON_WORKFLOW_STYLE` | Workflow style override (`branch` or `trunk`). |
215
216
  | `CARSON_RUBY_INDENTATION` | Ruby indentation policy (`tabs`, `spaces`, or `either`). |
216
217
 
217
218
  For the full configuration schema and `lint.languages` definition, see `API.md`.
data/RELEASE.md CHANGED
@@ -5,6 +5,31 @@ 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.11.0 — Self-Diagnosing Audit and Duplicate-Remote Prevention
9
+
10
+ ### What changed
11
+
12
+ - **Audit concise output now names the remote and suggests recovery actions.** "Main sync (origin): ahead by 1 — git fetch origin, or carson setup to switch remote." instead of the opaque "Main sync: ahead by 1 — reset local drift." The remote name is visible; the fix is embedded.
13
+ - **Setup warns when multiple remotes share the same URL.** Interactive mode annotates duplicates with `[duplicate]` and prints a warning. Silent mode logs a `duplicate_remotes:` verbose line. URL normalisation treats SSH and HTTPS variants as equal (`git@github.com:user/repo.git` matches `https://github.com/user/repo`).
14
+
15
+ ### What users must do now
16
+
17
+ 1. Upgrade Carson to `2.11.0`.
18
+ 2. If you have duplicate remotes (e.g. both `origin` and `github` pointing to the same URL), remove the stale one with `git remote remove <name>`.
19
+
20
+ ## 2.10.0 — Lower Ruby Requirement to 3.4
21
+
22
+ ### What changed
23
+
24
+ - **Minimum Ruby version lowered from 4.0 to 3.4.** Carson uses no Ruby 4.0-specific features. Lowering to 3.4 widens compatibility to the current stable Ruby series while enabling the `it` implicit block parameter.
25
+ - **Removed `# frozen_string_literal: true` pragma** from the one file that had it (`lib/carson/policy/ruby/lint.rb`). Ruby 4.0 freezes strings by default; the pragma is unnecessary.
26
+ - **Default workflow style now actually `branch` in code.** The 2.9.0 release notes documented this change, but the hooks and config default were not updated. Now fixed: hooks fall back to `branch`, config default is `branch`, and `CARSON_WORKFLOW_STYLE` env override is documented.
27
+
28
+ ### What users must do now
29
+
30
+ 1. Upgrade Carson to `2.10.0`.
31
+ 2. Ruby 3.4 or later is now sufficient — Ruby 4.0 is no longer required.
32
+
8
33
  ## 2.9.0 — Concise UX for All Commands
9
34
 
10
35
  ### What changed
@@ -14,16 +39,19 @@ Release-note scope rule:
14
39
  - **Audit concise output.** A healthy audit prints one line (`Audit: ok`). Problems print only actionable summaries (e.g. `Hooks: mismatch — run carson prepare.`).
15
40
  - **Refresh concise output.** Prints ~5 lines: hooks installed, templates in sync, audit result, done.
16
41
  - **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.
42
+ - **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.
17
43
 
18
44
  ### What users must do now
19
45
 
20
46
  1. Upgrade Carson to `2.9.0`.
21
47
  2. Use `--verbose` when you need full diagnostics (debugging, CI troubleshooting).
48
+ 3. If you rely on direct commits to main, re-run `carson setup` and choose `trunk`, or set `CARSON_WORKFLOW_STYLE=trunk` in your environment.
22
49
 
23
50
  ### Breaking or removed behaviour
24
51
 
25
52
  - Default output is now concise. Scripts that parse Carson's key-value diagnostic lines must add `--verbose`.
26
53
  - Removed `@concise` internal flag (replaced by `--verbose` opt-in pattern).
54
+ - 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`.
27
55
 
28
56
  ### Upgrade steps
29
57
 
@@ -41,16 +69,14 @@ carson version
41
69
  - **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
70
  - **Graceful handling of fresh repos.** Onboard no longer fails with a fatal error on repositories with no commits yet.
43
71
  - **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
72
 
46
73
  ### What users must do now
47
74
 
48
75
  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
76
 
51
77
  ### Breaking or removed behaviour
52
78
 
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`.
79
+ - None.
54
80
 
55
81
  ### Upgrade steps
56
82
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.9.0
1
+ 2.11.0
data/carson.gemspec CHANGED
@@ -11,7 +11,7 @@ Gem::Specification.new do |spec|
11
11
  spec.description = "Carson runs outside host repositories and applies governance checks, review gates, and managed GitHub-native files."
12
12
  spec.homepage = "https://github.com/wanghailei/carson"
13
13
  spec.license = "MIT"
14
- spec.required_ruby_version = ">= 4.0"
14
+ spec.required_ruby_version = ">= 3.4"
15
15
  spec.metadata = {
16
16
  "source_code_uri" => "https://github.com/wanghailei/carson",
17
17
  "changelog_uri" => "https://github.com/wanghailei/carson/blob/main/RELEASE.md",
@@ -2,7 +2,7 @@
2
2
  set -euo pipefail
3
3
 
4
4
  hooks_dir="$(cd "$(dirname "$0")" && pwd)"
5
- style="$(cat "$hooks_dir/workflow_style" 2>/dev/null || echo "trunk")"
5
+ style="$(cat "$hooks_dir/workflow_style" 2>/dev/null || echo "branch")"
6
6
  [ "$style" = "trunk" ] && exit 0
7
7
 
8
8
  branch_name="$(git rev-parse --abbrev-ref HEAD)"
data/hooks/pre-push CHANGED
@@ -2,7 +2,7 @@
2
2
  set -euo pipefail
3
3
 
4
4
  hooks_dir="$(cd "$(dirname "$0")" && pwd)"
5
- style="$(cat "$hooks_dir/workflow_style" 2>/dev/null || echo "trunk")"
5
+ style="$(cat "$hooks_dir/workflow_style" 2>/dev/null || echo "branch")"
6
6
  [ "$style" = "trunk" ] && exit 0
7
7
 
8
8
  remote_name="${1:-unknown}"
@@ -2,7 +2,7 @@
2
2
  set -euo pipefail
3
3
 
4
4
  hooks_dir="$(cd "$(dirname "$0")" && pwd)"
5
- style="$(cat "$hooks_dir/workflow_style" 2>/dev/null || echo "trunk")"
5
+ style="$(cat "$hooks_dir/workflow_style" 2>/dev/null || echo "branch")"
6
6
  [ "$style" = "trunk" ] && exit 0
7
7
 
8
8
  branch_name="$(git rev-parse --abbrev-ref HEAD)"
data/lib/carson/config.rb CHANGED
@@ -53,7 +53,7 @@ module Carson
53
53
  "languages" => default_lint_languages_data
54
54
  },
55
55
  "workflow" => {
56
- "style" => "trunk"
56
+ "style" => "branch"
57
57
  },
58
58
  "review" => {
59
59
  "bot_usernames" => [],
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
2
 
4
3
  require "open3"
5
4
 
@@ -46,13 +46,13 @@ module Carson
46
46
  puts_verbose "main_vs_remote_main_behind: #{behind_count}"
47
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."
48
48
  audit_state = "block"
49
- audit_concise_problems << "Main sync: ahead by #{ahead_count} — reset local drift."
49
+ audit_concise_problems << "Main sync (#{config.git_remote}): ahead by #{ahead_count} — git fetch #{config.git_remote}, or carson setup to switch remote."
50
50
  elsif behind_count.positive?
51
51
  puts_verbose "main_vs_remote_main_ahead: #{ahead_count}"
52
52
  puts_verbose "main_vs_remote_main_behind: #{behind_count}"
53
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."
54
54
  audit_state = "attention" if audit_state == "ok"
55
- audit_concise_problems << "Main sync: behind by #{behind_count} — run carson sync."
55
+ audit_concise_problems << "Main sync (#{config.git_remote}): behind by #{behind_count} — run carson sync."
56
56
  else
57
57
  puts_verbose "main_vs_remote_main_ahead: 0"
58
58
  puts_verbose "main_vs_remote_main_behind: 0"
@@ -1,3 +1,6 @@
1
+ require "set"
2
+ require "uri"
3
+
1
4
  module Carson
2
5
  class Runtime
3
6
  module Setup
@@ -51,6 +54,15 @@ module Carson
51
54
  puts_verbose "detected_remote: none"
52
55
  end
53
56
 
57
+ remotes = list_git_remotes
58
+ duplicates = duplicate_remote_groups( remotes: remotes )
59
+ unless duplicates.empty?
60
+ duplicates.each_value do |group|
61
+ names = group.map { it.fetch( :name ) }.join( " and " )
62
+ puts_verbose "duplicate_remotes: #{names} share the same URL"
63
+ end
64
+ end
65
+
54
66
  branch = detect_main_branch
55
67
  if branch && branch != config.main_branch
56
68
  choices[ "git.main_branch" ] = branch
@@ -69,9 +81,18 @@ module Carson
69
81
  return nil
70
82
  end
71
83
 
84
+ duplicates = duplicate_remote_groups( remotes: remotes )
85
+ duplicate_names = duplicates.values.flatten.map { it.fetch( :name ) }.to_set
86
+ unless duplicates.empty?
87
+ duplicates.each_value do |group|
88
+ names = group.map { it.fetch( :name ) }.join( " and " )
89
+ puts_line "Remotes #{names} share the same URL. Consider removing the duplicate."
90
+ end
91
+ end
92
+
72
93
  puts_line ""
73
94
  puts_line "Git remote"
74
- options = build_remote_options( remotes: remotes )
95
+ options = build_remote_options( remotes: remotes, duplicate_names: duplicate_names )
75
96
  options << { label: "Other (enter name)", value: :other }
76
97
 
77
98
  default_index = 0
@@ -151,12 +172,13 @@ module Carson
151
172
  value.empty? ? nil : value
152
173
  end
153
174
 
154
- def build_remote_options( remotes: )
175
+ def build_remote_options( remotes:, duplicate_names: Set.new )
155
176
  sorted = sort_remotes( remotes: remotes )
156
177
  sorted.map do |entry|
157
178
  name = entry.fetch( :name )
158
179
  url = entry.fetch( :url )
159
- { label: "#{name} (#{url})", value: name }
180
+ tag = duplicate_names.include?( name ) ? " [duplicate]" : ""
181
+ { label: "#{name} (#{url})#{tag}", value: name }
160
182
  end
161
183
  end
162
184
 
@@ -173,6 +195,35 @@ module Carson
173
195
  well_known.sort_by { |e| WELL_KNOWN_REMOTES.index( e.fetch( :name ) ) || 999 } + others.sort_by { |e| e.fetch( :name ) }
174
196
  end
175
197
 
198
+ # Normalises a remote URL so SSH and HTTPS variants of the same host/path compare equal.
199
+ # Strips trailing .git, lowercases, converts git@host:path to https://host/path.
200
+ def normalise_remote_url( url: )
201
+ text = url.to_s.strip
202
+ return "" if text.empty?
203
+
204
+ # Convert SSH shorthand (git@host:owner/repo) to HTTPS form.
205
+ if text.match?( /\A[\w.-]+@[\w.-]+:/ )
206
+ text = text.sub( /\A[\w.-]+@([\w.-]+):/, 'https://\1/' )
207
+ end
208
+
209
+ text = text.delete_suffix( ".git" )
210
+ text = text.chomp( "/" )
211
+ text.downcase
212
+ end
213
+
214
+ # Groups remotes that share the same normalised URL. Returns a hash of
215
+ # normalised_url => [remote entries] for groups with more than one member.
216
+ def duplicate_remote_groups( remotes: )
217
+ by_url = {}
218
+ remotes.each do |entry|
219
+ key = normalise_remote_url( url: entry.fetch( :url ) )
220
+ next if key.empty?
221
+
222
+ ( by_url[ key ] ||= [] ) << entry
223
+ end
224
+ by_url.select { |_url, entries| entries.length > 1 }
225
+ end
226
+
176
227
  def build_main_branch_options
177
228
  options = []
178
229
  main_exists = branch_exists_locally_or_remote?( branch: "main" )
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: carson
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.9.0
4
+ version: 2.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hailei Wang
@@ -88,7 +88,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
88
88
  requirements:
89
89
  - - ">="
90
90
  - !ruby/object:Gem::Version
91
- version: '4.0'
91
+ version: '3.4'
92
92
  required_rubygems_version: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - ">="