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,249 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Git
5
+ module Worktree
6
+ module Commands
7
+ # Switch command
8
+ #
9
+ # Switches to a worktree by various identifiers (task ID, branch name,
10
+ # directory name, or path). Outputs the path for navigation.
11
+ #
12
+ # @example Switch by task ID
13
+ # SwitchCommand.new.run(["081"])
14
+ #
15
+ # @example Switch by branch name
16
+ # SwitchCommand.new.run(["feature-branch"])
17
+ #
18
+ # @example Switch and change directory
19
+ # cd $(ace-git-worktree switch 081)
20
+ class SwitchCommand
21
+ # Initialize a new SwitchCommand
22
+ #
23
+ # @param manager [Object] Optional manager dependency for testing
24
+ def initialize(manager: nil)
25
+ @manager = manager || Organisms::WorktreeManager.new
26
+ end
27
+
28
+ # Run the switch command
29
+ #
30
+ # @param args [Array<String>] Command arguments
31
+ # @return [Integer] Exit code (0 for success, 1 for error)
32
+ def run(args = [])
33
+ options = parse_arguments(args)
34
+ return show_help if options[:help]
35
+
36
+ validate_options(options)
37
+
38
+ # Handle list option
39
+ if options[:list]
40
+ result = @manager.list_all(format: :simple)
41
+ if result[:success] && result[:worktrees].any?
42
+ result[:worktrees].each do |worktree|
43
+ prefix = worktree.task_associated? ? "Task #{worktree.task_id}: " : ""
44
+ puts " #{prefix}#{worktree.branch || "detached"} (#{worktree.path})"
45
+ end
46
+ return 0
47
+ else
48
+ puts "No worktrees found. Use 'ace-git-worktree create' to create one."
49
+ return 0
50
+ end
51
+ end
52
+
53
+ result = @manager.switch(options[:identifier])
54
+
55
+ if result[:success]
56
+ display_switch_result(result, options)
57
+ 0
58
+ else
59
+ puts "Failed to switch worktree: #{result[:error]}"
60
+ display_alternatives(options[:identifier]) unless result[:error].include?("not found")
61
+ 1
62
+ end
63
+ rescue ArgumentError => e
64
+ puts "Error: #{e.message}"
65
+ puts
66
+ show_help
67
+ 1
68
+ rescue => e
69
+ puts "Error: #{e.message}"
70
+ 1
71
+ end
72
+
73
+ # Show help for the switch command
74
+ #
75
+ # @return [Integer] Exit code
76
+ def show_help
77
+ puts <<~HELP
78
+ ace-git-worktree switch - Switch to a worktree
79
+
80
+ USAGE:
81
+ ace-git-worktree switch <identifier>
82
+
83
+ IDENTIFIERS:
84
+ Task ID: 081, task.081, v.0.9.0+081
85
+ Branch name: feature-branch, main
86
+ Directory name: task.081, feature-branch
87
+ Full path: /path/to/worktree
88
+
89
+ OPTIONS:
90
+ --help, -h Show this help message
91
+ --list, -l List available worktrees
92
+ --verbose, -v Show detailed information
93
+
94
+ EXAMPLES:
95
+ # Switch by task ID
96
+ ace-git-worktree switch 081
97
+
98
+ # Switch by branch name
99
+ ace-git-worktree switch feature-branch
100
+
101
+ # Switch and change directory
102
+ cd $(ace-git-worktree switch 081)
103
+
104
+ # List available worktrees
105
+ ace-git-worktree switch --list
106
+
107
+ OUTPUT:
108
+ The command outputs the worktree path for use with cd:
109
+ $ ace-git-worktree switch 081
110
+ /project/.ace-wt/task.081
111
+
112
+ To change directory:
113
+ $ cd $(ace-git-worktree switch 081)
114
+
115
+ CONFIGURATION:
116
+ Worktree paths and naming are controlled by .ace/git/worktree.yml
117
+ HELP
118
+ 0
119
+ end
120
+
121
+ private
122
+
123
+ # Parse command line arguments
124
+ #
125
+ # @param args [Array<String>] Command arguments
126
+ # @return [Hash] Parsed options
127
+ def parse_arguments(args)
128
+ options = {
129
+ identifier: nil,
130
+ list: false,
131
+ verbose: false,
132
+ help: false
133
+ }
134
+
135
+ i = 0
136
+ while i < args.length
137
+ arg = args[i]
138
+
139
+ case arg
140
+ when "--list", "-l"
141
+ options[:list] = true
142
+ when "--branch"
143
+ i += 1
144
+ options[:identifier] = args[i]
145
+ when "--task"
146
+ i += 1
147
+ options[:identifier] = args[i]
148
+ when "--verbose", "-v"
149
+ options[:verbose] = true
150
+ when "--help", "-h"
151
+ options[:help] = true
152
+ when /^--/
153
+ raise ArgumentError, "Unknown option: #{arg}"
154
+ else
155
+ # Positional argument - worktree identifier
156
+ if options[:identifier]
157
+ raise ArgumentError, "Multiple identifiers specified: #{options[:identifier]} and #{arg}"
158
+ end
159
+ options[:identifier] = arg
160
+ end
161
+
162
+ i += 1
163
+ end
164
+
165
+ options
166
+ end
167
+
168
+ # Validate parsed options
169
+ #
170
+ # @param options [Hash] Parsed options
171
+ def validate_options(options)
172
+ if !options[:list] && !options[:identifier]
173
+ raise ArgumentError, "Must specify <identifier> or use --list option"
174
+ end
175
+
176
+ if options[:list] && options[:identifier]
177
+ raise ArgumentError, "Cannot specify both identifier and --list option"
178
+ end
179
+
180
+ # Security validation for identifiers (can be task ID, branch, or path)
181
+ if options[:identifier] && contains_dangerous_patterns?(options[:identifier])
182
+ raise ArgumentError, "Identifier contains potentially dangerous characters"
183
+ end
184
+ end
185
+
186
+ # Check if a string contains dangerous patterns
187
+ #
188
+ # Matches TaskFetcher's validation to ensure consistent security boundaries.
189
+ # Rejects shell metacharacters, null bytes, newlines, redirects, and path traversal.
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
+ # Patterns from TaskFetcher.valid_task_reference? for consistency
197
+ dangerous_patterns = [
198
+ /[;&|`$(){}\[\]]/, # Shell metacharacters
199
+ /\x00/, # Null bytes
200
+ /[\r\n]/, # Newlines
201
+ /[<>]/, # Redirects
202
+ /\.\./ # Path traversal
203
+ ]
204
+
205
+ dangerous_patterns.any? { |pattern| value.match?(pattern) }
206
+ end
207
+
208
+ # Display switch result
209
+ #
210
+ # @param result [Hash] Switch result
211
+ # @param options [Hash] Command options
212
+ def display_switch_result(result, options)
213
+ # Just output the path for use with cd
214
+ puts result[:worktree_path]
215
+
216
+ if options[:verbose]
217
+ puts "\nWorktree details:"
218
+ puts " Branch: #{result[:branch]}"
219
+ puts " Task ID: #{result[:task_id]}" if result[:task_id]
220
+ puts " Description: #{result[:description]}"
221
+ end
222
+ end
223
+
224
+ # Display alternatives when worktree not found
225
+ #
226
+ # @param identifier [String] The identifier that wasn't found
227
+ def display_alternatives(identifier)
228
+ puts "\nAvailable worktrees:"
229
+
230
+ result = @manager.list_all(format: :simple)
231
+ if result[:success] && result[:worktrees].any?
232
+ result[:worktrees].each do |worktree|
233
+ prefix = worktree.task_associated? ? "Task #{worktree.task_id}: " : ""
234
+ puts " #{prefix}#{worktree.branch || "detached"} (#{worktree.path})"
235
+ end
236
+ else
237
+ puts " No worktrees found. Use 'ace-git-worktree create' to create one."
238
+ end
239
+
240
+ puts "\nSuggestions:"
241
+ puts " • Check the worktree identifier spelling"
242
+ puts " • Use 'ace-git-worktree list' to see available worktrees"
243
+ puts " • Use 'ace-git-worktree create' to create a new worktree"
244
+ end
245
+ end
246
+ end
247
+ end
248
+ end
249
+ end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Git
5
+ module Worktree
6
+ # Configuration module
7
+ #
8
+ # Provides constants and utility methods for the ace-git-worktree gem.
9
+ # Configuration values are loaded from .ace/git/worktree.yml via ace-core cascade.
10
+ # Default values are defined in WorktreeConfig::DEFAULT_CONFIG.
11
+ module Configuration
12
+ # Gem name and identifier
13
+ GEM_NAME = "ace-git-worktree"
14
+
15
+ # File and directory names
16
+ MISE_CONFIG_FILE = "mise.toml"
17
+ CONFIG_FILE = "worktree.yml"
18
+
19
+ # Configuration paths
20
+ CONFIG_NAMESPACE = ["git", "worktree"].freeze
21
+
22
+ # Template variable patterns
23
+ TEMPLATE_VARIABLES = %w[id task_id slug].freeze
24
+
25
+ # Git branch name restrictions
26
+ FORBIDDEN_BRANCH_CHARS = /[~\^:*?\[\]]/
27
+ SEPARATOR_CHARS = /[ ._\/\\]+/
28
+
29
+ # Task status values
30
+ TASK_STATUSES = %w[pending in-progress done blocked].freeze
31
+
32
+ # Task priority values
33
+ TASK_PRIORITIES = %w[high medium low].freeze
34
+
35
+ # Output formats
36
+ OUTPUT_FORMATS = %w[table json simple].freeze
37
+
38
+ # CLI command names and aliases
39
+ CLI_COMMANDS = {
40
+ "create" => "Create a new worktree",
41
+ "list" => "List all worktrees",
42
+ "switch" => "Switch to a worktree",
43
+ "remove" => "Remove a worktree",
44
+ "prune" => "Clean up deleted worktrees",
45
+ "config" => "Show/manage configuration"
46
+ }.freeze
47
+
48
+ CLI_ALIASES = {
49
+ "ls" => "list",
50
+ "rm" => "remove",
51
+ "cd" => "switch"
52
+ }.freeze
53
+
54
+ # Help text templates
55
+ HELP_TEMPLATES = {
56
+ usage: "ace-git-worktree <command> [OPTIONS]",
57
+ examples: "See 'ace-git-worktree <command> --help' for examples",
58
+ config_help: "See 'ace-git-worktree config --files' for configuration locations"
59
+ }.freeze
60
+
61
+ # Error messages
62
+ ERROR_MESSAGES = {
63
+ not_git_repo: "Not in a git repository",
64
+ task_not_found: "Task not found",
65
+ worktree_not_found: "Worktree not found",
66
+ config_invalid: "Invalid configuration",
67
+ command_failed: "Command failed",
68
+ unexpected_error: "Unexpected error"
69
+ }.freeze
70
+
71
+ # Success messages
72
+ SUCCESS_MESSAGES = {
73
+ worktree_created: "Worktree created successfully",
74
+ worktree_removed: "Worktree removed successfully",
75
+ config_valid: "Configuration is valid",
76
+ cleanup_completed: "Cleanup completed successfully"
77
+ }.freeze
78
+
79
+ # Warning messages
80
+ WARNING_MESSAGES = {
81
+ mise_trust_failed: "Failed to trust mise configuration",
82
+ task_commit_failed: "Failed to commit task changes",
83
+ metadata_add_failed: "Failed to add worktree metadata",
84
+ uncommitted_changes: "Worktree has uncommitted changes"
85
+ }.freeze
86
+
87
+ # Validate template variables
88
+ #
89
+ # @param template [String] Template string to validate
90
+ # @return [Array<String>] Array of missing variables (empty if valid)
91
+ def self.validate_template_variables(template)
92
+ return [] unless template.is_a?(String)
93
+
94
+ used_variables = template.scan(/\{([^}]+)\}/).flatten
95
+ TEMPLATE_VARIABLES - used_variables
96
+ end
97
+
98
+ # Validate configuration hash
99
+ #
100
+ # @param config [Hash] Configuration to validate
101
+ # @return [Array<String>] Array of error messages (empty if valid)
102
+ def self.validate_configuration(config)
103
+ errors = []
104
+
105
+ # Validate root_path
106
+ unless config["root_path"].is_a?(String) && !config["root_path"].empty?
107
+ errors << "root_path must be a non-empty string"
108
+ end
109
+
110
+ # Validate task section
111
+ task_config = config["task"] || {}
112
+
113
+ unless task_config["directory_format"].is_a?(String) && !task_config["directory_format"].empty?
114
+ errors << "task.directory_format must be a non-empty string"
115
+ end
116
+
117
+ unless task_config["branch_format"].is_a?(String) && !task_config["branch_format"].empty?
118
+ errors << "task.branch_format must be a non-empty string"
119
+ end
120
+
121
+ # Validate template variables
122
+ [task_config["directory_format"], task_config["branch_format"], task_config["commit_message_format"]].each do |template|
123
+ next unless template.is_a?(String)
124
+
125
+ missing = validate_template_variables(template)
126
+ if missing.any?
127
+ errors << "#{template} should include variables: #{missing.join(", ")}"
128
+ end
129
+ end
130
+
131
+ errors
132
+ end
133
+
134
+ # Get command description
135
+ #
136
+ # @param command_name [String] Command name
137
+ # @return [String] Command description or nil if not found
138
+ def self.get_command_description(command_name)
139
+ CLI_COMMANDS[command_name] || CLI_ALIASES[command_name]
140
+ end
141
+
142
+ # Check if command exists
143
+ #
144
+ # @param command_name [String] Command name
145
+ # @return [Boolean] true if command exists
146
+ def self.command_exists?(command_name)
147
+ CLI_COMMANDS.key?(command_name) || CLI_ALIASES.key?(command_name)
148
+ end
149
+
150
+ # Resolve command alias
151
+ #
152
+ # @param command_name [String] Command name or alias
153
+ # @return [String] Resolved command name
154
+ def self.resolve_command_alias(command_name)
155
+ CLI_ALIASES[command_name] || command_name
156
+ end
157
+
158
+ # Get all available commands
159
+ #
160
+ # @return [Array<String>] Array of command names
161
+ def self.available_commands
162
+ (CLI_COMMANDS.keys + CLI_ALIASES.keys).sort
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end