ace-git-worktree 0.19.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 +7 -0
- data/.ace-defaults/git/worktree.yml +250 -0
- data/.ace-defaults/nav/protocols/wfi-sources/ace-git-worktree.yml +19 -0
- data/CHANGELOG.md +957 -0
- data/LICENSE +21 -0
- data/README.md +40 -0
- data/Rakefile +14 -0
- data/docs/demo/ace-git-worktree-getting-started.gif +0 -0
- data/docs/demo/ace-git-worktree-getting-started.tape.yml +28 -0
- data/docs/demo/fixtures/README.md +3 -0
- data/docs/demo/fixtures/sample.txt +1 -0
- data/docs/getting-started.md +114 -0
- data/docs/handbook.md +38 -0
- data/docs/usage.md +334 -0
- data/exe/ace-git-worktree +24 -0
- data/handbook/agents/worktree.ag.md +189 -0
- data/handbook/skills/as-git-worktree/SKILL.md +27 -0
- data/handbook/skills/as-git-worktree-create/SKILL.md +21 -0
- data/handbook/skills/as-git-worktree-manage/SKILL.md +20 -0
- data/handbook/workflow-instructions/git/worktree-create.wf.md +262 -0
- data/handbook/workflow-instructions/git/worktree-manage.wf.md +384 -0
- data/handbook/workflow-instructions/git/worktree.wf.md +224 -0
- data/lib/ace/git/worktree/atoms/git_command.rb +121 -0
- data/lib/ace/git/worktree/atoms/path_expander.rb +189 -0
- data/lib/ace/git/worktree/atoms/slug_generator.rb +235 -0
- data/lib/ace/git/worktree/atoms/task_id_extractor.rb +91 -0
- data/lib/ace/git/worktree/cli/commands/config.rb +50 -0
- data/lib/ace/git/worktree/cli/commands/create.rb +80 -0
- data/lib/ace/git/worktree/cli/commands/list.rb +76 -0
- data/lib/ace/git/worktree/cli/commands/prune.rb +43 -0
- data/lib/ace/git/worktree/cli/commands/remove.rb +48 -0
- data/lib/ace/git/worktree/cli/commands/shared_helpers.rb +66 -0
- data/lib/ace/git/worktree/cli/commands/switch.rb +44 -0
- data/lib/ace/git/worktree/cli.rb +103 -0
- data/lib/ace/git/worktree/commands/config_command.rb +351 -0
- data/lib/ace/git/worktree/commands/create_command.rb +961 -0
- data/lib/ace/git/worktree/commands/list_command.rb +247 -0
- data/lib/ace/git/worktree/commands/prune_command.rb +260 -0
- data/lib/ace/git/worktree/commands/remove_command.rb +522 -0
- data/lib/ace/git/worktree/commands/switch_command.rb +249 -0
- data/lib/ace/git/worktree/configuration.rb +167 -0
- data/lib/ace/git/worktree/models/worktree_config.rb +502 -0
- data/lib/ace/git/worktree/models/worktree_info.rb +303 -0
- data/lib/ace/git/worktree/models/worktree_metadata.rb +294 -0
- data/lib/ace/git/worktree/molecules/config_loader.rb +125 -0
- data/lib/ace/git/worktree/molecules/current_task_linker.rb +136 -0
- data/lib/ace/git/worktree/molecules/hook_executor.rb +361 -0
- data/lib/ace/git/worktree/molecules/parent_task_resolver.rb +186 -0
- data/lib/ace/git/worktree/molecules/pr_creator.rb +253 -0
- data/lib/ace/git/worktree/molecules/task_committer.rb +329 -0
- data/lib/ace/git/worktree/molecules/task_fetcher.rb +244 -0
- data/lib/ace/git/worktree/molecules/task_pusher.rb +183 -0
- data/lib/ace/git/worktree/molecules/task_status_updater.rb +447 -0
- data/lib/ace/git/worktree/molecules/worktree_creator.rb +832 -0
- data/lib/ace/git/worktree/molecules/worktree_lister.rb +337 -0
- data/lib/ace/git/worktree/molecules/worktree_remover.rb +416 -0
- data/lib/ace/git/worktree/organisms/task_worktree_orchestrator.rb +906 -0
- data/lib/ace/git/worktree/organisms/worktree_manager.rb +714 -0
- data/lib/ace/git/worktree/version.rb +9 -0
- data/lib/ace/git/worktree.rb +215 -0
- metadata +218 -0
|
@@ -0,0 +1,961 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Git
|
|
5
|
+
module Worktree
|
|
6
|
+
module Commands
|
|
7
|
+
# Create command
|
|
8
|
+
#
|
|
9
|
+
# Handles worktree creation with support for both task-aware and traditional
|
|
10
|
+
# worktree creation. Provides various options for customization.
|
|
11
|
+
#
|
|
12
|
+
# @example Task-aware worktree creation
|
|
13
|
+
# CreateCommand.new.run(["--task", "081"])
|
|
14
|
+
#
|
|
15
|
+
# @example Traditional worktree creation
|
|
16
|
+
# CreateCommand.new.run(["feature-branch"])
|
|
17
|
+
#
|
|
18
|
+
# @example Dry run
|
|
19
|
+
# CreateCommand.new.run(["--task", "081", "--dry-run"])
|
|
20
|
+
class CreateCommand
|
|
21
|
+
# Pattern for validating PR numbers (digits only)
|
|
22
|
+
PR_NUMBER_PATTERN = /^\d+$/
|
|
23
|
+
|
|
24
|
+
# Initialize a new CreateCommand
|
|
25
|
+
#
|
|
26
|
+
# @param manager [Object] Optional manager dependency for testing
|
|
27
|
+
def initialize(manager: nil)
|
|
28
|
+
@manager = manager || Ace::Git::Worktree::Organisms::WorktreeManager.new
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Run the create command
|
|
32
|
+
#
|
|
33
|
+
# @param args [Array<String>] Command arguments
|
|
34
|
+
# @return [Integer] Exit code (0 for success, 1 for error)
|
|
35
|
+
def run(args = [])
|
|
36
|
+
# Show help if no arguments provided
|
|
37
|
+
return show_help if args.empty?
|
|
38
|
+
|
|
39
|
+
options = parse_arguments(args)
|
|
40
|
+
return show_help if options[:help]
|
|
41
|
+
|
|
42
|
+
validate_options(options)
|
|
43
|
+
|
|
44
|
+
if options[:task]
|
|
45
|
+
create_task_worktree(options)
|
|
46
|
+
elsif options[:pr]
|
|
47
|
+
create_pr_worktree(options)
|
|
48
|
+
elsif options[:branch]
|
|
49
|
+
create_branch_worktree(options)
|
|
50
|
+
else
|
|
51
|
+
create_traditional_worktree(options)
|
|
52
|
+
end
|
|
53
|
+
rescue ArgumentError => e
|
|
54
|
+
puts "Error: #{e.message}"
|
|
55
|
+
puts
|
|
56
|
+
show_help
|
|
57
|
+
1
|
|
58
|
+
rescue Ace::Git::PrNotFoundError,
|
|
59
|
+
Ace::Git::GhAuthenticationError,
|
|
60
|
+
Ace::Git::GhNotInstalledError,
|
|
61
|
+
Ace::Git::TimeoutError => e
|
|
62
|
+
puts "Error: #{e.message}"
|
|
63
|
+
1
|
|
64
|
+
rescue Ace::Git::Error => e
|
|
65
|
+
# Catch other ace-git specific errors
|
|
66
|
+
puts "Error: #{e.message}"
|
|
67
|
+
puts "Debug: #{e.class}" if ENV["DEBUG"]
|
|
68
|
+
1
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Show help for the create command
|
|
72
|
+
#
|
|
73
|
+
# @return [Integer] Exit code
|
|
74
|
+
def show_help
|
|
75
|
+
puts <<~HELP
|
|
76
|
+
ace-git-worktree create - Create a new worktree
|
|
77
|
+
|
|
78
|
+
USAGE:
|
|
79
|
+
ace-git-worktree create <branch-name> [OPTIONS]
|
|
80
|
+
ace-git-worktree create --task <task-id> [OPTIONS]
|
|
81
|
+
ace-git-worktree create --pr <pr-number> [OPTIONS]
|
|
82
|
+
ace-git-worktree create --branch <branch> [OPTIONS]
|
|
83
|
+
|
|
84
|
+
TASK-AWARE CREATION:
|
|
85
|
+
--task <task-id> Create worktree for a specific task
|
|
86
|
+
Task ID formats: 081, task.081, v.0.9.0+081
|
|
87
|
+
|
|
88
|
+
PR-AWARE CREATION:
|
|
89
|
+
--pr <number> Create worktree for a GitHub pull request
|
|
90
|
+
--pull-request <number> (alias for --pr)
|
|
91
|
+
Requires gh CLI to be installed and authenticated
|
|
92
|
+
|
|
93
|
+
BRANCH-AWARE CREATION:
|
|
94
|
+
-b <branch> Create worktree from a branch (local or remote)
|
|
95
|
+
--branch <branch> (alias for -b)
|
|
96
|
+
Remote branches: origin/feature, upstream/main
|
|
97
|
+
Local branches: feature-name
|
|
98
|
+
|
|
99
|
+
OPTIONS:
|
|
100
|
+
--path <path> Custom worktree path (default: from config)
|
|
101
|
+
--source <ref> Git ref to use as branch start-point (default: current branch)
|
|
102
|
+
Examples: main, origin/develop, HEAD~3, commit-sha
|
|
103
|
+
--dry-run Show what would be created without creating
|
|
104
|
+
--no-status-update Skip marking task as in-progress (task mode only)
|
|
105
|
+
--no-commit Skip committing task changes (task mode only)
|
|
106
|
+
--no-push Skip pushing task changes to remote (task mode only)
|
|
107
|
+
--no-upstream Skip pushing worktree branch with upstream tracking (task mode only)
|
|
108
|
+
--no-pr Skip creating draft PR (task mode only)
|
|
109
|
+
--push-remote <name> Remote to push to (default: origin) (task mode only)
|
|
110
|
+
--no-auto-navigate Stay in current directory (default: navigate to worktree)
|
|
111
|
+
--commit-message <msg> Custom commit message for task updates (task mode only)
|
|
112
|
+
--force Create even if worktree already exists
|
|
113
|
+
--help, -h Show this help message
|
|
114
|
+
|
|
115
|
+
EXAMPLES:
|
|
116
|
+
# Create task-aware worktree
|
|
117
|
+
ace-git-worktree create --task 081
|
|
118
|
+
|
|
119
|
+
# Create task worktree based on main instead of current branch
|
|
120
|
+
ace-git-worktree create --task 081 --source main
|
|
121
|
+
|
|
122
|
+
# Create PR worktree
|
|
123
|
+
ace-git-worktree create --pr 26
|
|
124
|
+
|
|
125
|
+
# Create worktree from remote branch
|
|
126
|
+
ace-git-worktree create -b origin/feature/auth
|
|
127
|
+
|
|
128
|
+
# Create worktree from local branch
|
|
129
|
+
ace-git-worktree create -b my-feature
|
|
130
|
+
|
|
131
|
+
# Create traditional worktree
|
|
132
|
+
ace-git-worktree create feature-branch
|
|
133
|
+
|
|
134
|
+
# Create traditional worktree based on specific commit
|
|
135
|
+
ace-git-worktree create feature-branch --source HEAD~3
|
|
136
|
+
|
|
137
|
+
# Custom path and dry run
|
|
138
|
+
ace-git-worktree create --pr 26 --path ~/worktrees --dry-run
|
|
139
|
+
|
|
140
|
+
CONFIGURATION:
|
|
141
|
+
Worktree creation is controlled by .ace/git/worktree.yml:
|
|
142
|
+
- root_path: Default worktree root directory
|
|
143
|
+
- task.auto_*: Automation settings for task workflows
|
|
144
|
+
- pr.directory_format: PR worktree naming format (default: ace-pr-{number})
|
|
145
|
+
- pr.branch_format: PR branch naming format (default: pr-{number}-{slug})
|
|
146
|
+
Variables: {number}, {slug}, {title_slug}, {base_branch}
|
|
147
|
+
- hooks.after_create: Commands to run after worktree creation
|
|
148
|
+
|
|
149
|
+
REQUIREMENTS:
|
|
150
|
+
PR-aware creation requires GitHub CLI (gh):
|
|
151
|
+
- Install: brew install gh
|
|
152
|
+
- Authenticate: gh auth login
|
|
153
|
+
HELP
|
|
154
|
+
0
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
private
|
|
158
|
+
|
|
159
|
+
# Parse command line arguments
|
|
160
|
+
#
|
|
161
|
+
# @param args [Array<String>] Command arguments
|
|
162
|
+
# @return [Hash] Parsed options
|
|
163
|
+
def parse_arguments(args)
|
|
164
|
+
options = {
|
|
165
|
+
task: nil,
|
|
166
|
+
pr: nil,
|
|
167
|
+
branch: nil,
|
|
168
|
+
path: nil,
|
|
169
|
+
source: nil,
|
|
170
|
+
dry_run: false,
|
|
171
|
+
no_status_update: false,
|
|
172
|
+
no_commit: false,
|
|
173
|
+
no_push: false,
|
|
174
|
+
no_upstream: false,
|
|
175
|
+
no_pr: false,
|
|
176
|
+
push_remote: nil,
|
|
177
|
+
no_auto_navigate: false,
|
|
178
|
+
commit_message: nil,
|
|
179
|
+
target_branch: nil,
|
|
180
|
+
force: false,
|
|
181
|
+
help: false
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
i = 0
|
|
185
|
+
while i < args.length
|
|
186
|
+
arg = args[i]
|
|
187
|
+
|
|
188
|
+
case arg
|
|
189
|
+
when "--task"
|
|
190
|
+
i += 1
|
|
191
|
+
options[:task] = args[i]
|
|
192
|
+
when "--pr", "--pull-request"
|
|
193
|
+
i += 1
|
|
194
|
+
options[:pr] = args[i]
|
|
195
|
+
when "-b", "--branch"
|
|
196
|
+
i += 1
|
|
197
|
+
options[:branch] = args[i]
|
|
198
|
+
when "--path"
|
|
199
|
+
i += 1
|
|
200
|
+
options[:path] = args[i]
|
|
201
|
+
when "--source"
|
|
202
|
+
i += 1
|
|
203
|
+
options[:source] = args[i]
|
|
204
|
+
when "--dry-run"
|
|
205
|
+
options[:dry_run] = true
|
|
206
|
+
when "--no-status-update"
|
|
207
|
+
options[:no_status_update] = true
|
|
208
|
+
when "--no-commit"
|
|
209
|
+
options[:no_commit] = true
|
|
210
|
+
when "--no-push"
|
|
211
|
+
options[:no_push] = true
|
|
212
|
+
when "--no-upstream"
|
|
213
|
+
options[:no_upstream] = true
|
|
214
|
+
when "--no-pr"
|
|
215
|
+
options[:no_pr] = true
|
|
216
|
+
when "--push-remote"
|
|
217
|
+
i += 1
|
|
218
|
+
options[:push_remote] = args[i]
|
|
219
|
+
when "--no-auto-navigate"
|
|
220
|
+
options[:no_auto_navigate] = true
|
|
221
|
+
when "--commit-message"
|
|
222
|
+
i += 1
|
|
223
|
+
options[:commit_message] = args[i]
|
|
224
|
+
when "--target-branch"
|
|
225
|
+
i += 1
|
|
226
|
+
options[:target_branch] = args[i]
|
|
227
|
+
when "--force"
|
|
228
|
+
options[:force] = true
|
|
229
|
+
when "--no-mise-trust"
|
|
230
|
+
options[:no_mise_trust] = true
|
|
231
|
+
when "--help", "-h"
|
|
232
|
+
options[:help] = true
|
|
233
|
+
when /^--/
|
|
234
|
+
raise ArgumentError, "Unknown option: #{arg}"
|
|
235
|
+
else
|
|
236
|
+
# Positional argument - branch name for traditional creation
|
|
237
|
+
if options[:branch_name]
|
|
238
|
+
raise ArgumentError, "Multiple branch names specified: #{options[:branch_name]} and #{arg}"
|
|
239
|
+
end
|
|
240
|
+
options[:branch_name] = arg
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
i += 1
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
options
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Detect which creation mode(s) are specified
|
|
250
|
+
#
|
|
251
|
+
# @param options [Hash] Parsed options
|
|
252
|
+
# @return [Array<Symbol>] List of detected modes (:task, :pr, :branch, :traditional)
|
|
253
|
+
def detect_creation_modes(options)
|
|
254
|
+
modes = []
|
|
255
|
+
modes << :task if options[:task]
|
|
256
|
+
modes << :pr if options[:pr]
|
|
257
|
+
modes << :branch if options[:branch]
|
|
258
|
+
modes << :traditional if options[:branch_name]
|
|
259
|
+
modes
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Validate parsed options
|
|
263
|
+
#
|
|
264
|
+
# @param options [Hash] Parsed options
|
|
265
|
+
def validate_options(options)
|
|
266
|
+
# Detect creation modes
|
|
267
|
+
modes = detect_creation_modes(options)
|
|
268
|
+
|
|
269
|
+
# Check for conflicts
|
|
270
|
+
if modes.length > 1
|
|
271
|
+
raise ArgumentError, "Cannot use multiple creation modes: #{modes.join(", ")}. " \
|
|
272
|
+
"Use only one of --task, --pr, --branch, or <branch-name>"
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Require at least one mode
|
|
276
|
+
if modes.empty?
|
|
277
|
+
raise ArgumentError, "Must specify either --task <task-id>, --pr <number>, --branch <branch>, or <branch-name>"
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Validate each mode's input
|
|
281
|
+
if options[:task] && options[:task].empty?
|
|
282
|
+
raise ArgumentError, "Task ID cannot be empty"
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Security validation for task IDs
|
|
286
|
+
if options[:task] && contains_dangerous_patterns?(options[:task])
|
|
287
|
+
raise ArgumentError, "Task ID contains potentially dangerous characters"
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
if options[:pr] && options[:pr].empty?
|
|
291
|
+
raise ArgumentError, "PR number cannot be empty"
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
if options[:branch] && options[:branch].empty?
|
|
295
|
+
raise ArgumentError, "Branch name cannot be empty"
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
if options[:branch_name] && options[:branch_name].empty?
|
|
299
|
+
raise ArgumentError, "Branch name cannot be empty"
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Validate PR number is numeric
|
|
303
|
+
if options[:pr] && !options[:pr].match?(/^\d+$/)
|
|
304
|
+
raise ArgumentError, "PR number must be a positive integer"
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
if options[:commit_message] && options[:commit_message].empty?
|
|
308
|
+
raise ArgumentError, "Commit message cannot be empty"
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Security validation for paths
|
|
312
|
+
if options[:path] && contains_dangerous_patterns?(options[:path])
|
|
313
|
+
raise ArgumentError, "Path contains potentially dangerous characters"
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Check if a string contains dangerous patterns
|
|
318
|
+
#
|
|
319
|
+
# Matches TaskFetcher's validation to ensure consistent security boundaries.
|
|
320
|
+
# Rejects shell metacharacters, null bytes, newlines, redirects, and path traversal.
|
|
321
|
+
#
|
|
322
|
+
# @param value [String] Value to check
|
|
323
|
+
# @return [Boolean] true if dangerous patterns found
|
|
324
|
+
def contains_dangerous_patterns?(value)
|
|
325
|
+
return false if value.nil?
|
|
326
|
+
|
|
327
|
+
# Patterns from TaskFetcher.valid_task_reference? for consistency
|
|
328
|
+
dangerous_patterns = [
|
|
329
|
+
/[;&|`$(){}\[\]]/, # Shell metacharacters
|
|
330
|
+
/\x00/, # Null bytes
|
|
331
|
+
/[\r\n]/, # Newlines
|
|
332
|
+
/[<>]/, # Redirects
|
|
333
|
+
/\.\./ # Path traversal
|
|
334
|
+
]
|
|
335
|
+
|
|
336
|
+
dangerous_patterns.any? { |pattern| value.match?(pattern) }
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# Create a task-aware worktree
|
|
340
|
+
#
|
|
341
|
+
# @param options [Hash] Command options
|
|
342
|
+
# @return [Integer] Exit code
|
|
343
|
+
def create_task_worktree(options)
|
|
344
|
+
@options = options
|
|
345
|
+
puts "Creating worktree for task: #{options[:task]}"
|
|
346
|
+
|
|
347
|
+
# Check ace-task availability first
|
|
348
|
+
availability_check = check_task_dependency_availability
|
|
349
|
+
unless availability_check[:available]
|
|
350
|
+
puts "Error: ace-task is required for task-aware worktree creation."
|
|
351
|
+
puts
|
|
352
|
+
puts availability_check[:message]
|
|
353
|
+
puts
|
|
354
|
+
puts "Alternative: Use traditional worktree creation:"
|
|
355
|
+
puts " ace-git-worktree create <branch-name>"
|
|
356
|
+
puts
|
|
357
|
+
return 1
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# Prepare creation options
|
|
361
|
+
creation_options = {
|
|
362
|
+
path: options[:path],
|
|
363
|
+
source: options[:source],
|
|
364
|
+
dry_run: options[:dry_run],
|
|
365
|
+
no_status_update: options[:no_status_update],
|
|
366
|
+
no_commit: options[:no_commit],
|
|
367
|
+
no_push: options[:no_push],
|
|
368
|
+
no_upstream: options[:no_upstream],
|
|
369
|
+
no_pr: options[:no_pr],
|
|
370
|
+
push_remote: options[:push_remote],
|
|
371
|
+
commit_message: options[:commit_message],
|
|
372
|
+
target_branch: options[:target_branch],
|
|
373
|
+
no_mise_trust: options[:no_mise_trust],
|
|
374
|
+
force: options[:force]
|
|
375
|
+
}.compact
|
|
376
|
+
|
|
377
|
+
# Create the worktree
|
|
378
|
+
result = @manager.create_task(options[:task], creation_options)
|
|
379
|
+
|
|
380
|
+
if result[:success]
|
|
381
|
+
display_task_creation_result(result, options[:dry_run])
|
|
382
|
+
0
|
|
383
|
+
else
|
|
384
|
+
puts "Failed to create worktree: #{result[:error]}"
|
|
385
|
+
display_warnings(result[:warnings]) if result[:warnings]
|
|
386
|
+
|
|
387
|
+
# Provide helpful guidance based on error type
|
|
388
|
+
if result[:error]&.include?("ace-task")
|
|
389
|
+
puts
|
|
390
|
+
puts "ace-task issue detected. Check that:"
|
|
391
|
+
puts " 1. ace-task is installed: gem install ace-task"
|
|
392
|
+
puts " 2. ace-task is in your PATH: which ace-task"
|
|
393
|
+
puts " 3. Task '#{options[:task]}' exists"
|
|
394
|
+
elsif result[:error]&.include?("not found")
|
|
395
|
+
puts
|
|
396
|
+
puts "Task not found suggestions:"
|
|
397
|
+
puts " 1. Check task ID format (try: #{options[:task]})"
|
|
398
|
+
puts " 2. List available tasks: ace-task list"
|
|
399
|
+
puts " 3. Verify you're in the correct project directory"
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
1
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
# Create a PR worktree
|
|
407
|
+
#
|
|
408
|
+
# @param options [Hash] Command options
|
|
409
|
+
# @return [Integer] Exit code
|
|
410
|
+
def create_pr_worktree(options)
|
|
411
|
+
@options = options
|
|
412
|
+
pr_input = options[:pr].to_s.strip
|
|
413
|
+
|
|
414
|
+
# Validate PR number is numeric to provide clear error message
|
|
415
|
+
unless pr_input.match?(PR_NUMBER_PATTERN)
|
|
416
|
+
puts "Error: Invalid PR number '#{pr_input}'. Please provide a numeric PR number."
|
|
417
|
+
return 1
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
pr_number = pr_input.to_i
|
|
421
|
+
|
|
422
|
+
# Validate PR number is within reasonable bounds
|
|
423
|
+
if pr_number <= 0 || pr_number > 999_999
|
|
424
|
+
puts "Error: PR number must be between 1 and 999999."
|
|
425
|
+
return 1
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
puts "Creating worktree for PR ##{pr_number}..."
|
|
429
|
+
|
|
430
|
+
# Check gh CLI availability using ace-git's PrMetadataFetcher
|
|
431
|
+
unless Ace::Git::Molecules::PrMetadataFetcher.gh_installed?
|
|
432
|
+
puts "Error: gh CLI is required for PR worktree creation."
|
|
433
|
+
puts
|
|
434
|
+
puts gh_not_available_message
|
|
435
|
+
return 1
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
unless Ace::Git::Molecules::PrMetadataFetcher.gh_authenticated?
|
|
439
|
+
puts "Error: gh CLI is not authenticated."
|
|
440
|
+
puts
|
|
441
|
+
puts "Authenticate with: gh auth login"
|
|
442
|
+
return 1
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
# Fetch PR data
|
|
446
|
+
puts "Fetching PR information..."
|
|
447
|
+
begin
|
|
448
|
+
result = Ace::Git::Molecules::PrMetadataFetcher.fetch_metadata(pr_number.to_s)
|
|
449
|
+
|
|
450
|
+
unless result[:success]
|
|
451
|
+
puts "Error: #{result[:error]}"
|
|
452
|
+
return 1
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
metadata = result[:metadata]
|
|
456
|
+
|
|
457
|
+
# Convert to pr_data format expected by worktree creator
|
|
458
|
+
pr_data = pr_data_from_metadata(metadata)
|
|
459
|
+
|
|
460
|
+
# Show PR details
|
|
461
|
+
puts "PR ##{pr_data[:number]}: #{pr_data[:title]}"
|
|
462
|
+
puts "Branch: #{pr_data[:head_branch]} -> #{pr_data[:base_branch]}"
|
|
463
|
+
|
|
464
|
+
# Warn about fork PRs
|
|
465
|
+
if pr_data[:is_cross_repository]
|
|
466
|
+
puts
|
|
467
|
+
puts "Warning: This PR is from a fork (#{pr_data[:head_repository_owner]})."
|
|
468
|
+
puts "You will not be able to push to the PR branch directly."
|
|
469
|
+
end
|
|
470
|
+
puts
|
|
471
|
+
|
|
472
|
+
# Prepare creation options
|
|
473
|
+
creation_options = {
|
|
474
|
+
path: options[:path],
|
|
475
|
+
dry_run: options[:dry_run],
|
|
476
|
+
no_mise_trust: options[:no_mise_trust],
|
|
477
|
+
force: options[:force]
|
|
478
|
+
}.compact
|
|
479
|
+
|
|
480
|
+
# Create the worktree
|
|
481
|
+
result = @manager.create_pr(pr_number, pr_data, creation_options)
|
|
482
|
+
|
|
483
|
+
if result[:success]
|
|
484
|
+
display_pr_creation_result(result, options[:dry_run])
|
|
485
|
+
0
|
|
486
|
+
else
|
|
487
|
+
puts "Failed to create worktree: #{result[:error]}"
|
|
488
|
+
display_warnings(result[:warnings]) if result[:warnings]
|
|
489
|
+
1
|
|
490
|
+
end
|
|
491
|
+
rescue Ace::Git::PrNotFoundError, Ace::Git::GhAuthenticationError,
|
|
492
|
+
Ace::Git::GhNotInstalledError => e
|
|
493
|
+
# Let specific ace-git errors be handled by handle_pr_fetch_error
|
|
494
|
+
# Other errors will bubble up to the top-level rescue in run()
|
|
495
|
+
handle_pr_fetch_error(e, pr_number)
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
# Create a branch worktree
|
|
500
|
+
#
|
|
501
|
+
# @param options [Hash] Command options
|
|
502
|
+
# @return [Integer] Exit code
|
|
503
|
+
def create_branch_worktree(options)
|
|
504
|
+
@options = options
|
|
505
|
+
branch_name = options[:branch]
|
|
506
|
+
puts "Creating worktree for branch: #{branch_name}..."
|
|
507
|
+
|
|
508
|
+
# Detect if remote branch
|
|
509
|
+
require_relative "../molecules/worktree_creator"
|
|
510
|
+
creator = Molecules::WorktreeCreator.new
|
|
511
|
+
remote_info = creator.send(:detect_remote_branch, branch_name)
|
|
512
|
+
|
|
513
|
+
if remote_info
|
|
514
|
+
puts "Detected remote branch: #{remote_info[:remote]}/#{remote_info[:branch]}"
|
|
515
|
+
puts "Will create with tracking..."
|
|
516
|
+
else
|
|
517
|
+
puts "Creating worktree from local branch..."
|
|
518
|
+
end
|
|
519
|
+
puts
|
|
520
|
+
|
|
521
|
+
# Prepare creation options
|
|
522
|
+
creation_options = {
|
|
523
|
+
path: options[:path],
|
|
524
|
+
dry_run: options[:dry_run],
|
|
525
|
+
no_mise_trust: options[:no_mise_trust],
|
|
526
|
+
force: options[:force]
|
|
527
|
+
}.compact
|
|
528
|
+
|
|
529
|
+
# Create the worktree
|
|
530
|
+
result = @manager.create_branch(branch_name, creation_options)
|
|
531
|
+
|
|
532
|
+
if result[:success]
|
|
533
|
+
display_branch_creation_result(result, options[:dry_run])
|
|
534
|
+
0
|
|
535
|
+
else
|
|
536
|
+
puts "Failed to create worktree: #{result[:error]}"
|
|
537
|
+
display_warnings(result[:warnings]) if result[:warnings]
|
|
538
|
+
|
|
539
|
+
# Provide helpful guidance
|
|
540
|
+
if result[:error]&.include?("not found")
|
|
541
|
+
puts
|
|
542
|
+
puts "Branch not found suggestions:"
|
|
543
|
+
if remote_info
|
|
544
|
+
puts " 1. Fetch the remote: git fetch #{remote_info[:remote]}"
|
|
545
|
+
puts " 2. List remote branches: git branch -r"
|
|
546
|
+
puts " 3. Verify branch name: git ls-remote #{remote_info[:remote]}"
|
|
547
|
+
else
|
|
548
|
+
puts " 1. List local branches: git branch"
|
|
549
|
+
puts " 2. Create the branch: git branch #{branch_name}"
|
|
550
|
+
puts " 3. Or use a remote branch: -b origin/#{branch_name}"
|
|
551
|
+
end
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
1
|
|
555
|
+
end
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
# Check if task dependencies are available with helpful messages
|
|
559
|
+
#
|
|
560
|
+
# @return [Hash] { available: boolean, message: string }
|
|
561
|
+
def check_task_dependency_availability
|
|
562
|
+
require_relative "../molecules/task_fetcher"
|
|
563
|
+
fetcher = Ace::Git::Worktree::Molecules::TaskFetcher.new
|
|
564
|
+
|
|
565
|
+
if fetcher.ace_task_available?
|
|
566
|
+
{available: true, message: "ace-task is available"}
|
|
567
|
+
else
|
|
568
|
+
{available: false, message: "ace-task is not available. Install with: gem install ace-task"}
|
|
569
|
+
end
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
# Create a traditional worktree
|
|
573
|
+
#
|
|
574
|
+
# @param options [Hash] Command options
|
|
575
|
+
# @return [Integer] Exit code
|
|
576
|
+
def create_traditional_worktree(options)
|
|
577
|
+
@options = options
|
|
578
|
+
puts "Creating worktree for branch: #{options[:branch_name]}"
|
|
579
|
+
|
|
580
|
+
# Prepare creation options
|
|
581
|
+
creation_options = {
|
|
582
|
+
path: options[:path],
|
|
583
|
+
source: options[:source],
|
|
584
|
+
dry_run: options[:dry_run],
|
|
585
|
+
no_mise_trust: options[:no_mise_trust],
|
|
586
|
+
force: options[:force]
|
|
587
|
+
}.compact
|
|
588
|
+
|
|
589
|
+
# Create the worktree
|
|
590
|
+
result = @manager.create(options[:branch_name], creation_options)
|
|
591
|
+
|
|
592
|
+
if result[:success]
|
|
593
|
+
display_traditional_creation_result(result, options[:dry_run])
|
|
594
|
+
0
|
|
595
|
+
else
|
|
596
|
+
puts "Failed to create worktree: #{result[:error]}"
|
|
597
|
+
1
|
|
598
|
+
end
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
# Display task worktree creation result
|
|
602
|
+
#
|
|
603
|
+
# @param result [Hash] Creation result
|
|
604
|
+
# @param dry_run [Boolean] Whether this was a dry run
|
|
605
|
+
def display_task_creation_result(result, dry_run = false)
|
|
606
|
+
if dry_run
|
|
607
|
+
puts "\nDry run - no changes made:"
|
|
608
|
+
puts "Would create worktree at: #{result[:would_create][:worktree_path]}"
|
|
609
|
+
puts "Would create branch: #{result[:would_create][:branch]}"
|
|
610
|
+
puts "Target branch: #{result[:would_create][:target_branch]}" if result[:would_create][:target_branch]
|
|
611
|
+
puts "Task ID: #{result[:task_id]}"
|
|
612
|
+
puts "Task title: #{result[:task_title]}"
|
|
613
|
+
if result[:would_create][:task_push]
|
|
614
|
+
puts "Would push to: #{result[:would_create][:push_remote]}"
|
|
615
|
+
end
|
|
616
|
+
if result[:would_create][:upstream_push]
|
|
617
|
+
puts "Would setup upstream: #{result[:would_create][:push_remote]}/#{result[:would_create][:branch]}"
|
|
618
|
+
end
|
|
619
|
+
if result[:would_create][:create_pr]
|
|
620
|
+
puts "Would create draft PR: #{result[:would_create][:pr_title]}"
|
|
621
|
+
end
|
|
622
|
+
puts "\nPlanned steps:"
|
|
623
|
+
result[:steps_planned].each_with_index do |step, i|
|
|
624
|
+
puts " #{i + 1}. #{step.tr("_", " ")}"
|
|
625
|
+
end
|
|
626
|
+
else
|
|
627
|
+
puts "\nWorktree created successfully!"
|
|
628
|
+
puts "Task ID: #{result[:task_id]}"
|
|
629
|
+
puts "Task title: #{result[:task_title]}" if result[:task_title]
|
|
630
|
+
puts "Worktree path: #{result[:worktree_path]}"
|
|
631
|
+
puts "Branch: #{result[:branch]}"
|
|
632
|
+
puts "Directory: #{result[:directory_name]}" if result[:directory_name]
|
|
633
|
+
puts "Start point: #{result[:start_point]}" if result[:start_point]
|
|
634
|
+
puts "Pushed to: #{result[:pushed_to]}" if result[:pushed_to]
|
|
635
|
+
|
|
636
|
+
# Display PR info if created
|
|
637
|
+
if result[:pr_number]
|
|
638
|
+
existing_label = result[:pr_existing] ? " (existing)" : ""
|
|
639
|
+
puts "PR: ##{result[:pr_number]}#{existing_label} - #{result[:pr_url]}"
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
puts "\nSteps completed:"
|
|
643
|
+
result[:steps_completed].each_with_index do |step, i|
|
|
644
|
+
puts " ✓ #{step.tr("_", " ")}"
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
# Display hooks results if available
|
|
648
|
+
if result[:hooks_results] && result[:hooks_results].any?
|
|
649
|
+
puts "\nHooks executed:"
|
|
650
|
+
result[:hooks_results].each do |hook_result|
|
|
651
|
+
status = hook_result[:success] ? "✓" : "✗"
|
|
652
|
+
command = (hook_result[:command].length > 60) ? "#{hook_result[:command][0..57]}..." : hook_result[:command]
|
|
653
|
+
puts " #{status} #{command}"
|
|
654
|
+
unless hook_result[:success]
|
|
655
|
+
puts " Error: #{hook_result[:error]}" if hook_result[:error]
|
|
656
|
+
end
|
|
657
|
+
end
|
|
658
|
+
end
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
display_warnings(result[:warnings]) if result[:warnings]
|
|
662
|
+
|
|
663
|
+
# Display cd command or launch tmux
|
|
664
|
+
unless dry_run
|
|
665
|
+
puts ""
|
|
666
|
+
launch_tmux_or_display_cd(result[:worktree_path])
|
|
667
|
+
end
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
# Display PR worktree creation result
|
|
671
|
+
#
|
|
672
|
+
# @param result [Hash] Creation result
|
|
673
|
+
# @param dry_run [Boolean] Whether this was a dry run
|
|
674
|
+
def display_pr_creation_result(result, dry_run = false)
|
|
675
|
+
if dry_run
|
|
676
|
+
puts "\nDry run - no changes made:"
|
|
677
|
+
puts "Would create worktree at: #{result[:would_create][:worktree_path]}"
|
|
678
|
+
puts "Would create branch: #{result[:would_create][:branch]}"
|
|
679
|
+
puts "Would track: #{result[:would_create][:tracking]}"
|
|
680
|
+
puts "PR: ##{result[:pr_number]} - #{result[:pr_title]}"
|
|
681
|
+
else
|
|
682
|
+
puts "\nWorktree created successfully!"
|
|
683
|
+
puts "✓ PR ##{result[:pr_number]}: #{result[:pr_title]}" if result[:pr_title]
|
|
684
|
+
puts "✓ Remote branch: #{result[:tracking]}" if result[:tracking]
|
|
685
|
+
puts "✓ Created worktree: #{result[:directory_name]}" if result[:directory_name]
|
|
686
|
+
puts "✓ Branch: #{result[:branch]} tracking #{result[:tracking]}"
|
|
687
|
+
puts "✓ Location: #{result[:worktree_path]}"
|
|
688
|
+
puts
|
|
689
|
+
|
|
690
|
+
# Display hooks results if available
|
|
691
|
+
if result[:hooks_results] && result[:hooks_results].any?
|
|
692
|
+
puts "Hooks executed:"
|
|
693
|
+
result[:hooks_results].each do |hook_result|
|
|
694
|
+
status = hook_result[:success] ? "✓" : "✗"
|
|
695
|
+
command = (hook_result[:command].length > 60) ? "#{hook_result[:command][0..57]}..." : hook_result[:command]
|
|
696
|
+
puts " #{status} #{command}"
|
|
697
|
+
unless hook_result[:success]
|
|
698
|
+
puts " Error: #{hook_result[:error]}" if hook_result[:error]
|
|
699
|
+
end
|
|
700
|
+
end
|
|
701
|
+
puts
|
|
702
|
+
end
|
|
703
|
+
end
|
|
704
|
+
|
|
705
|
+
display_warnings(result[:warnings]) if result[:warnings]
|
|
706
|
+
|
|
707
|
+
# Display cd command or launch tmux
|
|
708
|
+
unless dry_run
|
|
709
|
+
launch_tmux_or_display_cd(result[:worktree_path])
|
|
710
|
+
end
|
|
711
|
+
end
|
|
712
|
+
|
|
713
|
+
# Display branch worktree creation result
|
|
714
|
+
#
|
|
715
|
+
# @param result [Hash] Creation result
|
|
716
|
+
# @param dry_run [Boolean] Whether this was a dry run
|
|
717
|
+
def display_branch_creation_result(result, dry_run = false)
|
|
718
|
+
if dry_run
|
|
719
|
+
puts "\nDry run - no changes made:"
|
|
720
|
+
puts "Would create worktree at: #{result[:would_create][:worktree_path]}"
|
|
721
|
+
puts "Would create branch: #{result[:would_create][:branch]}"
|
|
722
|
+
if result[:would_create][:tracking]
|
|
723
|
+
puts "Would track: #{result[:would_create][:tracking]}"
|
|
724
|
+
else
|
|
725
|
+
puts "Local branch (no tracking)"
|
|
726
|
+
end
|
|
727
|
+
else
|
|
728
|
+
puts "\nWorktree created successfully!"
|
|
729
|
+
puts "✓ Created worktree: #{result[:directory_name]}" if result[:directory_name]
|
|
730
|
+
if result[:tracking]
|
|
731
|
+
puts "✓ Branch: #{result[:branch]} tracking #{result[:tracking]}"
|
|
732
|
+
else
|
|
733
|
+
puts "✓ Branch: #{result[:branch]} (local, no tracking)"
|
|
734
|
+
end
|
|
735
|
+
puts "✓ Location: #{result[:worktree_path]}"
|
|
736
|
+
puts
|
|
737
|
+
|
|
738
|
+
# Display hooks results if available
|
|
739
|
+
if result[:hooks_results] && result[:hooks_results].any?
|
|
740
|
+
puts "Hooks executed:"
|
|
741
|
+
result[:hooks_results].each do |hook_result|
|
|
742
|
+
status = hook_result[:success] ? "✓" : "✗"
|
|
743
|
+
command = (hook_result[:command].length > 60) ? "#{hook_result[:command][0..57]}..." : hook_result[:command]
|
|
744
|
+
puts " #{status} #{command}"
|
|
745
|
+
unless hook_result[:success]
|
|
746
|
+
puts " Error: #{hook_result[:error]}" if hook_result[:error]
|
|
747
|
+
end
|
|
748
|
+
end
|
|
749
|
+
puts
|
|
750
|
+
end
|
|
751
|
+
end
|
|
752
|
+
|
|
753
|
+
display_warnings(result[:warnings]) if result[:warnings]
|
|
754
|
+
|
|
755
|
+
# Display cd command or launch tmux
|
|
756
|
+
unless dry_run
|
|
757
|
+
launch_tmux_or_display_cd(result[:worktree_path])
|
|
758
|
+
end
|
|
759
|
+
end
|
|
760
|
+
|
|
761
|
+
# Display traditional worktree creation result
|
|
762
|
+
#
|
|
763
|
+
# @param result [Hash] Creation result
|
|
764
|
+
# @param dry_run [Boolean] Whether this was a dry run
|
|
765
|
+
def display_traditional_creation_result(result, dry_run = false)
|
|
766
|
+
if dry_run
|
|
767
|
+
puts "\nDry run - no changes made:"
|
|
768
|
+
puts "Would create worktree at: #{result[:would_create][:worktree_path]}"
|
|
769
|
+
puts "Would create branch: #{result[:would_create][:branch]}"
|
|
770
|
+
puts "Branch exists: #{result[:would_create][:branch_exists]}"
|
|
771
|
+
puts "Source: #{result[:would_create][:source]}"
|
|
772
|
+
else
|
|
773
|
+
puts "\nWorktree created successfully!"
|
|
774
|
+
puts "Worktree path: #{result[:worktree_path]}"
|
|
775
|
+
puts "Branch: #{result[:branch]}"
|
|
776
|
+
puts "Git root: #{result[:git_root]}"
|
|
777
|
+
|
|
778
|
+
display_warnings(result[:warnings]) if result[:warnings]
|
|
779
|
+
puts ""
|
|
780
|
+
launch_tmux_or_display_cd(result[:worktree_path])
|
|
781
|
+
end
|
|
782
|
+
end
|
|
783
|
+
|
|
784
|
+
# Display warnings
|
|
785
|
+
#
|
|
786
|
+
# @param warnings [Array<String>] Array of warning messages
|
|
787
|
+
def display_warnings(warnings)
|
|
788
|
+
return unless warnings&.any?
|
|
789
|
+
|
|
790
|
+
puts "\nWarnings:"
|
|
791
|
+
warnings.each { |warning| puts " ⚠️ #{warning}" }
|
|
792
|
+
end
|
|
793
|
+
|
|
794
|
+
# Check if auto-navigation should be performed
|
|
795
|
+
#
|
|
796
|
+
# @return [Boolean] true if auto-navigation should be performed
|
|
797
|
+
def should_auto_navigate?
|
|
798
|
+
# Check CLI flag first
|
|
799
|
+
return false if @options[:no_auto_navigate]
|
|
800
|
+
|
|
801
|
+
# Then check configuration by loading it directly
|
|
802
|
+
begin
|
|
803
|
+
require_relative "../molecules/config_loader"
|
|
804
|
+
config_loader = Ace::Git::Worktree::Molecules::ConfigLoader.new
|
|
805
|
+
config = config_loader.load
|
|
806
|
+
return false unless config
|
|
807
|
+
|
|
808
|
+
config.auto_navigate?
|
|
809
|
+
rescue
|
|
810
|
+
# If configuration loading fails, default to no auto-navigation
|
|
811
|
+
false
|
|
812
|
+
end
|
|
813
|
+
end
|
|
814
|
+
|
|
815
|
+
# Launch tmux session or display cd hint for navigation
|
|
816
|
+
#
|
|
817
|
+
# When tmux config is enabled and ace-tmux is available, launches a tmux
|
|
818
|
+
# session rooted at the worktree path (replaces current process).
|
|
819
|
+
# Otherwise falls back to displaying the cd command.
|
|
820
|
+
#
|
|
821
|
+
# @param worktree_path [String] Path to the worktree
|
|
822
|
+
def launch_tmux_or_display_cd(worktree_path)
|
|
823
|
+
return unless worktree_path
|
|
824
|
+
|
|
825
|
+
if tmux_enabled?
|
|
826
|
+
if ace_tmux_available?
|
|
827
|
+
Kernel.exec("ace-tmux", "--root", worktree_path)
|
|
828
|
+
else
|
|
829
|
+
puts "Warning: tmux is enabled in config but ace-tmux is not installed."
|
|
830
|
+
puts "Install ace-tmux or disable tmux in .ace/git/worktree.yml"
|
|
831
|
+
puts "cd #{worktree_path}"
|
|
832
|
+
end
|
|
833
|
+
else
|
|
834
|
+
puts "cd #{worktree_path}"
|
|
835
|
+
end
|
|
836
|
+
end
|
|
837
|
+
|
|
838
|
+
# Check if tmux integration is enabled in config
|
|
839
|
+
#
|
|
840
|
+
# @return [Boolean] true if tmux is enabled
|
|
841
|
+
def tmux_enabled?
|
|
842
|
+
require_relative "../molecules/config_loader"
|
|
843
|
+
config_loader = Ace::Git::Worktree::Molecules::ConfigLoader.new
|
|
844
|
+
config = config_loader.load
|
|
845
|
+
return false unless config
|
|
846
|
+
|
|
847
|
+
config.tmux?
|
|
848
|
+
rescue
|
|
849
|
+
false
|
|
850
|
+
end
|
|
851
|
+
|
|
852
|
+
# Check if ace-tmux binary is available on PATH
|
|
853
|
+
#
|
|
854
|
+
# @return [Boolean] true if ace-tmux is installed
|
|
855
|
+
def ace_tmux_available?
|
|
856
|
+
system("which ace-tmux > /dev/null 2>&1")
|
|
857
|
+
end
|
|
858
|
+
|
|
859
|
+
# Convert PR metadata from gh CLI to internal pr_data format
|
|
860
|
+
#
|
|
861
|
+
# Anti-corruption layer that translates gh CLI JSON output to internal format.
|
|
862
|
+
# This isolates worktree creation from gh CLI output structure changes.
|
|
863
|
+
#
|
|
864
|
+
# Expected metadata schema from ace-git PrMetadataFetcher (via gh pr view --json):
|
|
865
|
+
# {
|
|
866
|
+
# "number" => Integer,
|
|
867
|
+
# "title" => String,
|
|
868
|
+
# "headRefName" => String (PR source branch),
|
|
869
|
+
# "baseRefName" => String (PR target branch),
|
|
870
|
+
# "isCrossRepository" => Boolean (true for fork PRs),
|
|
871
|
+
# "headRepositoryOwner" => { "login" => String } (fork owner info)
|
|
872
|
+
# }
|
|
873
|
+
#
|
|
874
|
+
# @param metadata [Hash] PR metadata from gh CLI
|
|
875
|
+
# @return [Hash] pr_data format expected by worktree creator
|
|
876
|
+
def pr_data_from_metadata(metadata)
|
|
877
|
+
{
|
|
878
|
+
number: metadata["number"],
|
|
879
|
+
title: metadata["title"],
|
|
880
|
+
head_branch: metadata["headRefName"],
|
|
881
|
+
base_branch: metadata["baseRefName"],
|
|
882
|
+
is_cross_repository: metadata["isCrossRepository"] || false,
|
|
883
|
+
head_repository_owner: metadata.dig("headRepositoryOwner", "login") || "unknown"
|
|
884
|
+
}
|
|
885
|
+
end
|
|
886
|
+
|
|
887
|
+
# Handle errors during PR metadata fetch
|
|
888
|
+
#
|
|
889
|
+
# @param error [Exception] The error that occurred
|
|
890
|
+
# @param pr_number [Integer] PR number for context in error messages
|
|
891
|
+
# @return [Integer] Exit code (always 1 for errors)
|
|
892
|
+
def handle_pr_fetch_error(error, pr_number)
|
|
893
|
+
case error
|
|
894
|
+
when Ace::Git::PrNotFoundError
|
|
895
|
+
handle_pr_not_found(error, pr_number)
|
|
896
|
+
when Ace::Git::GhAuthenticationError
|
|
897
|
+
handle_gh_auth_error(error)
|
|
898
|
+
when Ace::Git::GhNotInstalledError
|
|
899
|
+
handle_gh_not_installed(error)
|
|
900
|
+
else
|
|
901
|
+
handle_unknown_error(error)
|
|
902
|
+
end
|
|
903
|
+
1
|
|
904
|
+
end
|
|
905
|
+
|
|
906
|
+
# Handle PR not found error with helpful suggestions
|
|
907
|
+
def handle_pr_not_found(error, pr_number)
|
|
908
|
+
puts "Error: #{error.message}"
|
|
909
|
+
puts
|
|
910
|
+
puts "Suggestions:"
|
|
911
|
+
puts " 1. Verify the PR number is correct"
|
|
912
|
+
puts " 2. Check if the PR exists: gh pr view #{pr_number}"
|
|
913
|
+
puts " 3. Ensure you're in the correct repository"
|
|
914
|
+
end
|
|
915
|
+
|
|
916
|
+
# Handle GitHub authentication error with troubleshooting steps
|
|
917
|
+
def handle_gh_auth_error(error)
|
|
918
|
+
puts "Error: #{error.message}"
|
|
919
|
+
puts
|
|
920
|
+
puts "Troubleshooting:"
|
|
921
|
+
puts " 1. Verify GitHub authentication: gh auth status"
|
|
922
|
+
puts " 2. Re-authenticate if needed: gh auth login"
|
|
923
|
+
puts " 3. Check repository access permissions"
|
|
924
|
+
end
|
|
925
|
+
|
|
926
|
+
# Handle gh CLI not installed error with installation guidance
|
|
927
|
+
def handle_gh_not_installed(error)
|
|
928
|
+
puts "Error: #{error.message}"
|
|
929
|
+
puts
|
|
930
|
+
puts gh_not_available_message
|
|
931
|
+
end
|
|
932
|
+
|
|
933
|
+
# Handle unknown/unexpected errors with debug info
|
|
934
|
+
def handle_unknown_error(error)
|
|
935
|
+
puts "Error: #{error.message}"
|
|
936
|
+
if ENV["DEBUG"]
|
|
937
|
+
puts "Debug: #{error.class}"
|
|
938
|
+
puts "Debug: #{error.backtrace&.first}" if error.backtrace
|
|
939
|
+
end
|
|
940
|
+
end
|
|
941
|
+
|
|
942
|
+
# Get helpful error message when gh CLI is unavailable
|
|
943
|
+
#
|
|
944
|
+
# @return [String] User-friendly error message with installation guidance
|
|
945
|
+
def gh_not_available_message
|
|
946
|
+
<<~MESSAGE
|
|
947
|
+
gh CLI is required for PR worktrees but is not installed.
|
|
948
|
+
|
|
949
|
+
Install gh CLI:
|
|
950
|
+
- macOS: brew install gh
|
|
951
|
+
- Linux: See https://github.com/cli/cli#installation
|
|
952
|
+
- Windows: See https://github.com/cli/cli#installation
|
|
953
|
+
|
|
954
|
+
After installation, authenticate with: gh auth login
|
|
955
|
+
MESSAGE
|
|
956
|
+
end
|
|
957
|
+
end
|
|
958
|
+
end
|
|
959
|
+
end
|
|
960
|
+
end
|
|
961
|
+
end
|