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,522 @@
|
|
|
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 Commands
|
|
9
|
+
# Remove command
|
|
10
|
+
#
|
|
11
|
+
# Removes worktrees with safety checks and cleanup options.
|
|
12
|
+
# Supports both task-aware and traditional worktree removal.
|
|
13
|
+
#
|
|
14
|
+
# @example Remove by task ID
|
|
15
|
+
# RemoveCommand.new.run(["--task", "081"])
|
|
16
|
+
#
|
|
17
|
+
# @example Remove by branch name
|
|
18
|
+
# RemoveCommand.new.run(["feature-branch"])
|
|
19
|
+
#
|
|
20
|
+
# @example Force remove
|
|
21
|
+
# RemoveCommand.new.run(["--task", "081", "--force"])
|
|
22
|
+
class RemoveCommand
|
|
23
|
+
# Initialize a new RemoveCommand
|
|
24
|
+
def initialize
|
|
25
|
+
@manager = Organisms::WorktreeManager.new
|
|
26
|
+
@task_fetcher = Molecules::TaskFetcher.new
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Run the remove command
|
|
30
|
+
#
|
|
31
|
+
# @param args [Array<String>] Command arguments
|
|
32
|
+
# @return [Integer] Exit code (0 for success, 1 for error)
|
|
33
|
+
def run(args = [])
|
|
34
|
+
options = parse_arguments(args)
|
|
35
|
+
return show_help if options[:help]
|
|
36
|
+
|
|
37
|
+
validate_options(options)
|
|
38
|
+
|
|
39
|
+
if options[:task]
|
|
40
|
+
remove_task_worktree(options)
|
|
41
|
+
else
|
|
42
|
+
remove_traditional_worktree(options)
|
|
43
|
+
end
|
|
44
|
+
rescue ArgumentError => e
|
|
45
|
+
puts "Error: #{e.message}"
|
|
46
|
+
puts
|
|
47
|
+
show_help
|
|
48
|
+
1
|
|
49
|
+
rescue => e
|
|
50
|
+
puts "Error: #{e.message}"
|
|
51
|
+
1
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Show help for the remove command
|
|
55
|
+
#
|
|
56
|
+
# @return [Integer] Exit code
|
|
57
|
+
def show_help
|
|
58
|
+
puts <<~HELP
|
|
59
|
+
ace-git-worktree remove - Remove a worktree
|
|
60
|
+
|
|
61
|
+
USAGE:
|
|
62
|
+
ace-git-worktree remove <identifier> [OPTIONS]
|
|
63
|
+
ace-git-worktree remove --task <task-id> [OPTIONS]
|
|
64
|
+
|
|
65
|
+
IDENTIFIERS:
|
|
66
|
+
Task ID: 081, task.081, v.0.9.0+081
|
|
67
|
+
Branch name: feature-branch, main
|
|
68
|
+
Directory name: task.081, feature-branch
|
|
69
|
+
Full path: /path/to/worktree
|
|
70
|
+
|
|
71
|
+
OPTIONS:
|
|
72
|
+
--task <task-id> Remove worktree for specific task
|
|
73
|
+
--force Force removal even with uncommitted changes
|
|
74
|
+
--keep-directory Keep the worktree directory (default: remove)
|
|
75
|
+
--delete-branch, -db Also delete the associated branch
|
|
76
|
+
--dry-run Show what would be removed without removing
|
|
77
|
+
--help, -h Show this help message
|
|
78
|
+
|
|
79
|
+
EXAMPLES:
|
|
80
|
+
# Remove task worktree
|
|
81
|
+
ace-git-worktree remove --task 081
|
|
82
|
+
|
|
83
|
+
# Remove by branch name
|
|
84
|
+
ace-git-worktree remove feature-branch
|
|
85
|
+
|
|
86
|
+
# Force remove with changes
|
|
87
|
+
ace-git-worktree remove --task 081 --force
|
|
88
|
+
|
|
89
|
+
# Dry run to see what would be removed
|
|
90
|
+
ace-git-worktree remove --task 081 --dry-run
|
|
91
|
+
|
|
92
|
+
# Remove but keep directory
|
|
93
|
+
ace-git-worktree remove --task 081 --keep-directory
|
|
94
|
+
|
|
95
|
+
SAFETY:
|
|
96
|
+
• The command checks for uncommitted changes
|
|
97
|
+
• Use --force to remove worktrees with changes
|
|
98
|
+
• Task removal also cleans up task metadata
|
|
99
|
+
• Main worktree cannot be removed accidentally
|
|
100
|
+
|
|
101
|
+
CONFIGURATION:
|
|
102
|
+
Worktree removal respects settings in .ace/git/worktree.yml
|
|
103
|
+
HELP
|
|
104
|
+
0
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
# Parse command line arguments
|
|
110
|
+
#
|
|
111
|
+
# @param args [Array<String>] Command arguments
|
|
112
|
+
# @return [Hash] Parsed options
|
|
113
|
+
def parse_arguments(args)
|
|
114
|
+
options = {
|
|
115
|
+
task: nil,
|
|
116
|
+
identifier: nil,
|
|
117
|
+
force: false,
|
|
118
|
+
keep_directory: false,
|
|
119
|
+
delete_branch: false,
|
|
120
|
+
dry_run: false,
|
|
121
|
+
help: false
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
i = 0
|
|
125
|
+
while i < args.length
|
|
126
|
+
arg = args[i]
|
|
127
|
+
|
|
128
|
+
case arg
|
|
129
|
+
when "--task"
|
|
130
|
+
i += 1
|
|
131
|
+
options[:task] = args[i]
|
|
132
|
+
when "--force"
|
|
133
|
+
options[:force] = true
|
|
134
|
+
when "--keep-directory"
|
|
135
|
+
options[:keep_directory] = true
|
|
136
|
+
when "--delete-branch", "-db"
|
|
137
|
+
options[:delete_branch] = true
|
|
138
|
+
when "--dry-run"
|
|
139
|
+
options[:dry_run] = true
|
|
140
|
+
when "--help", "-h"
|
|
141
|
+
options[:help] = true
|
|
142
|
+
when /^--/
|
|
143
|
+
raise ArgumentError, "Unknown option: #{arg}"
|
|
144
|
+
else
|
|
145
|
+
# Positional argument - worktree identifier
|
|
146
|
+
if options[:identifier]
|
|
147
|
+
raise ArgumentError, "Multiple identifiers specified: #{options[:identifier]} and #{arg}"
|
|
148
|
+
end
|
|
149
|
+
options[:identifier] = arg
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
i += 1
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
options
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Validate parsed options
|
|
159
|
+
#
|
|
160
|
+
# @param options [Hash] Parsed options
|
|
161
|
+
def validate_options(options)
|
|
162
|
+
if options[:task] && options[:identifier]
|
|
163
|
+
raise ArgumentError, "Cannot specify both --task and identifier"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
if !options[:task] && !options[:identifier]
|
|
167
|
+
raise ArgumentError, "Must specify either --task <task-id> or <identifier>"
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
if options[:task] && options[:task].empty?
|
|
171
|
+
raise ArgumentError, "Task ID cannot be empty"
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
if options[:identifier] && options[:identifier].empty?
|
|
175
|
+
raise ArgumentError, "Identifier cannot be empty"
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Security validation for identifiers
|
|
179
|
+
if options[:identifier] && contains_dangerous_patterns?(options[:identifier])
|
|
180
|
+
raise ArgumentError, "Identifier contains potentially dangerous characters"
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Security validation for task IDs
|
|
184
|
+
if options[:task] && contains_dangerous_patterns?(options[:task])
|
|
185
|
+
raise ArgumentError, "Task ID contains potentially dangerous characters"
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Check if a string contains dangerous patterns
|
|
190
|
+
#
|
|
191
|
+
# @param value [String] Value to check
|
|
192
|
+
# @return [Boolean] true if dangerous patterns found
|
|
193
|
+
def contains_dangerous_patterns?(value)
|
|
194
|
+
return false if value.nil?
|
|
195
|
+
|
|
196
|
+
dangerous_patterns = [
|
|
197
|
+
/;/, # Command separator
|
|
198
|
+
/\|/, # Pipe
|
|
199
|
+
/`/, # Backtick command substitution
|
|
200
|
+
/\$\(/, # Command substitution
|
|
201
|
+
/\.\.\//, # Path traversal
|
|
202
|
+
/&&/, # AND operator
|
|
203
|
+
/\|\|/, # OR operator
|
|
204
|
+
/\x00/ # Null byte
|
|
205
|
+
]
|
|
206
|
+
|
|
207
|
+
dangerous_patterns.any? { |pattern| value.match?(pattern) }
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Remove a task-aware worktree
|
|
211
|
+
#
|
|
212
|
+
# @param options [Hash] Command options
|
|
213
|
+
# @return [Integer] Exit code
|
|
214
|
+
def remove_task_worktree(options)
|
|
215
|
+
puts "Removing worktree for task: #{options[:task]}"
|
|
216
|
+
|
|
217
|
+
# Try to find task data first
|
|
218
|
+
task_data = @task_fetcher.fetch(options[:task])
|
|
219
|
+
task_found = false
|
|
220
|
+
worktree_info = nil
|
|
221
|
+
|
|
222
|
+
if task_data
|
|
223
|
+
puts "Task found: #{task_data[:title]} (status: #{task_data[:status]})"
|
|
224
|
+
worktree_info = find_worktree_for_task(task_data)
|
|
225
|
+
task_found = true
|
|
226
|
+
else
|
|
227
|
+
# Fallback: Try to find worktree by task reference (for cases where task metadata exists but worktree doesn't)
|
|
228
|
+
puts "Task not found in ace-task, checking for orphaned worktree..."
|
|
229
|
+
worktree_info = find_worktree_by_task_reference(options[:task])
|
|
230
|
+
task_found = false
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
unless worktree_info
|
|
234
|
+
# Even if worktree doesn't exist, try to clean up the branch if requested
|
|
235
|
+
if task_data
|
|
236
|
+
puts "Task found but no worktree is associated with it."
|
|
237
|
+
puts "Task: #{task_data[:title]} (status: #{task_data[:status]})"
|
|
238
|
+
|
|
239
|
+
# Try to find the orphaned branch
|
|
240
|
+
branch_name = find_branch_for_task(task_data, options[:task])
|
|
241
|
+
if branch_name
|
|
242
|
+
puts "Found orphaned branch: #{branch_name}"
|
|
243
|
+
|
|
244
|
+
# Only delete if user requested it
|
|
245
|
+
if options[:delete_branch]
|
|
246
|
+
# Use safe deletion method (same as main flow)
|
|
247
|
+
remover = Ace::Git::Worktree::Molecules::WorktreeRemover.new
|
|
248
|
+
delete_result = remover.send(:delete_branch_if_safe, branch_name, options[:force])
|
|
249
|
+
|
|
250
|
+
if delete_result[:success]
|
|
251
|
+
puts "Deleted branch: #{branch_name}"
|
|
252
|
+
return 0
|
|
253
|
+
else
|
|
254
|
+
puts "Warning: Branch '#{branch_name}' was not deleted"
|
|
255
|
+
puts "Note: Use --force to delete unmerged branches"
|
|
256
|
+
end
|
|
257
|
+
else
|
|
258
|
+
puts "Note: Branch '#{branch_name}' still exists. Use --delete-branch to remove it."
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
puts ""
|
|
263
|
+
puts "Use 'ace-git-worktree list' to see available worktrees"
|
|
264
|
+
else
|
|
265
|
+
puts "Error: Task not found: #{options[:task]}"
|
|
266
|
+
puts "Use 'ace-task list' to see available tasks"
|
|
267
|
+
end
|
|
268
|
+
return 1
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
if options[:dry_run]
|
|
272
|
+
puts "DRY RUN - No changes will be made"
|
|
273
|
+
puts "This would:"
|
|
274
|
+
puts " • Remove worktree and its metadata from task #{options[:task]}"
|
|
275
|
+
puts " • #{task_found ? "Clean up task file metadata" : "Skip task metadata cleanup (no worktree metadata found)"}"
|
|
276
|
+
puts " • #{options[:keep_directory] ? "Keep" : "Remove"} the worktree directory"
|
|
277
|
+
return 0
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Prepare removal options
|
|
281
|
+
{
|
|
282
|
+
force: options[:force],
|
|
283
|
+
remove_directory: !options[:keep_directory]
|
|
284
|
+
}.compact
|
|
285
|
+
|
|
286
|
+
if options[:dry_run]
|
|
287
|
+
puts "DRY RUN - No changes will be made"
|
|
288
|
+
puts "This would:"
|
|
289
|
+
puts " • Remove worktree and its metadata from task #{options[:task]}"
|
|
290
|
+
puts " • #{task_found ? "Clean up task file metadata" : "Skip task metadata cleanup (task not found)"}"
|
|
291
|
+
puts " • #{options[:keep_directory] ? "Keep" : "Remove"} the worktree directory"
|
|
292
|
+
return 0
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Remove the worktree using direct git worktree command
|
|
296
|
+
# This bypasses the problematic safety check in WorktreeRemover
|
|
297
|
+
puts "Removing worktree at: #{worktree_info.path}"
|
|
298
|
+
|
|
299
|
+
begin
|
|
300
|
+
# Use GitCommand atom for safe execution
|
|
301
|
+
git_result = Ace::Git::Worktree::Atoms::GitCommand.worktree("remove", worktree_info.path, "--force")
|
|
302
|
+
|
|
303
|
+
if git_result[:success]
|
|
304
|
+
puts "Worktree removed successfully!"
|
|
305
|
+
puts "Worktree path: #{worktree_info.path}"
|
|
306
|
+
puts "Branch: #{worktree_info.branch}"
|
|
307
|
+
|
|
308
|
+
# Delete the branch if requested and it exists (using safe deletion method)
|
|
309
|
+
if options[:delete_branch] && worktree_info.branch && !worktree_info.branch.empty?
|
|
310
|
+
# Use WorktreeRemover's safe deletion method for consistency
|
|
311
|
+
remover = Ace::Git::Worktree::Molecules::WorktreeRemover.new
|
|
312
|
+
delete_result = remover.send(:delete_branch_if_safe, worktree_info.branch, options[:force])
|
|
313
|
+
|
|
314
|
+
if delete_result[:success]
|
|
315
|
+
puts "Deleted branch: #{worktree_info.branch}"
|
|
316
|
+
else
|
|
317
|
+
# Branch deletion failed (likely unmerged without --force)
|
|
318
|
+
puts "Warning: Branch '#{worktree_info.branch}' was not deleted"
|
|
319
|
+
puts "Note: Use --force to delete unmerged branches"
|
|
320
|
+
end
|
|
321
|
+
elsif worktree_info.branch && !worktree_info.branch.empty?
|
|
322
|
+
puts "\nNote: Branch '#{worktree_info.branch}' still exists. Use --delete-branch to remove it."
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# Provide appropriate messaging based on task status
|
|
326
|
+
if task_found && task_data
|
|
327
|
+
status = task_data[:status].to_s.strip
|
|
328
|
+
if status.include?("done") || status.include?("completed")
|
|
329
|
+
puts "\nTask completed: no metadata cleanup needed"
|
|
330
|
+
else
|
|
331
|
+
puts "\nNote: Task metadata cleanup not available for this task status"
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
0
|
|
335
|
+
else
|
|
336
|
+
puts "Failed to remove worktree: #{git_result[:error]}"
|
|
337
|
+
1
|
|
338
|
+
end
|
|
339
|
+
rescue => e
|
|
340
|
+
puts "Error removing worktree: #{e.message}"
|
|
341
|
+
1
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# Remove a traditional worktree
|
|
346
|
+
#
|
|
347
|
+
# @param options [Hash] Command options
|
|
348
|
+
# @return [Integer] Exit code
|
|
349
|
+
def remove_traditional_worktree(options)
|
|
350
|
+
puts "Removing worktree: #{options[:identifier]}"
|
|
351
|
+
|
|
352
|
+
if options[:dry_run]
|
|
353
|
+
puts "DRY RUN - No changes will be made"
|
|
354
|
+
puts "This would remove the worktree and #{options[:keep_directory] ? "keep" : "remove"} its directory"
|
|
355
|
+
return 0
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# Prepare removal options
|
|
359
|
+
removal_options = {
|
|
360
|
+
force: options[:force],
|
|
361
|
+
remove_directory: !options[:keep_directory],
|
|
362
|
+
delete_branch: options[:delete_branch]
|
|
363
|
+
}.compact
|
|
364
|
+
|
|
365
|
+
# Remove the worktree
|
|
366
|
+
result = @manager.remove(options[:identifier], removal_options)
|
|
367
|
+
|
|
368
|
+
if result[:success]
|
|
369
|
+
display_traditional_removal_result(result, options)
|
|
370
|
+
0
|
|
371
|
+
else
|
|
372
|
+
puts "Failed to remove worktree: #{result[:error]}"
|
|
373
|
+
display_removal_hints(options[:identifier], result[:error])
|
|
374
|
+
1
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
# Display task worktree removal result
|
|
379
|
+
#
|
|
380
|
+
# @param result [Hash] Removal result
|
|
381
|
+
def display_task_removal_result(result)
|
|
382
|
+
puts "\nTask worktree removed successfully!"
|
|
383
|
+
puts "Task ID: #{result[:task_id]}"
|
|
384
|
+
puts "Worktree path: #{result[:worktree_path]}" if result[:worktree_path]
|
|
385
|
+
puts "Branch: #{result[:branch]}" if result[:branch]
|
|
386
|
+
puts "\nSteps completed:"
|
|
387
|
+
result[:steps_completed].each_with_index do |step, i|
|
|
388
|
+
puts " ✓ #{step.tr("_", " ")}"
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
puts "\nNote: Task metadata has been cleaned up."
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# Display traditional worktree removal result
|
|
395
|
+
#
|
|
396
|
+
# @param result [Hash] Removal result
|
|
397
|
+
# @param options [Hash] Command options
|
|
398
|
+
def display_traditional_removal_result(result, options = {})
|
|
399
|
+
puts "\nWorktree removed successfully!"
|
|
400
|
+
puts "Worktree path: #{result[:path]}" if result[:path]
|
|
401
|
+
puts "Branch: #{result[:branch]}" if result[:branch]
|
|
402
|
+
|
|
403
|
+
if result[:branch_deleted]
|
|
404
|
+
puts "Branch deleted: #{result[:branch]}"
|
|
405
|
+
elsif result[:branch] && !result[:branch_deleted]
|
|
406
|
+
# This message is shown if the branch exists but wasn't deleted,
|
|
407
|
+
# which happens if --delete-branch was not provided, or if the
|
|
408
|
+
# branch deletion failed (e.g., unmerged branch without --force)
|
|
409
|
+
puts "\nNote: Branch '#{result[:branch]}' still exists. Use --delete-branch to remove it."
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
# Display removal hints and suggestions
|
|
414
|
+
#
|
|
415
|
+
# @param identifier [String] The identifier that failed
|
|
416
|
+
# @param error [String] The error message
|
|
417
|
+
def display_removal_hints(identifier, error)
|
|
418
|
+
if error.include?("not found")
|
|
419
|
+
puts "\nWorktree not found. Available worktrees:"
|
|
420
|
+
list_result = @manager.list_all(format: :simple)
|
|
421
|
+
if list_result[:success] && list_result[:worktrees].any?
|
|
422
|
+
list_result[:worktrees].each do |worktree|
|
|
423
|
+
prefix = worktree.task_associated? ? "Task #{worktree.task_id}: " : ""
|
|
424
|
+
puts " #{prefix}#{worktree.branch || "detached"} (#{worktree.path})"
|
|
425
|
+
end
|
|
426
|
+
else
|
|
427
|
+
puts " No worktrees found."
|
|
428
|
+
end
|
|
429
|
+
elsif error.include?("uncommitted changes")
|
|
430
|
+
puts "\nWorktree has uncommitted changes. Options:"
|
|
431
|
+
puts " • Use --force to remove anyway (changes will be lost)"
|
|
432
|
+
puts " • Commit or stash changes first"
|
|
433
|
+
puts " • Check worktree status with: git status"
|
|
434
|
+
else
|
|
435
|
+
puts "\nSuggestions:"
|
|
436
|
+
puts " • Check the worktree identifier spelling"
|
|
437
|
+
puts " • Use 'ace-git-worktree list' to see available worktrees"
|
|
438
|
+
puts " • Use --force if you're sure about removal"
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
# Find worktree by task reference (for completed tasks)
|
|
443
|
+
#
|
|
444
|
+
# @param task_ref [String] Task reference
|
|
445
|
+
# @return [WorktreeInfo, nil] Worktree info or nil if not found
|
|
446
|
+
def find_worktree_by_task_reference(task_ref)
|
|
447
|
+
worktree_lister = Ace::Git::Worktree::Molecules::WorktreeLister.new
|
|
448
|
+
worktrees = worktree_lister.list_all
|
|
449
|
+
|
|
450
|
+
# Normalize task reference to match worktree IDs
|
|
451
|
+
normalized_id = normalize_task_id_for_matching(task_ref)
|
|
452
|
+
|
|
453
|
+
# Find worktree with matching task ID
|
|
454
|
+
worktrees.find do |worktree|
|
|
455
|
+
worktree.task_id == normalized_id
|
|
456
|
+
end
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
# Find worktree for task (when task data is available)
|
|
460
|
+
#
|
|
461
|
+
# @param task_data [Hash] Task data hash from ace-task
|
|
462
|
+
# @return [WorktreeInfo, nil] Worktree info or nil if not found
|
|
463
|
+
def find_worktree_for_task(task_data)
|
|
464
|
+
worktree_lister = Ace::Git::Worktree::Molecules::WorktreeLister.new
|
|
465
|
+
worktrees = worktree_lister.list_all
|
|
466
|
+
|
|
467
|
+
# Extract task number from task data
|
|
468
|
+
task_number = extract_task_number(task_data)
|
|
469
|
+
|
|
470
|
+
worktrees.find do |worktree|
|
|
471
|
+
worktree.task_id == task_number ||
|
|
472
|
+
worktree.task_id == task_data[:id] ||
|
|
473
|
+
worktree.branch == task_data[:branch]
|
|
474
|
+
end
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
# Extract task number from task data
|
|
478
|
+
#
|
|
479
|
+
# @param task_data [Hash] Task data
|
|
480
|
+
# @return [String] Task number
|
|
481
|
+
def extract_task_number(task_data)
|
|
482
|
+
# Use shared extractor that preserves subtask IDs (e.g., "121.01")
|
|
483
|
+
Atoms::TaskIDExtractor.extract(task_data)
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
# Normalize task ID for worktree matching
|
|
487
|
+
#
|
|
488
|
+
# @param task_ref [String] Task reference
|
|
489
|
+
# @return [String] Normalized task ID
|
|
490
|
+
def normalize_task_id_for_matching(task_ref)
|
|
491
|
+
# Use shared extractor that preserves subtask IDs (e.g., "121.01")
|
|
492
|
+
Atoms::TaskIDExtractor.normalize(task_ref) || task_ref
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
# Find branch associated with a task
|
|
496
|
+
#
|
|
497
|
+
# @param task_data [Hash] Task data
|
|
498
|
+
# @param task_ref [String] Task reference
|
|
499
|
+
# @return [String, nil] Branch name or nil if not found
|
|
500
|
+
def find_branch_for_task(task_data, task_ref)
|
|
501
|
+
# Get all branches
|
|
502
|
+
branches_result = Ace::Git::Worktree::Atoms::GitCommand.execute("branch", "--format=%(refname:short)")
|
|
503
|
+
return nil unless branches_result[:success]
|
|
504
|
+
|
|
505
|
+
branches = branches_result[:output].split("\n").map(&:strip)
|
|
506
|
+
task_number = extract_task_number(task_data)
|
|
507
|
+
|
|
508
|
+
# Try to find branch matching task patterns
|
|
509
|
+
# Pattern 1: 052-task-title
|
|
510
|
+
# Pattern 2: task-052
|
|
511
|
+
# Pattern 3: v.0.9.0-052
|
|
512
|
+
branches.find do |branch|
|
|
513
|
+
branch.start_with?("#{task_number}-", "task-#{task_number}") ||
|
|
514
|
+
branch =~ /\d+-#{task_number}-/ ||
|
|
515
|
+
branch.include?("-#{task_number}-")
|
|
516
|
+
end
|
|
517
|
+
end
|
|
518
|
+
end
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
end
|
|
522
|
+
end
|