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.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/.ace-defaults/git/worktree.yml +250 -0
  3. data/.ace-defaults/nav/protocols/wfi-sources/ace-git-worktree.yml +19 -0
  4. data/CHANGELOG.md +957 -0
  5. data/LICENSE +21 -0
  6. data/README.md +40 -0
  7. data/Rakefile +14 -0
  8. data/docs/demo/ace-git-worktree-getting-started.gif +0 -0
  9. data/docs/demo/ace-git-worktree-getting-started.tape.yml +28 -0
  10. data/docs/demo/fixtures/README.md +3 -0
  11. data/docs/demo/fixtures/sample.txt +1 -0
  12. data/docs/getting-started.md +114 -0
  13. data/docs/handbook.md +38 -0
  14. data/docs/usage.md +334 -0
  15. data/exe/ace-git-worktree +24 -0
  16. data/handbook/agents/worktree.ag.md +189 -0
  17. data/handbook/skills/as-git-worktree/SKILL.md +27 -0
  18. data/handbook/skills/as-git-worktree-create/SKILL.md +21 -0
  19. data/handbook/skills/as-git-worktree-manage/SKILL.md +20 -0
  20. data/handbook/workflow-instructions/git/worktree-create.wf.md +262 -0
  21. data/handbook/workflow-instructions/git/worktree-manage.wf.md +384 -0
  22. data/handbook/workflow-instructions/git/worktree.wf.md +224 -0
  23. data/lib/ace/git/worktree/atoms/git_command.rb +121 -0
  24. data/lib/ace/git/worktree/atoms/path_expander.rb +189 -0
  25. data/lib/ace/git/worktree/atoms/slug_generator.rb +235 -0
  26. data/lib/ace/git/worktree/atoms/task_id_extractor.rb +91 -0
  27. data/lib/ace/git/worktree/cli/commands/config.rb +50 -0
  28. data/lib/ace/git/worktree/cli/commands/create.rb +80 -0
  29. data/lib/ace/git/worktree/cli/commands/list.rb +76 -0
  30. data/lib/ace/git/worktree/cli/commands/prune.rb +43 -0
  31. data/lib/ace/git/worktree/cli/commands/remove.rb +48 -0
  32. data/lib/ace/git/worktree/cli/commands/shared_helpers.rb +66 -0
  33. data/lib/ace/git/worktree/cli/commands/switch.rb +44 -0
  34. data/lib/ace/git/worktree/cli.rb +103 -0
  35. data/lib/ace/git/worktree/commands/config_command.rb +351 -0
  36. data/lib/ace/git/worktree/commands/create_command.rb +961 -0
  37. data/lib/ace/git/worktree/commands/list_command.rb +247 -0
  38. data/lib/ace/git/worktree/commands/prune_command.rb +260 -0
  39. data/lib/ace/git/worktree/commands/remove_command.rb +522 -0
  40. data/lib/ace/git/worktree/commands/switch_command.rb +249 -0
  41. data/lib/ace/git/worktree/configuration.rb +167 -0
  42. data/lib/ace/git/worktree/models/worktree_config.rb +502 -0
  43. data/lib/ace/git/worktree/models/worktree_info.rb +303 -0
  44. data/lib/ace/git/worktree/models/worktree_metadata.rb +294 -0
  45. data/lib/ace/git/worktree/molecules/config_loader.rb +125 -0
  46. data/lib/ace/git/worktree/molecules/current_task_linker.rb +136 -0
  47. data/lib/ace/git/worktree/molecules/hook_executor.rb +361 -0
  48. data/lib/ace/git/worktree/molecules/parent_task_resolver.rb +186 -0
  49. data/lib/ace/git/worktree/molecules/pr_creator.rb +253 -0
  50. data/lib/ace/git/worktree/molecules/task_committer.rb +329 -0
  51. data/lib/ace/git/worktree/molecules/task_fetcher.rb +244 -0
  52. data/lib/ace/git/worktree/molecules/task_pusher.rb +183 -0
  53. data/lib/ace/git/worktree/molecules/task_status_updater.rb +447 -0
  54. data/lib/ace/git/worktree/molecules/worktree_creator.rb +832 -0
  55. data/lib/ace/git/worktree/molecules/worktree_lister.rb +337 -0
  56. data/lib/ace/git/worktree/molecules/worktree_remover.rb +416 -0
  57. data/lib/ace/git/worktree/organisms/task_worktree_orchestrator.rb +906 -0
  58. data/lib/ace/git/worktree/organisms/worktree_manager.rb +714 -0
  59. data/lib/ace/git/worktree/version.rb +9 -0
  60. data/lib/ace/git/worktree.rb +215 -0
  61. metadata +218 -0
@@ -0,0 +1,714 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../atoms/task_id_extractor"
4
+
5
+ module Ace
6
+ module Git
7
+ module Worktree
8
+ module Organisms
9
+ # Worktree manager organism
10
+ #
11
+ # High-level manager for all worktree operations, providing a unified interface
12
+ # for creating, listing, switching between, removing, and managing worktrees.
13
+ # Integrates both task-aware and traditional worktree operations.
14
+ #
15
+ # @example Create a worktree
16
+ # manager = WorktreeManager.new
17
+ # result = manager.create("feature-branch")
18
+ #
19
+ # @example Create a task-aware worktree
20
+ # result = manager.create_task("081")
21
+ #
22
+ # @example List all worktrees
23
+ # worktrees = manager.list_all
24
+ class WorktreeManager
25
+ # Initialize a new WorktreeManager
26
+ #
27
+ # @param config [WorktreeConfig, nil] Worktree configuration (loaded if nil)
28
+ # @param project_root [String] Project root directory
29
+ def initialize(config: nil, project_root: Dir.pwd)
30
+ @project_root = project_root
31
+
32
+ # Initialize molecules
33
+ @config_loader = Molecules::ConfigLoader.new(project_root)
34
+ @config = config || load_configuration
35
+ @worktree_creator = Molecules::WorktreeCreator.new(config: @config)
36
+ @worktree_lister = Molecules::WorktreeLister.new
37
+ @worktree_remover = Molecules::WorktreeRemover.new
38
+ @task_fetcher = Molecules::TaskFetcher.new
39
+
40
+ # Initialize orchestrator
41
+ @task_orchestrator = Organisms::TaskWorktreeOrchestrator.new(config: @config, project_root: project_root)
42
+ end
43
+
44
+ # Create a traditional worktree (not task-aware)
45
+ #
46
+ # @param branch_name [String] Branch name
47
+ # @param options [Hash] Options for creation
48
+ # @return [Hash] Creation result
49
+ #
50
+ # @example
51
+ # manager = WorktreeManager.new
52
+ # result = manager.create("feature-branch", path: "/tmp/worktree")
53
+ # # => { success: true, worktree_path: "/tmp/worktree", branch: "feature-branch" }
54
+ def create(branch_name, options = {})
55
+ # Validate inputs
56
+ return error_result("Branch name is required") if branch_name.nil? || branch_name.empty?
57
+
58
+ # Check if worktree already exists
59
+ existing = @worktree_lister.find_by_branch(branch_name)
60
+ if existing
61
+ return error_result("Worktree already exists for branch: #{branch_name}")
62
+ end
63
+
64
+ # Handle dry run
65
+ if options[:dry_run]
66
+ return dry_run_traditional_creation(branch_name, options)
67
+ end
68
+
69
+ # Create the worktree
70
+ result = @worktree_creator.create_traditional(
71
+ branch_name,
72
+ options[:path],
73
+ git_root: @project_root,
74
+ source: options[:source]
75
+ )
76
+
77
+ if result[:success]
78
+ result[:message] = "Worktree created successfully"
79
+
80
+ # Execute after-create hooks if configured
81
+ hooks = @config.after_create_hooks
82
+ if hooks && hooks.any?
83
+ require_relative "../molecules/hook_executor"
84
+ hook_executor = Molecules::HookExecutor.new
85
+ hook_result = hook_executor.execute_hooks(
86
+ hooks,
87
+ worktree_path: result[:worktree_path],
88
+ project_root: @project_root,
89
+ task_data: nil # No task data for traditional worktrees
90
+ )
91
+
92
+ if hook_result[:success]
93
+ result[:hooks_results] = hook_result[:results]
94
+ else
95
+ # Hooks are non-blocking - failures become warnings
96
+ result[:warnings] = hook_result[:errors] if hook_result[:errors]&.any?
97
+ result[:hooks_results] = hook_result[:results]
98
+ end
99
+ end
100
+ end
101
+
102
+ result
103
+ rescue => e
104
+ error_result("Unexpected error: #{e.message}")
105
+ end
106
+
107
+ # Create a task-aware worktree
108
+ #
109
+ # @param task_ref [String] Task reference
110
+ # @param options [Hash] Options for creation
111
+ # @return [Hash] Creation result
112
+ #
113
+ # @example
114
+ # result = manager.create_task("081")
115
+ # result = manager.create_task("081", dry_run: true)
116
+ def create_task(task_ref, options = {})
117
+ if options[:dry_run]
118
+ @task_orchestrator.dry_run_create(task_ref, options)
119
+ else
120
+ @task_orchestrator.create_for_task(task_ref, options)
121
+ end
122
+ end
123
+
124
+ # Create a worktree for a Pull Request
125
+ #
126
+ # @param pr_number [Integer] PR number
127
+ # @param pr_data [Hash] PR data from Ace::Git::Molecules::PrMetadataFetcher
128
+ # @param options [Hash] Options for creation
129
+ # @return [Hash] Creation result
130
+ #
131
+ # @example
132
+ # manager = WorktreeManager.new
133
+ # pr_data = { number: 26, title: "Add feature", head_branch: "feature/auth" }
134
+ # result = manager.create_pr(26, pr_data)
135
+ def create_pr(pr_number, pr_data, options = {})
136
+ return error_result("PR number is required") if pr_number.nil?
137
+ return error_result("PR data is required") if pr_data.nil?
138
+
139
+ # Check if worktree already exists for this PR's branch
140
+ head_branch = pr_data[:head_branch]
141
+ existing = @worktree_lister.find_by_branch("pr-#{pr_number}") ||
142
+ @worktree_lister.find_by_branch(head_branch)
143
+
144
+ if existing && !options[:force]
145
+ return error_result("Worktree already exists at: #{existing.path}")
146
+ end
147
+
148
+ # Handle dry run
149
+ if options[:dry_run]
150
+ return dry_run_pr_creation(pr_number, pr_data, options)
151
+ end
152
+
153
+ # Create the worktree
154
+ result = @worktree_creator.create_for_pr(
155
+ pr_data,
156
+ @config,
157
+ git_root: @project_root
158
+ )
159
+
160
+ if result[:success]
161
+ result[:pr_number] = pr_number
162
+ result[:pr_title] = pr_data[:title]
163
+ result[:message] = "PR worktree created successfully"
164
+
165
+ # Execute after-create hooks if configured
166
+ hooks = @config.after_create_hooks
167
+ if hooks && hooks.any?
168
+ require_relative "../molecules/hook_executor"
169
+ hook_executor = Molecules::HookExecutor.new
170
+ hook_result = hook_executor.execute_hooks(
171
+ hooks,
172
+ worktree_path: result[:worktree_path],
173
+ project_root: @project_root,
174
+ task_data: pr_data
175
+ )
176
+
177
+ if hook_result[:success]
178
+ result[:hooks_results] = hook_result[:results]
179
+ else
180
+ result[:warnings] = hook_result[:errors] if hook_result[:errors]&.any?
181
+ result[:hooks_results] = hook_result[:results]
182
+ end
183
+ end
184
+ end
185
+
186
+ result
187
+ rescue => e
188
+ error_result("Unexpected error: #{e.message}")
189
+ end
190
+
191
+ # Create a worktree for a branch (local or remote)
192
+ #
193
+ # @param branch_name [String] Branch name (e.g., "feature" or "origin/feature")
194
+ # @param options [Hash] Options for creation
195
+ # @return [Hash] Creation result
196
+ #
197
+ # @example Remote branch
198
+ # result = manager.create_branch("origin/feature/auth")
199
+ #
200
+ # @example Local branch
201
+ # result = manager.create_branch("my-feature")
202
+ def create_branch(branch_name, options = {})
203
+ return error_result("Branch name is required") if branch_name.nil? || branch_name.empty?
204
+
205
+ # Check if worktree already exists for this branch
206
+ # Extract just the branch name (remove remote prefix if present)
207
+ local_branch_name = branch_name.include?("/") ? branch_name.split("/").last : branch_name
208
+ existing = @worktree_lister.find_by_branch(local_branch_name) ||
209
+ @worktree_lister.find_by_branch(branch_name)
210
+
211
+ if existing && !options[:force]
212
+ return error_result("Worktree already exists at: #{existing.path}")
213
+ end
214
+
215
+ # Handle dry run
216
+ if options[:dry_run]
217
+ return dry_run_branch_creation(branch_name, options)
218
+ end
219
+
220
+ # Create the worktree
221
+ result = @worktree_creator.create_for_branch(
222
+ branch_name,
223
+ @config,
224
+ git_root: @project_root
225
+ )
226
+
227
+ if result[:success]
228
+ result[:message] = "Branch worktree created successfully"
229
+
230
+ # Execute after-create hooks if configured
231
+ hooks = @config.after_create_hooks
232
+ if hooks && hooks.any?
233
+ require_relative "../molecules/hook_executor"
234
+ hook_executor = Molecules::HookExecutor.new
235
+ hook_result = hook_executor.execute_hooks(
236
+ hooks,
237
+ worktree_path: result[:worktree_path],
238
+ project_root: @project_root,
239
+ task_data: nil
240
+ )
241
+
242
+ if hook_result[:success]
243
+ result[:hooks_results] = hook_result[:results]
244
+ else
245
+ result[:warnings] = hook_result[:errors] if hook_result[:errors]&.any?
246
+ result[:hooks_results] = hook_result[:results]
247
+ end
248
+ end
249
+ end
250
+
251
+ result
252
+ rescue => e
253
+ error_result("Unexpected error: #{e.message}")
254
+ end
255
+
256
+ # List all worktrees
257
+ #
258
+ # @param options [Hash] Listing options
259
+ # @return [Hash] Listing result
260
+ #
261
+ # @example
262
+ # result = manager.list_all
263
+ # result = manager.list_all(format: :json, show_tasks: true)
264
+ #
265
+ # @option options [Symbol] :format Output format (:table, :json, :simple)
266
+ # @option options [Boolean] :show_tasks Include task associations
267
+ # @option options [Boolean] :task_associated Filter by task association
268
+ # @option options [Boolean] :usable Filter by usability
269
+ # @option options [String] :search Filter by search pattern
270
+ def list_all(options = {})
271
+ task_filter_requested = !options[:task_associated].nil?
272
+
273
+ # Get worktrees
274
+ worktrees = if options[:show_tasks] || task_filter_requested
275
+ @worktree_lister.list_with_tasks
276
+ else
277
+ @worktree_lister.list_all
278
+ end
279
+
280
+ # Apply filters
281
+ if !options[:task_associated].nil? || !options[:usable].nil? || options[:search]
282
+ worktrees = @worktree_lister.filter(
283
+ worktrees,
284
+ task_associated: options[:task_associated],
285
+ usable: options[:usable],
286
+ branch_pattern: options[:search]
287
+ )
288
+ end
289
+
290
+ # Format output
291
+ formatted_output = @worktree_lister.format_for_display(
292
+ worktrees,
293
+ options[:format] || :table
294
+ )
295
+
296
+ # Get statistics
297
+ stats = @worktree_lister.get_statistics(worktrees)
298
+
299
+ {
300
+ success: true,
301
+ worktrees: worktrees,
302
+ formatted_output: formatted_output,
303
+ statistics: stats,
304
+ count: worktrees.length
305
+ }
306
+ rescue => e
307
+ error_result("Failed to list worktrees: #{e.message}")
308
+ end
309
+
310
+ # Switch to a worktree
311
+ #
312
+ # @param identifier [String] Worktree identifier (task ID, branch name, directory, or path)
313
+ # @return [Hash] Switch result
314
+ #
315
+ # @example
316
+ # result = manager.switch("081") # By task ID
317
+ # result = manager.switch("feature-branch") # By branch name
318
+ # result = manager.switch("task.081") # By directory name
319
+ # result = manager.switch("/path/to/worktree") # By path
320
+ def switch(identifier)
321
+ return error_result("Worktree identifier is required") if identifier.nil? || identifier.empty?
322
+
323
+ # Try different ways to find the worktree
324
+ worktree = find_worktree_by_identifier(identifier)
325
+ return error_result("Worktree not found: #{identifier}") unless worktree
326
+
327
+ # Check if worktree exists and is usable
328
+ unless worktree.exists?
329
+ return error_result("Worktree directory does not exist: #{worktree.path}")
330
+ end
331
+
332
+ unless worktree.usable?
333
+ return error_result("Worktree is not usable: #{worktree.description}")
334
+ end
335
+
336
+ # Return the path for the caller to use
337
+ {
338
+ success: true,
339
+ message: "Found worktree: #{worktree.description}",
340
+ worktree_path: worktree.path,
341
+ branch: worktree.branch,
342
+ task_id: worktree.task_id,
343
+ description: worktree.description
344
+ }
345
+ rescue => e
346
+ error_result("Unexpected error: #{e.message}")
347
+ end
348
+
349
+ # Remove a worktree
350
+ #
351
+ # @param identifier [String] Worktree identifier
352
+ # @param options [Hash] Removal options
353
+ # @return [Hash] Removal result
354
+ #
355
+ # @example
356
+ # result = manager.remove("081") # By task ID
357
+ # result = manager.remove("feature-branch") # By branch name
358
+ # result = manager.remove("/path/to/worktree", force: true)
359
+ #
360
+ # @option options [Boolean] :force Force removal even with changes
361
+ # @option options [Boolean] :remove_directory Also remove the directory
362
+ # @option options [Boolean] :ignore_untracked Ignore untracked files when checking changes
363
+ def remove(identifier, options = {})
364
+ return error_result("Worktree identifier is required") if identifier.nil? || identifier.empty?
365
+
366
+ # Find the worktree
367
+ worktree = find_worktree_by_identifier(identifier)
368
+
369
+ unless worktree
370
+ # Worktree not found - check if we should try branch-only deletion
371
+ if options[:delete_branch]
372
+ result = attempt_branch_only_deletion(identifier, options[:force])
373
+ return result if result[:success]
374
+ end
375
+
376
+ return error_result("Worktree not found: #{identifier}")
377
+ end
378
+
379
+ # Remove the worktree
380
+ result = @worktree_remover.remove(
381
+ worktree.path,
382
+ force: options[:force],
383
+ remove_directory: options[:remove_directory] != false,
384
+ delete_branch: options[:delete_branch] == true,
385
+ ignore_untracked: options[:ignore_untracked] == true
386
+ )
387
+
388
+ if result[:success]
389
+ result[:message] = "Worktree removed successfully: #{worktree.description}"
390
+ end
391
+
392
+ result
393
+ rescue => e
394
+ error_result("Unexpected error: #{e.message}")
395
+ end
396
+
397
+ # Remove a task worktree with full cleanup
398
+ #
399
+ # @param task_ref [String] Task reference
400
+ # @param options [Hash] Removal options
401
+ # @return [Hash] Removal result
402
+ #
403
+ # @example
404
+ # result = manager.remove_task("081", force: true)
405
+ def remove_task(task_ref, options = {})
406
+ @task_orchestrator.remove_task_worktree(task_ref, options)
407
+ end
408
+
409
+ # Prune deleted worktrees
410
+ #
411
+ # @return [Hash] Prune result
412
+ #
413
+ # @example
414
+ # result = manager.prune
415
+ # # => { success: true, message: "Pruned 2 worktrees", pruned_count: 2 }
416
+ def prune
417
+ result = @worktree_remover.prune
418
+ result[:message] = "Worktree pruning completed successfully" if result[:success]
419
+ result
420
+ rescue => e
421
+ error_result("Failed to prune worktrees: #{e.message}")
422
+ end
423
+
424
+ # Get worktree status and statistics
425
+ #
426
+ # @return [Hash] Status information
427
+ #
428
+ # @example
429
+ # status = manager.get_status
430
+ # puts "Total worktrees: #{status[:statistics][:total]}"
431
+ def get_status
432
+ # Get all worktrees with task associations
433
+ worktrees = @worktree_lister.list_with_tasks
434
+ stats = @worktree_lister.get_statistics
435
+
436
+ # Get task worktree status
437
+ task_status = @task_orchestrator.get_task_worktree_status
438
+
439
+ result = {
440
+ success: true,
441
+ worktrees: worktrees,
442
+ statistics: stats,
443
+ configuration: @config.to_h
444
+ }
445
+ result[:task_status] = task_status[:status] if task_status[:success]
446
+ result
447
+ rescue => e
448
+ error_result("Failed to get worktree status: #{e.message}")
449
+ end
450
+
451
+ # Search for worktrees
452
+ #
453
+ # @param query [String] Search query
454
+ # @param options [Hash] Search options
455
+ # @return [Hash] Search result
456
+ #
457
+ # @example
458
+ # result = manager.search("auth", search_in: [:branch, :task_id])
459
+ def search(query, options = {})
460
+ return error_result("Search query is required") if query.nil? || query.empty?
461
+
462
+ search_in = options[:search_in] || [:branch, :path, :task_id]
463
+ worktrees = @worktree_lister.search(query, search_in: search_in)
464
+
465
+ {
466
+ success: true,
467
+ query: query,
468
+ search_in: search_in,
469
+ results: worktrees,
470
+ count: worktrees.length
471
+ }
472
+ rescue => e
473
+ error_result("Search failed: #{e.message}")
474
+ end
475
+
476
+ # Validate worktree configuration
477
+ #
478
+ # @return [Hash] Validation result
479
+ #
480
+ # @example
481
+ # validation = manager.validate_configuration
482
+ # if validation[:valid]
483
+ # puts "Configuration is valid"
484
+ # else
485
+ # puts "Errors: #{validation[:errors].join(', ')}"
486
+ # end
487
+ def validate_configuration
488
+ errors = @config.validate
489
+
490
+ {
491
+ success: errors.empty?,
492
+ valid: errors.empty?,
493
+ errors: errors,
494
+ configuration: @config.to_h
495
+ }
496
+ rescue => e
497
+ error_result("Configuration validation failed: #{e.message}")
498
+ end
499
+
500
+ # Get configuration
501
+ #
502
+ # @return [WorktreeConfig] Current configuration
503
+ def configuration
504
+ @config
505
+ end
506
+
507
+ # Reload configuration
508
+ #
509
+ # @return [WorktreeConfig] Reloaded configuration
510
+ def reload_configuration
511
+ @config = load_configuration
512
+ @config_loader.reset_cache!
513
+ @task_orchestrator = Organisms::TaskWorktreeOrchestrator.new(config: @config, project_root: @project_root)
514
+ @config
515
+ end
516
+
517
+ private
518
+
519
+ # Load configuration
520
+ #
521
+ # @return [WorktreeConfig] Loaded configuration
522
+ def load_configuration
523
+ @config_loader.load
524
+ end
525
+
526
+ # Find worktree by various identifiers
527
+ #
528
+ # @param identifier [String] Worktree identifier
529
+ # @return [WorktreeInfo, nil] Worktree info or nil
530
+ def find_worktree_by_identifier(identifier)
531
+ # Try as task ID first (handles subtasks like "121.01")
532
+ normalized_task_id = Atoms::TaskIDExtractor.normalize(identifier)
533
+ if normalized_task_id
534
+ worktree = @worktree_lister.find_by_task_id(normalized_task_id)
535
+ return worktree if worktree
536
+ end
537
+
538
+ # Try as branch name
539
+ worktree = @worktree_lister.find_by_branch(identifier)
540
+ return worktree if worktree
541
+
542
+ # Try as directory name
543
+ worktree = @worktree_lister.find_by_directory(identifier)
544
+ return worktree if worktree
545
+
546
+ # Try as path
547
+ worktree = @worktree_lister.find_by_path(identifier)
548
+ return worktree if worktree
549
+
550
+ nil
551
+ end
552
+
553
+ # Create error result
554
+ #
555
+ # Dry run PR worktree creation
556
+ #
557
+ # @param pr_number [Integer] PR number
558
+ # @param pr_data [Hash] PR data
559
+ # @param options [Hash] Options
560
+ # @return [Hash] Dry run result
561
+ def dry_run_pr_creation(pr_number, pr_data, options)
562
+ # Simulate what would be created
563
+ pr_config = @config.pr_config || {}
564
+ directory_format = pr_config[:directory_format] || "ace-pr-{number}"
565
+ branch_format = pr_config[:branch_format] || "pr-{number}-{slug}"
566
+ remote_name = pr_config[:remote_name] || "origin"
567
+
568
+ # Use the format_pr_name logic for proper variable substitution
569
+ require_relative "../atoms/slug_generator"
570
+
571
+ directory_name = directory_format.dup
572
+ directory_name.gsub!("{number}", pr_number.to_s)
573
+ if pr_data[:title]
574
+ slug = Atoms::SlugGenerator.from_title(pr_data[:title])
575
+ directory_name.gsub!("{slug}", slug)
576
+ directory_name.gsub!("{title_slug}", slug)
577
+ end
578
+ directory_name.gsub!("{base_branch}", pr_data[:base_branch].to_s) if pr_data[:base_branch]
579
+
580
+ branch_name = branch_format.dup
581
+ branch_name.gsub!("{number}", pr_number.to_s)
582
+ if pr_data[:title]
583
+ slug = Atoms::SlugGenerator.from_title(pr_data[:title])
584
+ branch_name.gsub!("{slug}", slug)
585
+ branch_name.gsub!("{title_slug}", slug)
586
+ end
587
+ branch_name.gsub!("{base_branch}", pr_data[:base_branch].to_s) if pr_data[:base_branch]
588
+ worktree_path = File.join(@config.absolute_root_path, directory_name)
589
+ tracking = "#{remote_name}/#{pr_data[:head_branch]}"
590
+
591
+ {
592
+ success: true,
593
+ pr_number: pr_number,
594
+ pr_title: pr_data[:title],
595
+ would_create: {
596
+ worktree_path: worktree_path,
597
+ branch: branch_name,
598
+ tracking: tracking,
599
+ directory_name: directory_name
600
+ }
601
+ }
602
+ end
603
+
604
+ # Dry run branch worktree creation
605
+ #
606
+ # @param branch_name [String] Branch name
607
+ # @param options [Hash] Options
608
+ # @return [Hash] Dry run result
609
+ def dry_run_branch_creation(branch_name, options)
610
+ # Detect remote info
611
+ remote_info = @worktree_creator.send(:detect_remote_branch, branch_name)
612
+
613
+ local_branch = if remote_info
614
+ remote_info[:branch].split("/").last
615
+ else
616
+ branch_name
617
+ end
618
+
619
+ require_relative "../atoms/slug_generator"
620
+ directory_name = Atoms::SlugGenerator.to_directory_name(local_branch)
621
+ worktree_path = File.join(@config.absolute_root_path, directory_name)
622
+ tracking = remote_info ? branch_name : nil
623
+
624
+ {
625
+ success: true,
626
+ would_create: {
627
+ worktree_path: worktree_path,
628
+ branch: local_branch,
629
+ tracking: tracking,
630
+ directory_name: directory_name
631
+ }
632
+ }
633
+ end
634
+
635
+ # Dry run traditional worktree creation
636
+ #
637
+ # @param branch_name [String] Branch name
638
+ # @param options [Hash] Options
639
+ # @return [Hash] Dry run result
640
+ def dry_run_traditional_creation(branch_name, options)
641
+ # Check if branch exists (locally or remotely)
642
+ branch_exists = @worktree_creator.send(:branch_exists?, branch_name)
643
+
644
+ # Determine worktree path
645
+ worktree_path = if options[:path]
646
+ options[:path]
647
+ else
648
+ require_relative "../atoms/slug_generator"
649
+ directory_name = Atoms::SlugGenerator.to_directory_name(branch_name)
650
+ File.join(@config.absolute_root_path, directory_name)
651
+ end
652
+
653
+ {
654
+ success: true,
655
+ would_create: {
656
+ worktree_path: worktree_path,
657
+ branch: branch_name,
658
+ branch_exists: branch_exists,
659
+ source: options[:source] || "current branch"
660
+ }
661
+ }
662
+ end
663
+
664
+ # @param message [String] Error message
665
+ # @return [Hash] Error result
666
+ def error_result(message)
667
+ {
668
+ success: false,
669
+ error: message
670
+ }
671
+ end
672
+
673
+ # Attempt to delete an orphaned branch (when worktree doesn't exist)
674
+ #
675
+ # @param identifier [String] Branch name or identifier
676
+ # @param force [Boolean] Force deletion even if unmerged
677
+ # @return [Hash] Deletion result
678
+ def attempt_branch_only_deletion(identifier, force)
679
+ require_relative "../atoms/git_command"
680
+
681
+ # Get list of all branches
682
+ branches_result = Atoms::GitCommand.execute("branch", "--format=%(refname:short)", timeout: 5)
683
+ unless branches_result[:success]
684
+ return error_result("Worktree not found: #{identifier}")
685
+ end
686
+
687
+ # Check if identifier matches a branch name
688
+ branches = branches_result[:output].split("\n").map(&:strip)
689
+ unless branches.include?(identifier)
690
+ return error_result("Worktree not found: #{identifier}")
691
+ end
692
+
693
+ # Branch exists but no worktree - delete the orphaned branch
694
+ delete_result = @worktree_remover.delete_branch_if_safe(identifier, force)
695
+
696
+ if delete_result[:success]
697
+ {
698
+ success: true,
699
+ message: "Deleted orphaned branch: #{identifier}",
700
+ branch: identifier,
701
+ branch_deleted: true,
702
+ path: nil
703
+ }
704
+ else
705
+ # Include detailed message from delete_result for better troubleshooting
706
+ reason = delete_result[:message] || delete_result[:error] || "unknown reason"
707
+ error_result("Branch '#{identifier}' exists but could not be deleted: #{reason}")
708
+ end
709
+ end
710
+ end
711
+ end
712
+ end
713
+ end
714
+ end