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,247 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Git
5
+ module Worktree
6
+ module Commands
7
+ # List command
8
+ #
9
+ # Lists worktrees with various formatting and filtering options.
10
+ # Supports task-aware listing and search capabilities.
11
+ #
12
+ # @example List all worktrees
13
+ # ListCommand.new.run([])
14
+ #
15
+ # @example List with JSON output
16
+ # ListCommand.new.run(["--format", "json"])
17
+ #
18
+ # @example List only task-associated worktrees
19
+ # ListCommand.new.run(["--task-associated"])
20
+ class ListCommand
21
+ # Initialize a new ListCommand
22
+ def initialize
23
+ @manager = Organisms::WorktreeManager.new
24
+ end
25
+
26
+ # Run the list command
27
+ #
28
+ # @param args [Array<String>] Command arguments
29
+ # @return [Integer] Exit code (0 for success, 1 for error)
30
+ def run(args = [])
31
+ options = parse_arguments(args)
32
+ return show_help if options[:help]
33
+
34
+ validate_options(options)
35
+
36
+ # Convert format to symbol for WorktreeLister compatibility
37
+ options[:format] = options[:format].to_sym if options[:format]
38
+
39
+ result = @manager.list_all(options)
40
+
41
+ if result[:success]
42
+ display_list_result(result, options)
43
+ 0
44
+ else
45
+ puts "Failed to list worktrees: #{result[:error]}"
46
+ 1
47
+ end
48
+ rescue ArgumentError => e
49
+ puts "Error: #{e.message}"
50
+ puts
51
+ show_help
52
+ 1
53
+ rescue => e
54
+ puts "Error: #{e.message}"
55
+ 1
56
+ end
57
+
58
+ # Show help for the list command
59
+ #
60
+ # @return [Integer] Exit code
61
+ def show_help
62
+ puts <<~HELP
63
+ ace-git-worktree list - List worktrees
64
+
65
+ USAGE:
66
+ ace-git-worktree list [OPTIONS]
67
+
68
+ OUTPUT FORMATS:
69
+ --format <format> Output format: table, json, simple (default: table)
70
+ --show-tasks Include task associations
71
+
72
+ FILTERING:
73
+ --task-associated Show only task-associated worktrees
74
+ --no-task-associated Show only non-task worktrees
75
+ --usable Show only usable worktrees
76
+ --no-usable Show only unusable worktrees
77
+ --search <pattern> Filter by branch name pattern
78
+
79
+ EXAMPLES:
80
+ # List all worktrees in table format
81
+ ace-git-worktree list
82
+
83
+ # List with task associations in JSON format
84
+ ace-git-worktree list --show-tasks --format json
85
+
86
+ # List only task-associated worktrees
87
+ ace-git-worktree list --task-associated
88
+
89
+ # Search for worktrees with "auth" in branch name
90
+ ace-git-worktree list --search auth
91
+
92
+ # List only usable worktrees
93
+ ace-git-worktree list --usable
94
+
95
+ OUTPUT:
96
+ Table format columns:
97
+ - Task: Task ID (or - for non-task worktrees)
98
+ - Branch: Git branch name
99
+ - Path: Worktree directory path
100
+ - Status: worktree status (task, normal, bare, detached, etc.)
101
+
102
+ JSON format includes full worktree details and metadata.
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
+ format: "table",
116
+ show_tasks: false,
117
+ task_associated: nil,
118
+ usable: nil,
119
+ search: nil,
120
+ help: false
121
+ }
122
+
123
+ i = 0
124
+ while i < args.length
125
+ arg = args[i]
126
+
127
+ case arg
128
+ when "--format"
129
+ i += 1
130
+ format = args[i]&.downcase
131
+ if %w[table json simple].include?(format)
132
+ options[:format] = format
133
+ else
134
+ raise ArgumentError, "Invalid format: #{format}. Use: table, json, simple"
135
+ end
136
+ when "--show-tasks"
137
+ options[:show_tasks] = true
138
+ when "--task-associated"
139
+ options[:task_associated] = true
140
+ when "--no-task-associated"
141
+ options[:task_associated] = false
142
+ when "--usable"
143
+ options[:usable] = true
144
+ when "--no-usable"
145
+ options[:usable] = false
146
+ when "--search"
147
+ i += 1
148
+ options[:search] = args[i]
149
+ when "--help", "-h"
150
+ options[:help] = true
151
+ when /^--/
152
+ raise ArgumentError, "Unknown option: #{arg}"
153
+ else
154
+ raise ArgumentError, "Unexpected argument: #{arg}"
155
+ end
156
+
157
+ i += 1
158
+ end
159
+
160
+ options
161
+ end
162
+
163
+ # Validate parsed options
164
+ #
165
+ # @param options [Hash] Parsed options
166
+ def validate_options(options)
167
+ if options[:search] && options[:search].empty?
168
+ raise ArgumentError, "Search pattern cannot be empty"
169
+ end
170
+
171
+ if options[:format] && !%w[table json simple].include?(options[:format])
172
+ raise ArgumentError, "Invalid format: #{options[:format]}"
173
+ end
174
+
175
+ # Security validation for search patterns
176
+ if options[:search] && contains_dangerous_patterns?(options[:search])
177
+ raise ArgumentError, "Search pattern contains potentially dangerous characters"
178
+ end
179
+ end
180
+
181
+ # Check if a string contains dangerous patterns
182
+ #
183
+ # @param value [String] Value to check
184
+ # @return [Boolean] true if dangerous patterns found
185
+ def contains_dangerous_patterns?(value)
186
+ return false if value.nil?
187
+
188
+ dangerous_patterns = [
189
+ /;/, # Command separator
190
+ /\|/, # Pipe
191
+ /`/, # Backtick command substitution
192
+ /\$\(/, # Command substitution
193
+ /\.\.\//, # Path traversal
194
+ /&&/, # AND operator
195
+ /\|\|/ # OR operator
196
+ ]
197
+
198
+ dangerous_patterns.any? { |pattern| value.match?(pattern) }
199
+ end
200
+
201
+ # Display list result
202
+ #
203
+ # @param result [Hash] List result
204
+ # @param options [Hash] Command options
205
+ def display_list_result(result, options)
206
+ if result[:worktrees].empty?
207
+ puts "No worktrees found."
208
+ return
209
+ end
210
+
211
+ # Display the formatted output
212
+ puts result[:formatted_output]
213
+
214
+ # Display summary if requested or if not JSON format
215
+ if options[:format] != :json
216
+ display_summary(result, options)
217
+ end
218
+ end
219
+
220
+ # Display summary information
221
+ #
222
+ # @param result [Hash] List result
223
+ # @param options [Hash] Command options
224
+ def display_summary(result, options)
225
+ stats = result[:statistics]
226
+ puts "\nSummary:"
227
+ puts " Total worktrees: #{stats[:total]}"
228
+ puts " Task-associated: #{stats[:task_associated]}"
229
+ puts " Usable: #{stats[:usable]}"
230
+
231
+ if options[:show_tasks] && stats[:task_ids].any?
232
+ puts " Tasks with worktrees: #{stats[:task_ids].join(", ")}"
233
+ end
234
+
235
+ if stats[:branches].any?
236
+ puts " Branches: #{stats[:branches].join(", ")}"
237
+ end
238
+
239
+ # Show active worktrees count
240
+ active_count = result[:worktrees].count { |wt| wt.exists? && wt.usable? }
241
+ puts " Active worktrees: #{active_count}"
242
+ end
243
+ end
244
+ end
245
+ end
246
+ end
247
+ end
@@ -0,0 +1,260 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Git
5
+ module Worktree
6
+ module Commands
7
+ # Prune command
8
+ #
9
+ # Cleans up git metadata for deleted worktrees and removes orphaned
10
+ # worktree directories that are no longer tracked by git.
11
+ #
12
+ # @example Prune deleted worktrees
13
+ # PruneCommand.new.run([])
14
+ #
15
+ # @example Prune with directory cleanup
16
+ # PruneCommand.new.run(["--cleanup-directories"])
17
+ class PruneCommand
18
+ # Initialize a new PruneCommand
19
+ def initialize
20
+ @manager = Organisms::WorktreeManager.new
21
+ end
22
+
23
+ # Run the prune command
24
+ #
25
+ # @param args [Array<String>] Command arguments
26
+ # @return [Integer] Exit code (0 for success, 1 for error)
27
+ def run(args = [])
28
+ options = parse_arguments(args)
29
+ return show_help if options[:help]
30
+
31
+ validate_options(options)
32
+
33
+ result = @manager.prune
34
+
35
+ if result[:success]
36
+ display_prune_result(result, options)
37
+
38
+ # Additional directory cleanup if requested
39
+ if options[:cleanup_directories]
40
+ cleanup_orphaned_directories(options)
41
+ end
42
+
43
+ 0
44
+ else
45
+ puts "Failed to prune worktrees: #{result[:error]}"
46
+ 1
47
+ end
48
+ rescue ArgumentError => e
49
+ puts "Error: #{e.message}"
50
+ puts
51
+ show_help
52
+ 1
53
+ rescue => e
54
+ puts "Error: #{e.message}"
55
+ 1
56
+ end
57
+
58
+ # Show help for the prune command
59
+ #
60
+ # @return [Integer] Exit code
61
+ def show_help
62
+ puts <<~HELP
63
+ ace-git-worktree prune - Clean up deleted worktrees
64
+
65
+ USAGE:
66
+ ace-git-worktree prune [OPTIONS]
67
+
68
+ OPTIONS:
69
+ --dry-run Show what would be pruned without pruning
70
+ --cleanup-directories Remove orphaned worktree directories
71
+ --verbose, -v Show detailed pruning information
72
+ --help, -h Show this help message
73
+
74
+ EXAMPLES:
75
+ # Prune deleted worktrees (git metadata cleanup only)
76
+ ace-git-worktree prune
77
+
78
+ # Dry run to see what would be pruned
79
+ ace-git-worktree prune --dry-run
80
+
81
+ # Prune and cleanup orphaned directories
82
+ ace-git-worktree prune --cleanup-directories
83
+
84
+ # Verbose output
85
+ ace-git-worktree prune --verbose
86
+
87
+ WHAT IT DOES:
88
+ 1. Prunes git worktree metadata for deleted worktrees
89
+ 2. Removes stale worktree entries from git's tracking
90
+ 3. Optionally removes orphaned worktree directories
91
+ 4. Reports what was cleaned up
92
+
93
+ SAFETY:
94
+ • Only removes worktrees that are no longer tracked by git
95
+ • Does not affect active worktrees or current worktree
96
+ • Directory cleanup is optional and requires explicit flag
97
+ • Dry run available to preview changes
98
+
99
+ CONFIGURATION:
100
+ Pruning behavior can be configured in .ace/git/worktree.yml:
101
+ - cleanup.on_delete: Automatic cleanup on branch deletion
102
+ - cleanup.on_merge: Automatic cleanup on branch merge
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
+ dry_run: false,
116
+ cleanup_directories: false,
117
+ force: false,
118
+ verbose: false,
119
+ help: false
120
+ }
121
+
122
+ i = 0
123
+ while i < args.length
124
+ arg = args[i]
125
+
126
+ case arg
127
+ when "--dry-run"
128
+ options[:dry_run] = true
129
+ when "--cleanup-directories"
130
+ options[:cleanup_directories] = true
131
+ when "--force"
132
+ options[:force] = true
133
+ when "--verbose", "-v"
134
+ options[:verbose] = true
135
+ when "--help", "-h"
136
+ options[:help] = true
137
+ when /^--/
138
+ raise ArgumentError, "Unknown option: #{arg}"
139
+ else
140
+ raise ArgumentError, "Unexpected argument: #{arg}"
141
+ end
142
+
143
+ i += 1
144
+ end
145
+
146
+ options
147
+ end
148
+
149
+ # Validate parsed options
150
+ #
151
+ # @param options [Hash] Parsed options
152
+ def validate_options(options)
153
+ # No specific validation needed for prune command
154
+ end
155
+
156
+ # Display prune result
157
+ #
158
+ # @param result [Hash] Prune result
159
+ # @param options [Hash] Command options
160
+ def display_prune_result(result, options)
161
+ if result[:pruned_count] && result[:pruned_count] > 0
162
+ puts "Pruned #{result[:pruned_count]} worktree(s) successfully."
163
+
164
+ if options[:verbose] && result[:output]
165
+ puts "\nPruned worktrees:"
166
+ result[:output].split("\n").each do |line|
167
+ next unless line.include?("Pruning worktree")
168
+ path = line.match(/Pruning worktree (.+)$/)[1]
169
+ puts " ✓ #{path}"
170
+ end
171
+ end
172
+ else
173
+ puts "No worktrees to prune. Git metadata is clean."
174
+ end
175
+
176
+ if options[:dry_run]
177
+ puts "\nDRY RUN - No changes were made."
178
+ end
179
+ end
180
+
181
+ # Clean up orphaned directories
182
+ #
183
+ # @param options [Hash] Command options
184
+ def cleanup_orphaned_directories(options)
185
+ puts "\nChecking for orphaned worktree directories..."
186
+
187
+ # Get current worktree root from configuration
188
+ config = @manager.configuration
189
+ worktree_root = config.absolute_root_path
190
+
191
+ unless Dir.exist?(worktree_root)
192
+ puts "Worktree root directory does not exist: #{worktree_root}"
193
+ return
194
+ end
195
+
196
+ # Get currently tracked worktrees
197
+ list_result = @manager.list_all
198
+ return unless list_result[:success]
199
+
200
+ tracked_paths = list_result[:worktrees].map { |wt| File.expand_path(wt.path) }
201
+
202
+ # Find directories in worktree root that are not tracked
203
+ orphaned_count = 0
204
+ Dir.glob(File.join(worktree_root, "*")).each do |path|
205
+ next unless File.directory?(path)
206
+ next if tracked_paths.include?(File.expand_path(path))
207
+
208
+ # Check if this looks like a worktree directory
209
+ if looks_like_worktree_directory?(path)
210
+ if options[:dry_run]
211
+ puts " Would remove orphaned directory: #{File.basename(path)}"
212
+ orphaned_count += 1
213
+ else
214
+ if options[:verbose]
215
+ puts " Removing orphaned directory: #{File.basename(path)}"
216
+ end
217
+
218
+ begin
219
+ FileUtils.rm_rf(path)
220
+ orphaned_count += 1
221
+ rescue => e
222
+ puts " Failed to remove #{path}: #{e.message}"
223
+ end
224
+ end
225
+ end
226
+ end
227
+
228
+ if orphaned_count > 0
229
+ action = options[:dry_run] ? "Would remove" : "Removed"
230
+ puts "#{action} #{orphaned_count} orphaned director(y/ies)."
231
+ else
232
+ puts "No orphaned directories found."
233
+ end
234
+ end
235
+
236
+ # Check if directory looks like a worktree
237
+ #
238
+ # @param path [String] Directory path
239
+ # @return [Boolean] true if it looks like a worktree directory
240
+ def looks_like_worktree_directory?(path)
241
+ # Check for common indicators of a worktree
242
+ indicators = [
243
+ File.join(path, ".git"), # Git directory (file)
244
+ File.join(path, "mise.toml"), # Mise configuration
245
+ File.join(path, ".mise"), # Mise directory
246
+ File.join(path, "package.json"), # Node.js project
247
+ File.join(path, "Gemfile"), # Ruby project
248
+ File.join(path, "Cargo.toml") # Rust project
249
+ ]
250
+
251
+ # Check if any indicators exist
252
+ indicators.any? { |indicator| File.exist?(indicator) }
253
+ rescue
254
+ false
255
+ end
256
+ end
257
+ end
258
+ end
259
+ end
260
+ end