carson 2.31.0 → 2.33.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 +26 -0
- data/VERSION +1 -1
- data/lib/carson/cli.rb +3 -2
- data/lib/carson/runtime/local/prune.rb +35 -6
- data/lib/carson/runtime/local/template.rb +3 -1
- data/lib/carson/runtime/local/worktree.rb +15 -4
- 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: 83e70e6824d3de6da2d444aa9d0b2859f3710e357f87cdcf65cecc512ff63fee
|
|
4
|
+
data.tar.gz: 6c09bd64199ab8ee6f7fa14722b670a123c7cb051ea2937c1e2bbfddb696fded
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3a6570b397a73c27aab1d33e044bd28f56d1f643e57e76b40d3bbd5cdd1ff5da2779467980c9f595c6a25cbe2524cf5ff6508050a2736677ebb94079434eb749
|
|
7
|
+
data.tar.gz: 33d29172314ab043a5a38122b91fac3c150b3d846ef113cff953fefec5f56f8c35af1290a48ef951fe1154de4caf749cd779daf98c09c939518c0e64f7a72ecd
|
data/RELEASE.md
CHANGED
|
@@ -5,6 +5,32 @@ 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.33.0 — Safe Worktree Remove
|
|
9
|
+
|
|
10
|
+
### What changed
|
|
11
|
+
|
|
12
|
+
- **`carson worktree remove` no longer force-removes by default.** If the worktree has uncommitted or untracked changes, Carson refuses with a clear message instead of silently discarding work. Pass `--force` to override when you intentionally want to discard changes.
|
|
13
|
+
- **Template sync cleanup is safer.** Carson's internal template propagation tries safe worktree removal first, falling back to `--force` only for its own ephemeral sync worktree.
|
|
14
|
+
|
|
15
|
+
### UX
|
|
16
|
+
|
|
17
|
+
- Dirty worktree removal now shows the worktree name and actionable guidance: "Commit or discard changes first, or use --force to override."
|
|
18
|
+
|
|
19
|
+
### Migration
|
|
20
|
+
|
|
21
|
+
- No action required. Existing `carson worktree remove` calls without `--force` will now refuse on dirty worktrees instead of silently destroying uncommitted work.
|
|
22
|
+
|
|
23
|
+
## 2.32.0 — Worktree-Aware Pruning
|
|
24
|
+
|
|
25
|
+
### What changed
|
|
26
|
+
|
|
27
|
+
- **Prune now clears worktrees that block branch deletion.** When a branch is checked out in a worktree, `carson prune` safely removes the worktree first (refuses if uncommitted changes exist), then deletes the branch. Previously, these branches were silently skipped.
|
|
28
|
+
- **Honest skip reporting.** Non-verbose output no longer says "No stale branches" when branches were detected but couldn't be pruned. Shows "Skipped N branch(es)" or "Pruned N, skipped M" as appropriate, with a `--verbose` hint.
|
|
29
|
+
|
|
30
|
+
### Migration
|
|
31
|
+
|
|
32
|
+
- No action required. Existing prune behaviour is strictly improved.
|
|
33
|
+
|
|
8
34
|
## 2.31.0 — Worktree Lifecycle + Prune --all
|
|
9
35
|
|
|
10
36
|
### What changed
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.
|
|
1
|
+
2.33.0
|
data/lib/carson/cli.rb
CHANGED
|
@@ -175,12 +175,13 @@ module Carson
|
|
|
175
175
|
|
|
176
176
|
case action
|
|
177
177
|
when "remove"
|
|
178
|
+
force = argv.delete( "--force" ) ? true : false
|
|
178
179
|
worktree_path = argv.shift
|
|
179
180
|
if worktree_path.to_s.strip.empty?
|
|
180
181
|
err.puts "#{BADGE} Missing path for worktree remove. Use: carson worktree remove <name-or-path>"
|
|
181
182
|
return { command: :invalid }
|
|
182
183
|
end
|
|
183
|
-
{ command: "worktree:remove", worktree_path: worktree_path }
|
|
184
|
+
{ command: "worktree:remove", worktree_path: worktree_path, force: force }
|
|
184
185
|
else
|
|
185
186
|
err.puts "#{BADGE} Unknown worktree subcommand: #{action}. Use: carson worktree remove <name-or-path>"
|
|
186
187
|
{ command: :invalid }
|
|
@@ -276,7 +277,7 @@ module Carson
|
|
|
276
277
|
when "prune:all"
|
|
277
278
|
runtime.prune_all!
|
|
278
279
|
when "worktree:remove"
|
|
279
|
-
runtime.worktree_remove!( worktree_path: parsed.fetch( :worktree_path ) )
|
|
280
|
+
runtime.worktree_remove!( worktree_path: parsed.fetch( :worktree_path ), force: parsed.fetch( :force, false ) )
|
|
280
281
|
when "onboard"
|
|
281
282
|
runtime.onboard!
|
|
282
283
|
when "refresh"
|
|
@@ -25,11 +25,15 @@ module Carson
|
|
|
25
25
|
puts_verbose "prune_summary: deleted=#{counters.fetch( :deleted )} skipped=#{counters.fetch( :skipped )}"
|
|
26
26
|
unless verbose?
|
|
27
27
|
deleted_count = counters.fetch( :deleted )
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
skipped_count = counters.fetch( :skipped )
|
|
29
|
+
message = if deleted_count > 0 && skipped_count > 0
|
|
30
|
+
"Pruned #{deleted_count}, skipped #{skipped_count} (--verbose for details)."
|
|
31
|
+
elsif deleted_count > 0
|
|
32
|
+
"Pruned #{deleted_count} stale branch#{plural_suffix( count: deleted_count )}."
|
|
30
33
|
else
|
|
31
|
-
|
|
34
|
+
"Skipped #{skipped_count} branch#{plural_suffix( count: skipped_count )} (--verbose for details)."
|
|
32
35
|
end
|
|
36
|
+
puts_line message
|
|
33
37
|
end
|
|
34
38
|
EXIT_OK
|
|
35
39
|
end
|
|
@@ -93,7 +97,7 @@ module Carson
|
|
|
93
97
|
)
|
|
94
98
|
return prune_force_delete_skipped( branch: branch, upstream: upstream, delete_error_text: delete_error_text, force_error: force_error ) if merged_pr.nil?
|
|
95
99
|
|
|
96
|
-
force_stdout, force_stderr, force_success
|
|
100
|
+
force_stdout, force_stderr, force_success = force_delete_local_branch( branch: branch )
|
|
97
101
|
return prune_force_delete_success( branch: branch, upstream: upstream, merged_pr: merged_pr, force_stdout: force_stdout ) if force_success
|
|
98
102
|
|
|
99
103
|
prune_force_delete_failed( branch: branch, upstream: upstream, force_stderr: force_stderr )
|
|
@@ -122,6 +126,31 @@ module Carson
|
|
|
122
126
|
text.empty? ? "unknown error" : text
|
|
123
127
|
end
|
|
124
128
|
|
|
129
|
+
# Attempts git branch -D. If blocked by a worktree, skips with a diagnostic —
|
|
130
|
+
# prune never removes worktrees because another session may own them.
|
|
131
|
+
def force_delete_local_branch( branch: )
|
|
132
|
+
stdout, stderr, success, = git_run( "branch", "-D", branch )
|
|
133
|
+
return [ stdout, stderr, success ] if success
|
|
134
|
+
|
|
135
|
+
if worktree_blocked_error?( error_text: stderr )
|
|
136
|
+
wt_path = worktree_path_for_branch( branch: branch )
|
|
137
|
+
hint = wt_path ? "run: carson worktree remove #{File.basename( wt_path )}" : "remove the worktree first"
|
|
138
|
+
puts_verbose "skip_worktree_blocked: #{branch} (#{hint})"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
[ stdout, stderr, false ]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def worktree_blocked_error?( error_text: )
|
|
145
|
+
error_text.to_s.downcase.include?( "used by worktree" )
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Returns the worktree path for a branch, or nil if not checked out in any worktree.
|
|
149
|
+
def worktree_path_for_branch( branch: )
|
|
150
|
+
entry = worktree_list.find { |wt| wt.fetch( :branch, nil ) == branch }
|
|
151
|
+
entry&.fetch( :path, nil )
|
|
152
|
+
end
|
|
153
|
+
|
|
125
154
|
# Detects local branches whose upstream tracking is marked [gone] after fetch --prune.
|
|
126
155
|
def stale_local_branches
|
|
127
156
|
git_capture!( "for-each-ref", "--format=%(refname:short)\t%(upstream:short)\t%(upstream:track)", "refs/heads" ).lines.map do |line|
|
|
@@ -218,7 +247,7 @@ module Carson
|
|
|
218
247
|
return :skipped
|
|
219
248
|
end
|
|
220
249
|
|
|
221
|
-
force_stdout, force_stderr, force_success
|
|
250
|
+
force_stdout, force_stderr, force_success = force_delete_local_branch( branch: branch )
|
|
222
251
|
unless force_success
|
|
223
252
|
error_text = normalise_branch_delete_error( error_text: force_stderr )
|
|
224
253
|
puts_verbose "fail_delete_absorbed_branch: #{branch} reason=#{error_text}"
|
|
@@ -287,7 +316,7 @@ module Carson
|
|
|
287
316
|
return :skipped
|
|
288
317
|
end
|
|
289
318
|
|
|
290
|
-
force_stdout, force_stderr, force_success
|
|
319
|
+
force_stdout, force_stderr, force_success = force_delete_local_branch( branch: branch )
|
|
291
320
|
if force_success
|
|
292
321
|
out.print force_stdout if verbose? && !force_stdout.empty?
|
|
293
322
|
puts_verbose "deleted_orphan_branch: #{branch} merged_pr=#{merged_pr.fetch( :url )}"
|
|
@@ -244,7 +244,9 @@ module Carson
|
|
|
244
244
|
end
|
|
245
245
|
|
|
246
246
|
def template_propagate_cleanup!( worktree_dir: )
|
|
247
|
-
|
|
247
|
+
# Try safe removal first; fall back to force only for Carson's own sync worktree.
|
|
248
|
+
_, _, safe_success, = git_run( "worktree", "remove", worktree_dir )
|
|
249
|
+
git_run( "worktree", "remove", "--force", worktree_dir ) unless safe_success
|
|
248
250
|
git_run( "branch", "-D", TEMPLATE_SYNC_BRANCH )
|
|
249
251
|
puts_verbose "template_propagate: worktree and local branch cleaned up"
|
|
250
252
|
rescue StandardError => e
|
|
@@ -3,7 +3,9 @@ module Carson
|
|
|
3
3
|
module Local
|
|
4
4
|
# Safe worktree lifecycle management for coding agents.
|
|
5
5
|
# Enforces the teardown order: exit worktree → git worktree remove → branch cleanup.
|
|
6
|
-
|
|
6
|
+
# Never forces removal — if the worktree has uncommitted changes, refuses unless
|
|
7
|
+
# the user explicitly passes force: true via CLI --force flag.
|
|
8
|
+
def worktree_remove!( worktree_path:, force: false )
|
|
7
9
|
fingerprint_status = block_if_outsider_fingerprints!
|
|
8
10
|
return fingerprint_status unless fingerprint_status.nil?
|
|
9
11
|
|
|
@@ -17,14 +19,23 @@ module Carson
|
|
|
17
19
|
end
|
|
18
20
|
|
|
19
21
|
branch = worktree_branch( path: resolved_path )
|
|
20
|
-
puts_verbose "worktree_remove: path=#{resolved_path} branch=#{branch}"
|
|
22
|
+
puts_verbose "worktree_remove: path=#{resolved_path} branch=#{branch} force=#{force}"
|
|
21
23
|
|
|
22
24
|
# Step 1: remove the worktree (directory + git registration).
|
|
23
|
-
|
|
25
|
+
# Try safe removal first. Only use --force if the user explicitly requested it.
|
|
26
|
+
rm_args = [ "worktree", "remove" ]
|
|
27
|
+
rm_args << "--force" if force
|
|
28
|
+
rm_args << resolved_path
|
|
29
|
+
rm_stdout, rm_stderr, rm_success, = git_run( *rm_args )
|
|
24
30
|
unless rm_success
|
|
25
31
|
error_text = rm_stderr.to_s.strip
|
|
26
32
|
error_text = "unable to remove worktree" if error_text.empty?
|
|
27
|
-
|
|
33
|
+
if !force && ( error_text.downcase.include?( "untracked" ) || error_text.downcase.include?( "modified" ) )
|
|
34
|
+
puts_line "Worktree has uncommitted changes: #{File.basename( resolved_path )}"
|
|
35
|
+
puts_line " Commit or discard changes first, or use --force to override."
|
|
36
|
+
else
|
|
37
|
+
puts_line "ERROR: #{error_text}"
|
|
38
|
+
end
|
|
28
39
|
return EXIT_ERROR
|
|
29
40
|
end
|
|
30
41
|
puts_verbose "worktree_removed: #{resolved_path}"
|