carson 3.0.0 → 3.2.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 +36 -0
- data/VERSION +1 -1
- data/lib/carson/cli.rb +50 -3
- data/lib/carson/runtime/deliver.rb +192 -0
- data/lib/carson/runtime/local/worktree.rb +77 -1
- data/lib/carson/runtime.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 21eb861872b515965379fc3dc8afe24fceb228589b479e475999116aca5eca2a
|
|
4
|
+
data.tar.gz: da1ac628ac9da29969d973a5265c7ddff472370c1865e08af69f8f7bff3203e5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ebafee0b573830243f4706fd5884ab9cece7ec07ecf657b936ea76d48c79af9eec30a9af2ac23ea1d7e7fbad604d49573aa67c2c53e9a500f16225684e81c67c
|
|
7
|
+
data.tar.gz: 0aea5423f60e9d33ca4f42f15b5f7bc4ecfd219960cec6d65d0c868fdc53a44831cb0eb4d12264f489b1fd7baafbf55b7226386b9fb1a7e8e4f360b06f9106f7
|
data/RELEASE.md
CHANGED
|
@@ -5,6 +5,42 @@ 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.2.0 — Deliver
|
|
9
|
+
|
|
10
|
+
### What changed
|
|
11
|
+
|
|
12
|
+
- **`carson deliver`** — pushes the current branch to the remote, creates a PR if none exists, and reports the PR URL. Collapses the manual push/create-PR/report cycle into one command.
|
|
13
|
+
- **`carson deliver --merge`** — does everything above, plus checks CI status and merges the PR if all checks pass. Reports pending or failing CI with actionable guidance. Uses the configured merge method (squash, rebase, or merge). Syncs main after merge.
|
|
14
|
+
- **`carson deliver --title "..." --body-file <path>`** — optional PR title and body file for custom PR metadata. Title defaults to a humanised branch name.
|
|
15
|
+
|
|
16
|
+
### UX
|
|
17
|
+
|
|
18
|
+
- `carson deliver` guards against delivering from main — exits with an error and recovery guidance.
|
|
19
|
+
- `carson deliver --merge` reports CI status clearly: pass, pending, or failing.
|
|
20
|
+
- Default PR title is generated from the branch name (e.g. `feature/add-search` becomes "Feature: add search").
|
|
21
|
+
|
|
22
|
+
### Migration
|
|
23
|
+
|
|
24
|
+
- No breaking changes. All existing commands continue to work unchanged.
|
|
25
|
+
|
|
26
|
+
## 3.1.0 — Worktree Lifecycle
|
|
27
|
+
|
|
28
|
+
### What changed
|
|
29
|
+
|
|
30
|
+
- **`carson worktree create <name>`** — creates a worktree under `.claude/worktrees/<name>` with a new branch based on main. One command, one result: path and branch name reported.
|
|
31
|
+
- **`carson worktree done <name>`** — marks a worktree as completed without deleting it. Verifies all changes are committed and pushed. Blocks with actionable guidance if uncommitted changes or unpushed commits exist. The worktree directory persists for batch cleanup later.
|
|
32
|
+
- **Deferred deletion model.** The full worktree lifecycle is now: create → work → done → batch cleanup. No worktree is deleted during an active session. Cleanup happens later via `carson worktree remove` or `carson housekeep`.
|
|
33
|
+
|
|
34
|
+
### UX
|
|
35
|
+
|
|
36
|
+
- `carson worktree create` reports path and branch — ready to `cd` into immediately.
|
|
37
|
+
- `carson worktree done` gives recovery commands when changes are uncommitted or unpushed.
|
|
38
|
+
- Error messages across worktree subcommands now show `create|done|remove` in usage hints.
|
|
39
|
+
|
|
40
|
+
### Migration
|
|
41
|
+
|
|
42
|
+
- No breaking changes. `carson worktree remove` continues to work as before.
|
|
43
|
+
|
|
8
44
|
## 3.0.0 — Agent-Oriented Carson
|
|
9
45
|
|
|
10
46
|
### Theme
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.2.0
|
data/lib/carson/cli.rb
CHANGED
|
@@ -53,7 +53,7 @@ module Carson
|
|
|
53
53
|
|
|
54
54
|
def self.build_parser
|
|
55
55
|
OptionParser.new do |opts|
|
|
56
|
-
opts.banner = "Usage: carson [status [--json]|setup [--
|
|
56
|
+
opts.banner = "Usage: carson [status [--json]|setup|audit|sync|deliver [--merge] [--title T] [--body-file F]|prune [--all]|worktree create|done|remove <name>|onboard|refresh [--all]|offboard|template check|apply|review gate|sweep|govern [--dry-run] [--json] [--loop SECONDS]|version]"
|
|
57
57
|
end
|
|
58
58
|
end
|
|
59
59
|
|
|
@@ -90,6 +90,8 @@ module Carson
|
|
|
90
90
|
parse_named_subcommand( command: command, usage: "gate|sweep", argv: argv, parser: parser, err: err )
|
|
91
91
|
when "status"
|
|
92
92
|
parse_status_command( argv: argv, err: err )
|
|
93
|
+
when "deliver"
|
|
94
|
+
parse_deliver_command( argv: argv, err: err )
|
|
93
95
|
when "govern"
|
|
94
96
|
parse_govern_subcommand( argv: argv, err: err )
|
|
95
97
|
else
|
|
@@ -170,12 +172,22 @@ module Carson
|
|
|
170
172
|
def self.parse_worktree_subcommand( argv:, parser:, err: )
|
|
171
173
|
action = argv.shift
|
|
172
174
|
if action.to_s.strip.empty?
|
|
173
|
-
err.puts "#{BADGE} Missing subcommand for worktree. Use: carson worktree remove <name
|
|
175
|
+
err.puts "#{BADGE} Missing subcommand for worktree. Use: carson worktree create|done|remove <name>"
|
|
174
176
|
err.puts parser
|
|
175
177
|
return { command: :invalid }
|
|
176
178
|
end
|
|
177
179
|
|
|
178
180
|
case action
|
|
181
|
+
when "create"
|
|
182
|
+
name = argv.shift
|
|
183
|
+
if name.to_s.strip.empty?
|
|
184
|
+
err.puts "#{BADGE} Missing name for worktree create. Use: carson worktree create <name>"
|
|
185
|
+
return { command: :invalid }
|
|
186
|
+
end
|
|
187
|
+
{ command: "worktree:create", worktree_name: name }
|
|
188
|
+
when "done"
|
|
189
|
+
name = argv.shift
|
|
190
|
+
{ command: "worktree:done", worktree_name: name }
|
|
179
191
|
when "remove"
|
|
180
192
|
force = argv.delete( "--force" ) ? true : false
|
|
181
193
|
worktree_path = argv.shift
|
|
@@ -185,7 +197,7 @@ module Carson
|
|
|
185
197
|
end
|
|
186
198
|
{ command: "worktree:remove", worktree_path: worktree_path, force: force }
|
|
187
199
|
else
|
|
188
|
-
err.puts "#{BADGE} Unknown worktree subcommand: #{action}. Use: carson worktree remove <name
|
|
200
|
+
err.puts "#{BADGE} Unknown worktree subcommand: #{action}. Use: carson worktree create|done|remove <name>"
|
|
189
201
|
{ command: :invalid }
|
|
190
202
|
end
|
|
191
203
|
end
|
|
@@ -239,6 +251,31 @@ module Carson
|
|
|
239
251
|
{ command: "status", json: json_flag }
|
|
240
252
|
end
|
|
241
253
|
|
|
254
|
+
def self.parse_deliver_command( argv:, err: )
|
|
255
|
+
options = { merge: false, title: nil, body_file: nil }
|
|
256
|
+
deliver_parser = OptionParser.new do |opts|
|
|
257
|
+
opts.banner = "Usage: carson deliver [--merge] [--title TITLE] [--body-file PATH]"
|
|
258
|
+
opts.on( "--merge", "Also merge the PR if CI passes" ) { options[ :merge ] = true }
|
|
259
|
+
opts.on( "--title TITLE", "PR title (defaults to branch name)" ) { |v| options[ :title ] = v }
|
|
260
|
+
opts.on( "--body-file PATH", "File containing PR body text" ) { |v| options[ :body_file ] = v }
|
|
261
|
+
end
|
|
262
|
+
deliver_parser.parse!( argv )
|
|
263
|
+
unless argv.empty?
|
|
264
|
+
err.puts "#{BADGE} Unexpected arguments for deliver: #{argv.join( ' ' )}"
|
|
265
|
+
err.puts deliver_parser
|
|
266
|
+
return { command: :invalid }
|
|
267
|
+
end
|
|
268
|
+
{
|
|
269
|
+
command: "deliver",
|
|
270
|
+
merge: options.fetch( :merge ),
|
|
271
|
+
title: options[ :title ],
|
|
272
|
+
body_file: options[ :body_file ]
|
|
273
|
+
}
|
|
274
|
+
rescue OptionParser::ParseError => e
|
|
275
|
+
err.puts "#{BADGE} #{e.message}"
|
|
276
|
+
{ command: :invalid }
|
|
277
|
+
end
|
|
278
|
+
|
|
242
279
|
def self.parse_govern_subcommand( argv:, err: )
|
|
243
280
|
options = {
|
|
244
281
|
dry_run: false,
|
|
@@ -289,6 +326,10 @@ module Carson
|
|
|
289
326
|
runtime.prune!
|
|
290
327
|
when "prune:all"
|
|
291
328
|
runtime.prune_all!
|
|
329
|
+
when "worktree:create"
|
|
330
|
+
runtime.worktree_create!( name: parsed.fetch( :worktree_name ) )
|
|
331
|
+
when "worktree:done"
|
|
332
|
+
runtime.worktree_done!( name: parsed.fetch( :worktree_name, nil ) )
|
|
292
333
|
when "worktree:remove"
|
|
293
334
|
runtime.worktree_remove!( worktree_path: parsed.fetch( :worktree_path ), force: parsed.fetch( :force, false ) )
|
|
294
335
|
when "onboard"
|
|
@@ -303,6 +344,12 @@ module Carson
|
|
|
303
344
|
runtime.template_check!
|
|
304
345
|
when "template:apply"
|
|
305
346
|
runtime.template_apply!( push_prep: parsed.fetch( :push_prep, false ) )
|
|
347
|
+
when "deliver"
|
|
348
|
+
runtime.deliver!(
|
|
349
|
+
merge: parsed.fetch( :merge, false ),
|
|
350
|
+
title: parsed.fetch( :title, nil ),
|
|
351
|
+
body_file: parsed.fetch( :body_file, nil )
|
|
352
|
+
)
|
|
306
353
|
when "review:gate"
|
|
307
354
|
runtime.review_gate!
|
|
308
355
|
when "review:sweep"
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# PR delivery lifecycle — push, create PR, and optionally merge.
|
|
2
|
+
# Collapses the 8-step manual PR flow into one or two commands.
|
|
3
|
+
# `carson deliver` pushes and creates the PR.
|
|
4
|
+
# `carson deliver --merge` also merges if CI is green.
|
|
5
|
+
module Carson
|
|
6
|
+
class Runtime
|
|
7
|
+
module Deliver
|
|
8
|
+
# Entry point for `carson deliver`.
|
|
9
|
+
# Pushes current branch, creates a PR if needed, reports the PR URL.
|
|
10
|
+
# With merge: true, also merges if CI passes and cleans up.
|
|
11
|
+
def deliver!( merge: false, title: nil, body_file: nil )
|
|
12
|
+
branch = current_branch
|
|
13
|
+
main = config.main_branch
|
|
14
|
+
remote = config.git_remote
|
|
15
|
+
|
|
16
|
+
# Guard: cannot deliver from main.
|
|
17
|
+
if branch == main
|
|
18
|
+
puts_line "ERROR: cannot deliver from #{main}. Switch to a feature branch first."
|
|
19
|
+
return EXIT_ERROR
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Step 1: push the branch.
|
|
23
|
+
push_result = push_branch!( branch: branch, remote: remote )
|
|
24
|
+
return push_result unless push_result == EXIT_OK
|
|
25
|
+
|
|
26
|
+
# Step 2: find or create the PR.
|
|
27
|
+
pr_number, pr_url = find_or_create_pr!(
|
|
28
|
+
branch: branch, title: title, body_file: body_file
|
|
29
|
+
)
|
|
30
|
+
return EXIT_ERROR if pr_number.nil?
|
|
31
|
+
|
|
32
|
+
puts_line "PR: ##{pr_number} #{pr_url}"
|
|
33
|
+
|
|
34
|
+
# Without --merge, we are done.
|
|
35
|
+
return EXIT_OK unless merge
|
|
36
|
+
|
|
37
|
+
# Step 3: check CI status.
|
|
38
|
+
ci_status = check_pr_ci( number: pr_number )
|
|
39
|
+
case ci_status
|
|
40
|
+
when :pass
|
|
41
|
+
puts_line "CI: pass"
|
|
42
|
+
when :pending
|
|
43
|
+
puts_line "CI: pending — merge when checks complete."
|
|
44
|
+
return EXIT_OK
|
|
45
|
+
when :fail
|
|
46
|
+
puts_line "CI: failing — fix before merging."
|
|
47
|
+
return EXIT_BLOCK
|
|
48
|
+
else
|
|
49
|
+
puts_line "CI: unknown — check manually."
|
|
50
|
+
return EXIT_OK
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Step 4: merge.
|
|
54
|
+
merge_result = merge_pr!( number: pr_number )
|
|
55
|
+
return merge_result unless merge_result == EXIT_OK
|
|
56
|
+
|
|
57
|
+
# Step 5: sync main.
|
|
58
|
+
sync_after_merge!( remote: remote, main: main )
|
|
59
|
+
|
|
60
|
+
EXIT_OK
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
# Pushes the branch to the remote with tracking.
|
|
66
|
+
def push_branch!( branch:, remote: )
|
|
67
|
+
_, push_stderr, push_success, = git_run( "push", "-u", remote, branch )
|
|
68
|
+
unless push_success
|
|
69
|
+
error_text = push_stderr.to_s.strip
|
|
70
|
+
error_text = "push failed" if error_text.empty?
|
|
71
|
+
puts_line "ERROR: #{error_text}"
|
|
72
|
+
return EXIT_ERROR
|
|
73
|
+
end
|
|
74
|
+
puts_verbose "pushed #{branch} to #{remote}"
|
|
75
|
+
EXIT_OK
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Finds an existing PR for the branch, or creates a new one.
|
|
79
|
+
# Returns [number, url] or [nil, nil] on failure.
|
|
80
|
+
def find_or_create_pr!( branch:, title: nil, body_file: nil )
|
|
81
|
+
# Check for existing PR.
|
|
82
|
+
existing = find_existing_pr( branch: branch )
|
|
83
|
+
return existing if existing.first
|
|
84
|
+
|
|
85
|
+
# Create a new PR.
|
|
86
|
+
create_pr!( branch: branch, title: title, body_file: body_file )
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Queries gh for an open PR on this branch.
|
|
90
|
+
# Returns [number, url] or [nil, nil].
|
|
91
|
+
def find_existing_pr( branch: )
|
|
92
|
+
stdout, _, success, = gh_run(
|
|
93
|
+
"pr", "view", branch,
|
|
94
|
+
"--json", "number,url"
|
|
95
|
+
)
|
|
96
|
+
if success
|
|
97
|
+
data = JSON.parse( stdout ) rescue nil
|
|
98
|
+
if data && data[ "number" ]
|
|
99
|
+
return [ data[ "number" ], data[ "url" ].to_s ]
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
[ nil, nil ]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Creates a PR via gh. Title defaults to branch name humanised.
|
|
106
|
+
# Returns [number, url] or [nil, nil] on failure.
|
|
107
|
+
def create_pr!( branch:, title: nil, body_file: nil )
|
|
108
|
+
pr_title = title || default_pr_title( branch: branch )
|
|
109
|
+
|
|
110
|
+
args = [ "pr", "create", "--title", pr_title, "--head", branch ]
|
|
111
|
+
if body_file && File.exist?( body_file )
|
|
112
|
+
args.push( "--body-file", body_file )
|
|
113
|
+
else
|
|
114
|
+
args.push( "--body", "" )
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
stdout, stderr, success, = gh_run( *args )
|
|
118
|
+
unless success
|
|
119
|
+
error_text = stderr.to_s.strip
|
|
120
|
+
error_text = "pr create failed" if error_text.empty?
|
|
121
|
+
puts_line "ERROR: #{error_text}"
|
|
122
|
+
return [ nil, nil ]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# gh pr create prints the URL on success. Parse number from it.
|
|
126
|
+
pr_url = stdout.to_s.strip
|
|
127
|
+
pr_number = pr_url.split( "/" ).last.to_i
|
|
128
|
+
if pr_number > 0
|
|
129
|
+
[ pr_number, pr_url ]
|
|
130
|
+
else
|
|
131
|
+
# Fallback: query the just-created PR.
|
|
132
|
+
find_existing_pr( branch: branch )
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Generates a default PR title from the branch name.
|
|
137
|
+
def default_pr_title( branch: )
|
|
138
|
+
branch.tr( "-", " " ).gsub( "/", ": " ).sub( /\A\w/ ) { |c| c.upcase }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Checks CI status on a PR. Returns :pass, :fail, :pending, or :none.
|
|
142
|
+
def check_pr_ci( number: )
|
|
143
|
+
stdout, _, success, = gh_run(
|
|
144
|
+
"pr", "checks", number.to_s,
|
|
145
|
+
"--json", "name,state,conclusion"
|
|
146
|
+
)
|
|
147
|
+
return :none unless success
|
|
148
|
+
|
|
149
|
+
checks = JSON.parse( stdout ) rescue []
|
|
150
|
+
return :none if checks.empty?
|
|
151
|
+
|
|
152
|
+
conclusions = checks.map { |c| c[ "conclusion" ].to_s.upcase }
|
|
153
|
+
states = checks.map { |c| c[ "state" ].to_s.upcase }
|
|
154
|
+
|
|
155
|
+
return :fail if conclusions.any? { |c| c == "FAILURE" || c == "CANCELLED" || c == "TIMED_OUT" }
|
|
156
|
+
return :pending if states.any? { |s| s == "PENDING" || s == "QUEUED" || s == "IN_PROGRESS" } ||
|
|
157
|
+
conclusions.any? { |c| c == "" || c == "PENDING" }
|
|
158
|
+
|
|
159
|
+
:pass
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Merges the PR using the configured merge method.
|
|
163
|
+
def merge_pr!( number: )
|
|
164
|
+
method = config.govern_merge_method
|
|
165
|
+
stdout, stderr, success, = gh_run(
|
|
166
|
+
"pr", "merge", number.to_s,
|
|
167
|
+
"--#{method}",
|
|
168
|
+
"--delete-branch"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
if success
|
|
172
|
+
puts_line "Merged PR ##{number} via #{method}."
|
|
173
|
+
EXIT_OK
|
|
174
|
+
else
|
|
175
|
+
error_text = stderr.to_s.strip
|
|
176
|
+
error_text = "merge failed" if error_text.empty?
|
|
177
|
+
puts_line "ERROR: #{error_text}"
|
|
178
|
+
EXIT_ERROR
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Syncs main after a successful merge.
|
|
183
|
+
def sync_after_merge!( remote:, main: )
|
|
184
|
+
git_run( "checkout", main )
|
|
185
|
+
git_run( "pull", remote, main )
|
|
186
|
+
puts_verbose "synced #{main} from #{remote}"
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
include Deliver
|
|
191
|
+
end
|
|
192
|
+
end
|
|
@@ -2,7 +2,83 @@ module Carson
|
|
|
2
2
|
class Runtime
|
|
3
3
|
module Local
|
|
4
4
|
# Safe worktree lifecycle management for coding agents.
|
|
5
|
-
#
|
|
5
|
+
# Three operations: create, done (mark completed), remove (batch cleanup).
|
|
6
|
+
# The deferred deletion model: worktrees persist after use, cleaned up later.
|
|
7
|
+
|
|
8
|
+
# Creates a new worktree under .claude/worktrees/<name> with a fresh branch.
|
|
9
|
+
def worktree_create!( name: )
|
|
10
|
+
worktrees_dir = File.join( repo_root, ".claude", "worktrees" )
|
|
11
|
+
wt_path = File.join( worktrees_dir, name )
|
|
12
|
+
|
|
13
|
+
if Dir.exist?( wt_path )
|
|
14
|
+
puts_line "ERROR: worktree already exists: #{name}"
|
|
15
|
+
puts_line " Path: #{wt_path}"
|
|
16
|
+
return EXIT_ERROR
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Determine the base branch (main branch from config).
|
|
20
|
+
base = config.main_branch
|
|
21
|
+
|
|
22
|
+
# Create the worktree with a new branch based on the main branch.
|
|
23
|
+
FileUtils.mkdir_p( worktrees_dir )
|
|
24
|
+
_, wt_stderr, wt_success, = git_run( "worktree", "add", wt_path, "-b", name, base )
|
|
25
|
+
unless wt_success
|
|
26
|
+
error_text = wt_stderr.to_s.strip
|
|
27
|
+
error_text = "unable to create worktree" if error_text.empty?
|
|
28
|
+
puts_line "ERROR: #{error_text}"
|
|
29
|
+
return EXIT_ERROR
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
puts_line "Worktree created: #{name}"
|
|
33
|
+
puts_line " Path: #{wt_path}"
|
|
34
|
+
puts_line " Branch: #{name}"
|
|
35
|
+
EXIT_OK
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Marks a worktree as completed without deleting it.
|
|
39
|
+
# Verifies all changes are committed. Deferred deletion — cleanup happens later.
|
|
40
|
+
def worktree_done!( name: nil )
|
|
41
|
+
if name.to_s.strip.empty?
|
|
42
|
+
# Try to detect current worktree from CWD.
|
|
43
|
+
puts_line "ERROR: missing worktree name. Use: carson worktree done <name>"
|
|
44
|
+
return EXIT_ERROR
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
resolved_path = resolve_worktree_path( worktree_path: name )
|
|
48
|
+
|
|
49
|
+
unless worktree_registered?( path: resolved_path )
|
|
50
|
+
puts_line "ERROR: #{name} is not a registered worktree."
|
|
51
|
+
return EXIT_ERROR
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Check for uncommitted changes in the worktree.
|
|
55
|
+
wt_status, _, status_success, = Open3.capture3( "git", "status", "--porcelain", chdir: resolved_path )
|
|
56
|
+
if status_success && !wt_status.strip.empty?
|
|
57
|
+
puts_line "Worktree has uncommitted changes: #{name}"
|
|
58
|
+
puts_line " Commit your changes first, then run `carson worktree done #{name}` again."
|
|
59
|
+
return EXIT_BLOCK
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Check for unpushed commits.
|
|
63
|
+
branch = worktree_branch( path: resolved_path )
|
|
64
|
+
if branch
|
|
65
|
+
remote = config.git_remote
|
|
66
|
+
remote_ref = "#{remote}/#{branch}"
|
|
67
|
+
ahead, _, ahead_ok, = Open3.capture3( "git", "rev-list", "--count", "#{remote_ref}..#{branch}", chdir: resolved_path )
|
|
68
|
+
if ahead_ok && ahead.strip.to_i > 0
|
|
69
|
+
puts_line "Worktree has unpushed commits: #{name}"
|
|
70
|
+
puts_line " Push with `git -C #{resolved_path} push #{remote} #{branch}` first."
|
|
71
|
+
return EXIT_BLOCK
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
puts_line "Worktree done: #{name}"
|
|
76
|
+
puts_line " Branch: #{branch || '(detached)'}"
|
|
77
|
+
puts_line " Cleanup later with `carson worktree remove #{name}` or `carson housekeep`."
|
|
78
|
+
EXIT_OK
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Removes a worktree: directory, git registration, and branch.
|
|
6
82
|
# Never forces removal — if the worktree has uncommitted changes, refuses unless
|
|
7
83
|
# the user explicitly passes force: true via CLI --force flag.
|
|
8
84
|
def worktree_remove!( worktree_path:, force: false )
|
data/lib/carson/runtime.rb
CHANGED
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.
|
|
4
|
+
version: 3.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Hailei Wang
|
|
@@ -52,6 +52,7 @@ files:
|
|
|
52
52
|
- lib/carson/config.rb
|
|
53
53
|
- lib/carson/runtime.rb
|
|
54
54
|
- lib/carson/runtime/audit.rb
|
|
55
|
+
- lib/carson/runtime/deliver.rb
|
|
55
56
|
- lib/carson/runtime/govern.rb
|
|
56
57
|
- lib/carson/runtime/local.rb
|
|
57
58
|
- lib/carson/runtime/local/hooks.rb
|