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,337 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Git
|
|
5
|
+
module Worktree
|
|
6
|
+
module Molecules
|
|
7
|
+
# Worktree lister molecule
|
|
8
|
+
#
|
|
9
|
+
# Lists and manages git worktrees with task association capabilities.
|
|
10
|
+
# Provides filtering, searching, and formatting of worktree information.
|
|
11
|
+
#
|
|
12
|
+
# @example List all worktrees
|
|
13
|
+
# lister = WorktreeLister.new
|
|
14
|
+
# worktrees = lister.list_all
|
|
15
|
+
#
|
|
16
|
+
# @example Find worktree by task ID
|
|
17
|
+
# worktree = lister.find_by_task_id("081")
|
|
18
|
+
#
|
|
19
|
+
# @example List with task associations
|
|
20
|
+
# worktrees = lister.list_with_tasks
|
|
21
|
+
class WorktreeLister
|
|
22
|
+
# Fallback timeout for git commands
|
|
23
|
+
# Used only when config is unavailable
|
|
24
|
+
FALLBACK_TIMEOUT = 30
|
|
25
|
+
|
|
26
|
+
# Initialize a new WorktreeLister
|
|
27
|
+
#
|
|
28
|
+
# @param timeout [Integer, nil] Command timeout in seconds (uses config default if nil)
|
|
29
|
+
def initialize(timeout: nil)
|
|
30
|
+
@timeout = timeout || config_timeout
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# Get timeout from config or fallback
|
|
36
|
+
# @return [Integer] Timeout in seconds
|
|
37
|
+
def config_timeout
|
|
38
|
+
Ace::Git::Worktree.list_timeout
|
|
39
|
+
rescue
|
|
40
|
+
FALLBACK_TIMEOUT
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
public
|
|
44
|
+
|
|
45
|
+
# List all worktrees in the repository
|
|
46
|
+
#
|
|
47
|
+
# @return [Array<WorktreeInfo>] Array of worktree information
|
|
48
|
+
#
|
|
49
|
+
# @example
|
|
50
|
+
# lister = WorktreeLister.new
|
|
51
|
+
# worktrees = lister.list_all
|
|
52
|
+
# worktrees.each { |wt| puts "#{wt.branch} at #{wt.path}" }
|
|
53
|
+
def list_all
|
|
54
|
+
output = execute_git_worktree_list
|
|
55
|
+
return [] unless output
|
|
56
|
+
|
|
57
|
+
Models::WorktreeInfo.from_git_output_list(output)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# List worktrees with task associations resolved
|
|
61
|
+
#
|
|
62
|
+
# @return [Array<WorktreeInfo>] Array of worktree information with task IDs
|
|
63
|
+
#
|
|
64
|
+
# @example
|
|
65
|
+
# worktrees = lister.list_with_tasks
|
|
66
|
+
# worktrees.each { |wt| puts "Task #{wt.task_id}: #{wt.branch}" if wt.task_associated? }
|
|
67
|
+
def list_with_tasks
|
|
68
|
+
worktrees = list_all
|
|
69
|
+
resolve_task_associations(worktrees)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Find worktree by task ID
|
|
73
|
+
#
|
|
74
|
+
# @param task_id [String] Task ID to search for
|
|
75
|
+
# @return [WorktreeInfo, nil] Matching worktree or nil
|
|
76
|
+
#
|
|
77
|
+
# @example
|
|
78
|
+
# worktree = lister.find_by_task_id("081")
|
|
79
|
+
# if worktree
|
|
80
|
+
# puts "Found worktree for task 081: #{worktree.path}"
|
|
81
|
+
# end
|
|
82
|
+
def find_by_task_id(task_id)
|
|
83
|
+
worktrees = list_with_tasks
|
|
84
|
+
Models::WorktreeInfo.find_by_task_id(worktrees, task_id.to_s)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Find worktree by branch name
|
|
88
|
+
#
|
|
89
|
+
# @param branch_name [String] Branch name to search for
|
|
90
|
+
# @return [WorktreeInfo, nil] Matching worktree or nil
|
|
91
|
+
#
|
|
92
|
+
# @example
|
|
93
|
+
# worktree = lister.find_by_branch("081-fix-auth")
|
|
94
|
+
def find_by_branch(branch_name)
|
|
95
|
+
worktrees = list_all
|
|
96
|
+
Models::WorktreeInfo.find_by_branch(worktrees, branch_name.to_s)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Find worktree by directory name
|
|
100
|
+
#
|
|
101
|
+
# @param directory [String] Directory name to search for
|
|
102
|
+
# @return [WorktreeInfo, nil] Matching worktree or nil
|
|
103
|
+
#
|
|
104
|
+
# @example
|
|
105
|
+
# worktree = lister.find_by_directory("task.081")
|
|
106
|
+
def find_by_directory(directory)
|
|
107
|
+
worktrees = list_all
|
|
108
|
+
Models::WorktreeInfo.find_by_directory(worktrees, directory.to_s)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Find worktree by path
|
|
112
|
+
#
|
|
113
|
+
# @param path [String] Path to search for
|
|
114
|
+
# @return [WorktreeInfo, nil] Matching worktree or nil
|
|
115
|
+
#
|
|
116
|
+
# @example
|
|
117
|
+
# worktree = lister.find_by_path("/project/.ace-wt/task.081")
|
|
118
|
+
def find_by_path(path)
|
|
119
|
+
worktrees = list_all
|
|
120
|
+
expanded_path = File.expand_path(path)
|
|
121
|
+
worktrees.find { |wt| File.expand_path(wt.path) == expanded_path }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Filter worktrees by criteria
|
|
125
|
+
#
|
|
126
|
+
# @param worktrees [Array<WorktreeInfo>] Worktrees to filter
|
|
127
|
+
# @param task_associated [Boolean, nil] Filter by task association
|
|
128
|
+
# @param usable [Boolean, nil] Filter by usability
|
|
129
|
+
# @param branch_pattern [String, nil] Filter by branch name pattern
|
|
130
|
+
# @return [Array<WorktreeInfo>] Filtered worktrees
|
|
131
|
+
#
|
|
132
|
+
# @example
|
|
133
|
+
# # Get only task-associated worktrees
|
|
134
|
+
# task_worktrees = lister.filter(worktrees, task_associated: true)
|
|
135
|
+
#
|
|
136
|
+
# # Get only usable worktrees
|
|
137
|
+
# usable_worktrees = lister.filter(worktrees, usable: true)
|
|
138
|
+
#
|
|
139
|
+
# # Get worktrees with branches matching a pattern
|
|
140
|
+
# auth_worktrees = lister.filter(worktrees, branch_pattern: "auth")
|
|
141
|
+
def filter(worktrees, task_associated: nil, usable: nil, branch_pattern: nil)
|
|
142
|
+
filtered = Array(worktrees)
|
|
143
|
+
|
|
144
|
+
# Filter by task association
|
|
145
|
+
if task_associated == true
|
|
146
|
+
filtered = filtered.select(&:task_associated?)
|
|
147
|
+
elsif task_associated == false
|
|
148
|
+
filtered = filtered.reject(&:task_associated?)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Filter by usability
|
|
152
|
+
if usable == true
|
|
153
|
+
filtered = filtered.select(&:usable?)
|
|
154
|
+
elsif usable == false
|
|
155
|
+
filtered = filtered.reject(&:usable?)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Filter by branch pattern
|
|
159
|
+
if branch_pattern
|
|
160
|
+
pattern = Regexp.new(branch_pattern, Regexp::IGNORECASE)
|
|
161
|
+
filtered = filtered.select { |wt| wt.branch&.match?(pattern) }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
filtered
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Search worktrees by various criteria
|
|
168
|
+
#
|
|
169
|
+
# @param query [String] Search query
|
|
170
|
+
# @param search_in [Array<Symbol>] Where to search ([:branch, :path, :task_id])
|
|
171
|
+
# @return [Array<WorktreeInfo>] Matching worktrees
|
|
172
|
+
#
|
|
173
|
+
# @example
|
|
174
|
+
# results = lister.search("auth", search_in: [:branch, :task_id])
|
|
175
|
+
def search(query, search_in: [:branch, :path, :task_id])
|
|
176
|
+
return [] if query.nil? || query.empty?
|
|
177
|
+
|
|
178
|
+
worktrees = list_with_tasks
|
|
179
|
+
pattern = Regexp.new(query, Regexp::IGNORECASE)
|
|
180
|
+
|
|
181
|
+
worktrees.select do |worktree|
|
|
182
|
+
search_in.any? do |field|
|
|
183
|
+
case field
|
|
184
|
+
when :branch
|
|
185
|
+
worktree.branch&.match?(pattern)
|
|
186
|
+
when :path
|
|
187
|
+
worktree.path.match?(pattern)
|
|
188
|
+
when :task_id
|
|
189
|
+
worktree.task_id&.match?(pattern)
|
|
190
|
+
else
|
|
191
|
+
false
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Get worktree statistics
|
|
198
|
+
#
|
|
199
|
+
# @param worktrees [Array<WorktreeInfo>, nil] Optional pre-filtered worktree list
|
|
200
|
+
# @return [Hash] Statistics about worktrees
|
|
201
|
+
#
|
|
202
|
+
# @example
|
|
203
|
+
# stats = lister.get_statistics
|
|
204
|
+
# puts "Total worktrees: #{stats[:total]}"
|
|
205
|
+
# puts "Task-associated: #{stats[:task_associated]}"
|
|
206
|
+
# puts "Usable: #{stats[:usable]}"
|
|
207
|
+
def get_statistics(worktrees = nil)
|
|
208
|
+
worktrees = worktrees ? Array(worktrees) : list_with_tasks
|
|
209
|
+
|
|
210
|
+
{
|
|
211
|
+
total: worktrees.length,
|
|
212
|
+
task_associated: worktrees.count(&:task_associated?),
|
|
213
|
+
non_task_associated: worktrees.reject(&:task_associated?).length,
|
|
214
|
+
usable: worktrees.count(&:usable?),
|
|
215
|
+
unusable: worktrees.reject(&:usable?).length,
|
|
216
|
+
bare: worktrees.count(&:bare),
|
|
217
|
+
detached: worktrees.count(&:detached),
|
|
218
|
+
branches: worktrees.map(&:branch).compact,
|
|
219
|
+
task_ids: worktrees.map(&:task_id).compact.uniq
|
|
220
|
+
}
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Format worktree list for display
|
|
224
|
+
#
|
|
225
|
+
# @param worktrees [Array<WorktreeInfo>] Worktrees to format
|
|
226
|
+
# @param format [Symbol] Output format (:table, :json, :simple)
|
|
227
|
+
# @return [String] Formatted output
|
|
228
|
+
#
|
|
229
|
+
# @example
|
|
230
|
+
# output = lister.format_for_display(worktrees, :table)
|
|
231
|
+
# puts output
|
|
232
|
+
def format_for_display(worktrees, format = :table)
|
|
233
|
+
case format
|
|
234
|
+
when :table
|
|
235
|
+
format_as_table(worktrees)
|
|
236
|
+
when :json
|
|
237
|
+
format_as_json(worktrees)
|
|
238
|
+
when :simple
|
|
239
|
+
format_as_simple(worktrees)
|
|
240
|
+
else
|
|
241
|
+
format_as_table(worktrees)
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
private
|
|
246
|
+
|
|
247
|
+
# Execute git worktree list command
|
|
248
|
+
#
|
|
249
|
+
# @return [String, nil] Command output or nil if failed
|
|
250
|
+
def execute_git_worktree_list
|
|
251
|
+
require_relative "../atoms/git_command"
|
|
252
|
+
result = Atoms::GitCommand.worktree("list", "--porcelain", timeout: @timeout)
|
|
253
|
+
result[:success] ? result[:output] : nil
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Resolve task associations for worktrees
|
|
257
|
+
#
|
|
258
|
+
# @param worktrees [Array<WorktreeInfo>] Worktrees to process
|
|
259
|
+
# @return [Array<WorktreeInfo>] Worktrees with resolved task associations
|
|
260
|
+
def resolve_task_associations(worktrees)
|
|
261
|
+
return worktrees if worktrees.empty?
|
|
262
|
+
|
|
263
|
+
# Try to resolve task associations from worktree metadata
|
|
264
|
+
# This would typically involve looking up task files
|
|
265
|
+
# For now, we'll use the automatic extraction from WorktreeInfo
|
|
266
|
+
|
|
267
|
+
worktrees
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Format worktrees as a table
|
|
271
|
+
#
|
|
272
|
+
# @param worktrees [Array<WorktreeInfo>] Worktrees to format
|
|
273
|
+
# @return [String] Table-formatted string
|
|
274
|
+
def format_as_table(worktrees)
|
|
275
|
+
return "No worktrees found.\n" if worktrees.empty?
|
|
276
|
+
|
|
277
|
+
# Calculate column widths
|
|
278
|
+
max_path_width = [worktrees.map { |wt| wt.path.length }.max, 40].max
|
|
279
|
+
max_branch_width = [worktrees.map { |wt| (wt.branch || "detached").length }.max, 15].max
|
|
280
|
+
max_task_width = [worktrees.map { |wt| (wt.task_id || "-").length }.max, 8].max
|
|
281
|
+
|
|
282
|
+
# Build header
|
|
283
|
+
header = sprintf("%-#{max_task_width}s %-#{max_branch_width}s %-#{max_path_width}s %s",
|
|
284
|
+
"Task", "Branch", "Path", "Status")
|
|
285
|
+
separator = "-" * header.length
|
|
286
|
+
|
|
287
|
+
# Build table rows
|
|
288
|
+
rows = worktrees.map do |wt|
|
|
289
|
+
status = if wt.bare
|
|
290
|
+
"bare"
|
|
291
|
+
elsif wt.detached
|
|
292
|
+
"detached"
|
|
293
|
+
elsif wt.task_associated?
|
|
294
|
+
"task"
|
|
295
|
+
else
|
|
296
|
+
"normal"
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
sprintf("%-#{max_task_width}s %-#{max_branch_width}s %-#{max_path_width}s %s",
|
|
300
|
+
wt.task_id || "-",
|
|
301
|
+
wt.branch || "detached",
|
|
302
|
+
wt.path,
|
|
303
|
+
status)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
[header, separator, *rows].join("\n") + "\n"
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# Format worktrees as JSON
|
|
310
|
+
#
|
|
311
|
+
# @param worktrees [Array<WorktreeInfo>] Worktrees to format
|
|
312
|
+
# @return [String] JSON-formatted string
|
|
313
|
+
def format_as_json(worktrees)
|
|
314
|
+
require "json"
|
|
315
|
+
worktrees.map(&:to_h).to_json
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Format worktrees as simple list
|
|
319
|
+
#
|
|
320
|
+
# @param worktrees [Array<WorktreeInfo>] Worktrees to format
|
|
321
|
+
# @return [String] Simple list string
|
|
322
|
+
def format_as_simple(worktrees)
|
|
323
|
+
return "No worktrees found.\n" if worktrees.empty?
|
|
324
|
+
|
|
325
|
+
worktrees.map do |wt|
|
|
326
|
+
if wt.task_associated?
|
|
327
|
+
"Task #{wt.task_id}: #{wt.branch} at #{wt.path}"
|
|
328
|
+
else
|
|
329
|
+
"#{wt.branch || "detached"} at #{wt.path}"
|
|
330
|
+
end
|
|
331
|
+
end.join("\n") + "\n"
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
end
|