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