carson 3.10.4 → 3.10.5

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: 15dca7c329b421bc534d0ea857d38af70bfe89bad5eb2ec40be9bbe09015e37f
4
- data.tar.gz: a2a997bf5f9be278a33034155fffa61caa1c28e78c24fa3d8af85e43f72241c0
3
+ metadata.gz: 29198792c7eb5bb9c02c43fc31d6a2ba04fc1430684e978cc365c2e74b47f810
4
+ data.tar.gz: b0e164e172315cc707017a9c82e83328fdb1e84a5e4f983aa5aeabc9caae1b04
5
5
  SHA512:
6
- metadata.gz: 8e7f231146222978f1f880a08e16ce99bb75ae3c69899f20d23413bd88b712edc0cc8a21b3b1515437496ef625e079318a62f435dd4634490cafa3ac034c09b9
7
- data.tar.gz: 2e3ec460ec390a021b48e132597899e5c6d7df5f3d1e667aa29572f0b45de673a076410bf81a6b768b6dd06fb60adec5672278e4cbbd695a5d598688afaecbb2
6
+ metadata.gz: 45361da34684eaf4d399f7d40eba060a63f3c364f41ed890ac448f6ffeeb4d886961f451ce393adec495240ffc5bff5bc95399b59d7afd08aaba0eadf0579fde
7
+ data.tar.gz: b827c09fcf9cef558f237471abd328906ecda337f2bdef04a7991ee2547b2a31437840eb651d04e1ca659c1d55b891e84890cf57b857247240fd3a5ecd81f71c
data/RELEASE.md CHANGED
@@ -5,17 +5,16 @@ 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
- ## 3.10.4
8
+ ## 3.10.5
9
9
 
10
10
  ### What changed
11
11
 
12
- - **Worktree remove guards unpushed commits** — `carson worktree remove` now checks for unpushed commits before deleting a worktree. Blocks with recovery guidance (push command or `--force` to override). Prevents accidental destruction of work that exists only locally.
13
- - **Shared unpushed-commits check** — extracted `check_unpushed_commits` method used by both `worktree done` and `worktree remove`, eliminating code duplication.
14
- - **Fix resolve path from inside worktrees** — `resolve_worktree_path` now uses `main_worktree_root` instead of `repo_root` for bare-name resolution. Previously, calling `carson worktree remove <name>` from inside a worktree would look in the wrong directory.
12
+ - **CWD guard for prune** — `carson prune` now proactively detects when the process CWD is inside a worktree and skips that worktree's branch in all prune paths (stale, orphan, absorbed). Previously, prune relied on git's own refusal to delete a branch checked out in a worktree — accidental protection, not principled safety. The new guard matches the same CWD-awareness that `worktree remove` already has.
13
+ - **`cwd_worktree_branch` helper** — new method that finds the branch checked out in the worktree containing the process CWD. Uses longest-path matching because worktree directories live inside the main repo tree (`.claude/worktrees/`).
15
14
 
16
15
  ### Migration
17
16
 
18
- - No breaking changes. `--force` overrides the new unpushed-commits guard.
17
+ - No breaking changes. New safety guard previously git-protected operations now fail earlier with clearer diagnostics.
19
18
 
20
19
  ## 3.10.3
21
20
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 3.10.4
1
+ 3.10.5
@@ -20,16 +20,17 @@ module Carson
20
20
 
21
21
  prune_git!( "fetch", config.git_remote, "--prune", json_output: json_output )
22
22
  active_branch = current_branch
23
+ cwd_branch = cwd_worktree_branch
23
24
  counters = { deleted: 0, skipped: 0 }
24
25
  branches = []
25
26
 
26
27
  stale_branches = stale_local_branches
27
- prune_stale_branch_entries( stale_branches: stale_branches, active_branch: active_branch, counters: counters, branches: branches )
28
+ prune_stale_branch_entries( stale_branches: stale_branches, active_branch: active_branch, cwd_branch: cwd_branch, counters: counters, branches: branches )
28
29
 
29
- orphan_branches = orphan_local_branches( active_branch: active_branch )
30
+ orphan_branches = orphan_local_branches( active_branch: active_branch, cwd_branch: cwd_branch )
30
31
  prune_orphan_branch_entries( orphan_branches: orphan_branches, counters: counters, branches: branches )
31
32
 
32
- absorbed_branches = absorbed_local_branches( active_branch: active_branch )
33
+ absorbed_branches = absorbed_local_branches( active_branch: active_branch, cwd_branch: cwd_branch )
33
34
  prune_absorbed_branch_entries( absorbed_branches: absorbed_branches, counters: counters, branches: branches )
34
35
 
35
36
  prune_finish(
@@ -90,27 +91,28 @@ module Carson
90
91
  end
91
92
  end
92
93
 
93
- def prune_stale_branch_entries( stale_branches:, active_branch:, counters: { deleted: 0, skipped: 0 }, branches: [] )
94
+ def prune_stale_branch_entries( stale_branches:, active_branch:, cwd_branch: nil, counters: { deleted: 0, skipped: 0 }, branches: [] )
94
95
  stale_branches.each do |entry|
95
- result = prune_stale_branch_entry( entry: entry, active_branch: active_branch )
96
+ result = prune_stale_branch_entry( entry: entry, active_branch: active_branch, cwd_branch: cwd_branch )
96
97
  counters[ result.fetch( :action ) ] += 1
97
98
  branches << result
98
99
  end
99
100
  counters
100
101
  end
101
102
 
102
- def prune_stale_branch_entry( entry:, active_branch: )
103
+ def prune_stale_branch_entry( entry:, active_branch:, cwd_branch: nil )
103
104
  branch = entry.fetch( :branch )
104
105
  upstream = entry.fetch( :upstream )
105
106
  return prune_skip_stale_branch( type: :protected, branch: branch, upstream: upstream ) if config.protected_branches.include?( branch )
106
107
  return prune_skip_stale_branch( type: :current, branch: branch, upstream: upstream ) if branch == active_branch
108
+ return prune_skip_stale_branch( type: :cwd_worktree, branch: branch, upstream: upstream ) if cwd_branch && branch == cwd_branch
107
109
 
108
110
  prune_delete_stale_branch( branch: branch, upstream: upstream )
109
111
  end
110
112
 
111
113
  def prune_skip_stale_branch( type:, branch:, upstream: )
112
- reason = type == :protected ? "protected branch" : "current branch"
113
- status = type == :protected ? "skip_protected_branch" : "skip_current_branch"
114
+ reason = { protected: "protected branch", current: "current branch", cwd_worktree: "checked out in CWD worktree" }.fetch( type, type.to_s )
115
+ status = { protected: "skip_protected_branch", current: "skip_current_branch", cwd_worktree: "skip_cwd_worktree_branch" }.fetch( type, "skip_#{type}" )
114
116
  puts_verbose "#{status}: #{branch} (upstream=#{upstream})"
115
117
  { action: :skipped, branch: branch, upstream: upstream, type: "stale", reason: reason }
116
118
  end
@@ -208,7 +210,7 @@ module Carson
208
210
  end
209
211
 
210
212
  # Detects local branches with no upstream tracking ref — candidates for orphan pruning.
211
- def orphan_local_branches( active_branch: )
213
+ def orphan_local_branches( active_branch:, cwd_branch: nil )
212
214
  git_capture!( "for-each-ref", "--format=%(refname:short)\t%(upstream:short)", "refs/heads" ).lines.filter_map do |line|
213
215
  branch, upstream = line.strip.split( "\t", 2 )
214
216
  branch = branch.to_s.strip
@@ -217,6 +219,7 @@ module Carson
217
219
  next unless upstream.empty?
218
220
  next if config.protected_branches.include?( branch )
219
221
  next if branch == active_branch
222
+ next if cwd_branch && branch == cwd_branch
220
223
  next if branch == TEMPLATE_SYNC_BRANCH
221
224
 
222
225
  branch
@@ -226,7 +229,7 @@ module Carson
226
229
  # Detects local branches whose upstream still exists but whose content is already on main.
227
230
  # Two-step evidence: (1) find the merge-base, (2) verify every file the branch changed
228
231
  # relative to the merge-base has identical content on main.
229
- def absorbed_local_branches( active_branch: )
232
+ def absorbed_local_branches( active_branch:, cwd_branch: nil )
230
233
  git_capture!( "for-each-ref", "--format=%(refname:short)\t%(upstream:short)\t%(upstream:track)", "refs/heads" ).lines.filter_map do |line|
231
234
  branch, upstream, track = line.strip.split( "\t", 3 )
232
235
  branch = branch.to_s.strip
@@ -237,6 +240,7 @@ module Carson
237
240
  next if track.include?( "gone" )
238
241
  next if config.protected_branches.include?( branch )
239
242
  next if branch == active_branch
243
+ next if cwd_branch && branch == cwd_branch
240
244
  next if branch == TEMPLATE_SYNC_BRANCH
241
245
 
242
246
  next unless branch_absorbed_into_main?( branch: branch )
@@ -299,6 +299,28 @@ module Carson
299
299
  nil
300
300
  end
301
301
 
302
+ # Returns the branch checked out in the worktree that contains the process CWD,
303
+ # or nil if CWD is not inside any worktree. Used by prune to proactively
304
+ # protect the CWD worktree's branch from deletion.
305
+ # Matches the longest (most specific) path because worktree directories
306
+ # live under the main repo tree (.claude/worktrees/).
307
+ def cwd_worktree_branch
308
+ cwd = realpath_safe( Dir.pwd )
309
+ best_branch = nil
310
+ best_length = -1
311
+ worktree_list.each do |wt|
312
+ wt_path = wt.fetch( :path )
313
+ normalised = File.join( wt_path, "" )
314
+ if ( cwd == wt_path || cwd.start_with?( normalised ) ) && wt_path.length > best_length
315
+ best_branch = wt.fetch( :branch, nil )
316
+ best_length = wt_path.length
317
+ end
318
+ end
319
+ best_branch
320
+ rescue StandardError
321
+ nil
322
+ end
323
+
302
324
  # Returns the main (non-worktree) repository root.
303
325
  # Uses git-common-dir to find the shared .git directory, then takes its parent.
304
326
  # Falls back to repo_root if detection fails.
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: 3.10.4
4
+ version: 3.10.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hailei Wang