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,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
|