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,502 @@
|
|
|
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 Models
|
|
9
|
+
# Configuration model for worktree settings
|
|
10
|
+
#
|
|
11
|
+
# Represents the configuration loaded from .ace/git/worktree.yml
|
|
12
|
+
# with defaults, validation, and accessors for all configuration options.
|
|
13
|
+
#
|
|
14
|
+
# @example Create configuration with defaults
|
|
15
|
+
# config = WorktreeConfig.new
|
|
16
|
+
# config.root_path # => ".ace-wt"
|
|
17
|
+
#
|
|
18
|
+
# @example Load from configuration hash
|
|
19
|
+
# config = WorktreeConfig.new({
|
|
20
|
+
# "git" => {
|
|
21
|
+
# "worktree" => {
|
|
22
|
+
# "root_path" => "~/worktrees",
|
|
23
|
+
# "mise_trust_auto" => false
|
|
24
|
+
# }
|
|
25
|
+
# }
|
|
26
|
+
# })
|
|
27
|
+
class WorktreeConfig
|
|
28
|
+
# Default configuration values
|
|
29
|
+
DEFAULT_CONFIG = {
|
|
30
|
+
"root_path" => ".ace-wt",
|
|
31
|
+
"auto_navigate" => true,
|
|
32
|
+
"tmux" => false,
|
|
33
|
+
"mise_trust_auto" => true,
|
|
34
|
+
"task" => {
|
|
35
|
+
"directory_format" => "t.{task_id}",
|
|
36
|
+
"branch_format" => "{id}-{slug}",
|
|
37
|
+
"auto_mark_in_progress" => true,
|
|
38
|
+
"auto_commit_task" => true,
|
|
39
|
+
"auto_push_task" => true,
|
|
40
|
+
"push_remote" => "origin",
|
|
41
|
+
"commit_message_format" => "chore({task_id}): mark as in-progress, creating worktree for {slug}",
|
|
42
|
+
"add_worktree_metadata" => true,
|
|
43
|
+
"auto_setup_upstream" => false,
|
|
44
|
+
"auto_create_pr" => false,
|
|
45
|
+
"pr_title_format" => "{id} - {slug}",
|
|
46
|
+
"create_current_symlink" => true,
|
|
47
|
+
"current_symlink_name" => "_current"
|
|
48
|
+
},
|
|
49
|
+
"pr" => {
|
|
50
|
+
"directory_format" => "ace-pr-{number}",
|
|
51
|
+
"branch_format" => "pr-{number}-{slug}",
|
|
52
|
+
"remote_name" => "origin",
|
|
53
|
+
"fetch_before_create" => true,
|
|
54
|
+
"configure_push_for_mismatch" => true
|
|
55
|
+
},
|
|
56
|
+
"branch" => {
|
|
57
|
+
"fetch_if_remote" => true,
|
|
58
|
+
"auto_detect_remote" => true
|
|
59
|
+
},
|
|
60
|
+
"cleanup" => {
|
|
61
|
+
"on_merge" => false,
|
|
62
|
+
"on_delete" => true
|
|
63
|
+
},
|
|
64
|
+
"hooks" => {
|
|
65
|
+
"after_create" => []
|
|
66
|
+
}
|
|
67
|
+
}.freeze
|
|
68
|
+
|
|
69
|
+
# Configuration namespace paths
|
|
70
|
+
CONFIG_NAMESPACE = ["git", "worktree"].freeze
|
|
71
|
+
|
|
72
|
+
attr_reader :root_path, :auto_navigate, :tmux, :mise_trust_auto, :task_config, :pr_config, :branch_config, :cleanup_config, :hooks_config
|
|
73
|
+
|
|
74
|
+
# Initialize a new WorktreeConfig
|
|
75
|
+
#
|
|
76
|
+
# @param config_hash [Hash] Configuration hash (typically from ace-core)
|
|
77
|
+
# @param project_root [String] Project root directory for relative paths
|
|
78
|
+
def initialize(config_hash = {}, project_root = Dir.pwd)
|
|
79
|
+
@project_root = project_root
|
|
80
|
+
@raw_config = extract_worktree_config(config_hash)
|
|
81
|
+
@merged_config = merge_with_defaults(@raw_config)
|
|
82
|
+
|
|
83
|
+
initialize_attributes
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Get the directory format for task-based worktrees
|
|
87
|
+
#
|
|
88
|
+
# @return [String] Directory format template
|
|
89
|
+
def directory_format
|
|
90
|
+
@task_config["directory_format"]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Get the branch format for task-based worktrees
|
|
94
|
+
#
|
|
95
|
+
# @return [String] Branch format template
|
|
96
|
+
def branch_format
|
|
97
|
+
@task_config["branch_format"]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Check if auto-navigation should be performed
|
|
101
|
+
#
|
|
102
|
+
# @return [Boolean] true if auto-navigation should be performed
|
|
103
|
+
def auto_navigate?
|
|
104
|
+
@auto_navigate
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Check if tmux session should be launched after worktree creation
|
|
108
|
+
#
|
|
109
|
+
# @return [Boolean] true if tmux launch is enabled
|
|
110
|
+
def tmux?
|
|
111
|
+
@tmux
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Check if mise should automatically trust worktree directories
|
|
115
|
+
#
|
|
116
|
+
# @return [Boolean] true if mise auto-trust is enabled
|
|
117
|
+
def mise_trust_auto?
|
|
118
|
+
@mise_trust_auto
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Check if tasks should be marked as in-progress automatically
|
|
122
|
+
#
|
|
123
|
+
# @return [Boolean] true if tasks should be marked in-progress
|
|
124
|
+
def auto_mark_in_progress?
|
|
125
|
+
@task_config["auto_mark_in_progress"]
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Check if task changes should be committed automatically
|
|
129
|
+
#
|
|
130
|
+
# @return [Boolean] true if task changes should be committed
|
|
131
|
+
def auto_commit_task?
|
|
132
|
+
@task_config["auto_commit_task"]
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Check if task changes should be pushed automatically
|
|
136
|
+
#
|
|
137
|
+
# @return [Boolean] true if task changes should be pushed
|
|
138
|
+
def auto_push_task?
|
|
139
|
+
@task_config["auto_push_task"] != false
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Get the remote for pushing task changes
|
|
143
|
+
#
|
|
144
|
+
# @return [String] Remote name (default: "origin")
|
|
145
|
+
def push_remote
|
|
146
|
+
@task_config["push_remote"] || "origin"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Get the commit message format for task updates
|
|
150
|
+
#
|
|
151
|
+
# @return [String] Commit message template
|
|
152
|
+
def commit_message_format
|
|
153
|
+
@task_config["commit_message_format"]
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Check if worktree metadata should be added to tasks
|
|
157
|
+
#
|
|
158
|
+
# @return [Boolean] true if metadata should be added
|
|
159
|
+
def add_worktree_metadata?
|
|
160
|
+
@task_config["add_worktree_metadata"]
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Check if new worktree branch should be pushed to remote with upstream tracking
|
|
164
|
+
#
|
|
165
|
+
# @return [Boolean] true if upstream setup is enabled
|
|
166
|
+
def auto_setup_upstream?
|
|
167
|
+
@task_config["auto_setup_upstream"]
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Check if draft PR should be created automatically
|
|
171
|
+
#
|
|
172
|
+
# @return [Boolean] true if auto PR creation is enabled
|
|
173
|
+
def auto_create_pr?
|
|
174
|
+
@task_config["auto_create_pr"]
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Get the PR title format template
|
|
178
|
+
#
|
|
179
|
+
# @return [String] PR title format template
|
|
180
|
+
def pr_title_format
|
|
181
|
+
@task_config["pr_title_format"]
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Check if _current symlink should be created
|
|
185
|
+
#
|
|
186
|
+
# @return [Boolean] true if symlink should be created
|
|
187
|
+
def create_current_symlink?
|
|
188
|
+
@task_config["create_current_symlink"] != false
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Get the name for the _current symlink
|
|
192
|
+
#
|
|
193
|
+
# @return [String] Symlink name (default: "_current")
|
|
194
|
+
def current_symlink_name
|
|
195
|
+
@task_config["current_symlink_name"] || "_current"
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Format a PR title using task data
|
|
199
|
+
#
|
|
200
|
+
# @param task_data [Hash] Task data hash from ace-task
|
|
201
|
+
# @return [String] Formatted PR title
|
|
202
|
+
#
|
|
203
|
+
# @example
|
|
204
|
+
# config.format_pr_title(task) # => "081 - fix-authentication-bug"
|
|
205
|
+
def format_pr_title(task_data)
|
|
206
|
+
template = pr_title_format
|
|
207
|
+
apply_template_variables(template, task_data)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Get the root path for worktrees (expanded and absolute)
|
|
211
|
+
#
|
|
212
|
+
# @return [String] Absolute path to worktree root directory
|
|
213
|
+
def absolute_root_path
|
|
214
|
+
@absolute_root_path ||= expand_root_path
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Check if worktrees should be cleaned up on branch merge
|
|
218
|
+
#
|
|
219
|
+
# @return [Boolean] true if cleanup on merge
|
|
220
|
+
def cleanup_on_merge?
|
|
221
|
+
@cleanup_config["on_merge"]
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Check if worktrees should be cleaned up on branch delete
|
|
225
|
+
#
|
|
226
|
+
# @return [Boolean] true if cleanup on delete
|
|
227
|
+
def cleanup_on_delete?
|
|
228
|
+
@cleanup_config["on_delete"]
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Get after-create hooks configuration
|
|
232
|
+
#
|
|
233
|
+
# @return [Array<Hash>] Array of hook definitions
|
|
234
|
+
def after_create_hooks
|
|
235
|
+
@hooks_config["after_create"] || []
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Check if push should be configured for PR branches with name mismatches
|
|
239
|
+
#
|
|
240
|
+
# @return [Boolean] true if push should be configured for mismatched branch names
|
|
241
|
+
def configure_push_for_mismatch?
|
|
242
|
+
@pr_config["configure_push_for_mismatch"]
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Check if hooks are configured
|
|
246
|
+
#
|
|
247
|
+
# @return [Boolean] true if any hooks are configured
|
|
248
|
+
def hooks_enabled?
|
|
249
|
+
hooks = after_create_hooks
|
|
250
|
+
hooks.is_a?(Array) && hooks.any?
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Format a directory path using task data
|
|
254
|
+
#
|
|
255
|
+
# @param task_data [Hash] Task data hash from ace-task
|
|
256
|
+
# @param counter [Integer, nil] Counter for multiple worktrees of same task
|
|
257
|
+
# @return [String] Formatted directory path
|
|
258
|
+
#
|
|
259
|
+
# @example
|
|
260
|
+
# config.format_directory(task) # => "t.081"
|
|
261
|
+
# config.format_directory(task, 2) # => "t.081-2"
|
|
262
|
+
def format_directory(task_data, counter = nil)
|
|
263
|
+
template = directory_format
|
|
264
|
+
formatted = apply_template_variables(template, task_data)
|
|
265
|
+
|
|
266
|
+
# Add counter if provided
|
|
267
|
+
formatted = "#{formatted}-#{counter}" if counter
|
|
268
|
+
|
|
269
|
+
formatted
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Format a branch name using task data
|
|
273
|
+
#
|
|
274
|
+
# @param task_data [Hash] Task data hash from ace-task
|
|
275
|
+
# @return [String] Formatted branch name
|
|
276
|
+
#
|
|
277
|
+
# @example
|
|
278
|
+
# config.format_branch(task) # => "081-fix-authentication-bug"
|
|
279
|
+
def format_branch(task_data)
|
|
280
|
+
template = branch_format
|
|
281
|
+
apply_template_variables(template, task_data)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Format a commit message for task updates
|
|
285
|
+
#
|
|
286
|
+
# @param task_data [Hash] Task data hash from ace-task
|
|
287
|
+
# @return [String] Formatted commit message
|
|
288
|
+
#
|
|
289
|
+
# @example
|
|
290
|
+
# config.format_commit_message(task) # => "chore(task-081): mark as in-progress, creating worktree"
|
|
291
|
+
def format_commit_message(task_data)
|
|
292
|
+
template = commit_message_format
|
|
293
|
+
apply_template_variables(template, task_data)
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Validate configuration settings
|
|
297
|
+
#
|
|
298
|
+
# @return [Array<String>] Array of validation error messages (empty if valid)
|
|
299
|
+
def validate
|
|
300
|
+
errors = []
|
|
301
|
+
|
|
302
|
+
# Validate root_path
|
|
303
|
+
unless root_path.is_a?(String) && !root_path.empty?
|
|
304
|
+
errors << "root_path must be a non-empty string"
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Validate task template formats
|
|
308
|
+
unless directory_format.is_a?(String) && !directory_format.empty?
|
|
309
|
+
errors << "task.directory_format must be a non-empty string"
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
unless branch_format.is_a?(String) && !branch_format.empty?
|
|
313
|
+
errors << "task.branch_format must be a non-empty string"
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Validate PR template formats
|
|
317
|
+
pr_dir_format = @pr_config["directory_format"]
|
|
318
|
+
unless pr_dir_format.is_a?(String) && !pr_dir_format.empty?
|
|
319
|
+
errors << "pr.directory_format must be a non-empty string"
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
pr_branch_format = @pr_config["branch_format"]
|
|
323
|
+
unless pr_branch_format.is_a?(String) && !pr_branch_format.empty?
|
|
324
|
+
errors << "pr.branch_format must be a non-empty string"
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# Validate template variables for task configuration
|
|
328
|
+
task_templates = {
|
|
329
|
+
"task.directory_format" => {template: directory_format, valid_vars: %w[task_id id slug]},
|
|
330
|
+
"task.branch_format" => {template: branch_format, valid_vars: %w[id slug task_id]},
|
|
331
|
+
"task.commit_message_format" => {template: commit_message_format, valid_vars: %w[task_id slug id]}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
task_templates.each do |name, config|
|
|
335
|
+
invalid_vars = find_invalid_template_variables(config[:template], config[:valid_vars])
|
|
336
|
+
if invalid_vars.any?
|
|
337
|
+
errors << "#{name} contains invalid variables: #{invalid_vars.join(", ")}. Valid variables: #{config[:valid_vars].map { |v| "{#{v}}" }.join(", ")}"
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# Warn if template has no variables (except for commit_message_format which is optional)
|
|
341
|
+
unless name == "task.commit_message_format"
|
|
342
|
+
if config[:template] && !config[:template].match?(/\{[^}]+\}/)
|
|
343
|
+
errors << "#{name} should include at least one template variable from: #{config[:valid_vars].map { |v| "{#{v}}" }.join(", ")}"
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# Validate template variables for PR configuration
|
|
349
|
+
pr_templates = {
|
|
350
|
+
"pr.directory_format" => {template: pr_dir_format, valid_vars: %w[number slug title base_branch]},
|
|
351
|
+
"pr.branch_format" => {template: pr_branch_format, valid_vars: %w[number slug title base_branch]}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
pr_templates.each do |name, config|
|
|
355
|
+
invalid_vars = find_invalid_template_variables(config[:template], config[:valid_vars])
|
|
356
|
+
if invalid_vars.any?
|
|
357
|
+
errors << "#{name} contains invalid variables: #{invalid_vars.join(", ")}. Valid variables: #{config[:valid_vars].map { |v| "{#{v}}" }.join(", ")}"
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
errors
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# Get configuration as a hash
|
|
365
|
+
#
|
|
366
|
+
# @return [Hash] Configuration hash
|
|
367
|
+
def to_h
|
|
368
|
+
{
|
|
369
|
+
root_path: root_path,
|
|
370
|
+
auto_navigate: auto_navigate?,
|
|
371
|
+
mise_trust_auto: mise_trust_auto?,
|
|
372
|
+
task: @task_config.dup,
|
|
373
|
+
cleanup: @cleanup_config.dup,
|
|
374
|
+
hooks: @hooks_config.dup
|
|
375
|
+
}
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
private
|
|
379
|
+
|
|
380
|
+
# Find invalid template variables in a template string
|
|
381
|
+
#
|
|
382
|
+
# @param template [String] Template string with {variable} placeholders
|
|
383
|
+
# @param valid_vars [Array<String>] List of valid variable names
|
|
384
|
+
# @return [Array<String>] List of invalid variable names
|
|
385
|
+
def find_invalid_template_variables(template, valid_vars)
|
|
386
|
+
return [] unless template.is_a?(String)
|
|
387
|
+
|
|
388
|
+
# Extract all variables from template
|
|
389
|
+
template_vars = template.scan(/\{(\w+)\}/).flatten
|
|
390
|
+
|
|
391
|
+
# Find variables that are not in the valid list
|
|
392
|
+
template_vars.uniq.reject { |var| valid_vars.include?(var) }
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
# Extract worktree configuration from nested config hash
|
|
396
|
+
#
|
|
397
|
+
# @param config_hash [Hash] Full configuration hash
|
|
398
|
+
# @return [Hash] Worktree-specific configuration
|
|
399
|
+
def extract_worktree_config(config_hash)
|
|
400
|
+
CONFIG_NAMESPACE.reduce(config_hash) do |current, key|
|
|
401
|
+
current&.dig(key) || {}
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# Merge configuration with defaults
|
|
406
|
+
#
|
|
407
|
+
# @param config [Hash] User configuration
|
|
408
|
+
# @return [Hash] Merged configuration
|
|
409
|
+
def merge_with_defaults(config)
|
|
410
|
+
deep_merge(DEFAULT_CONFIG.dup, config)
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
# Deep merge two hashes
|
|
414
|
+
#
|
|
415
|
+
# @param target [Hash] Target hash
|
|
416
|
+
# @param source [Hash] Source hash
|
|
417
|
+
# @return [Hash] Merged hash
|
|
418
|
+
def deep_merge(target, source)
|
|
419
|
+
target.merge(source) do |key, old_val, new_val|
|
|
420
|
+
if old_val.is_a?(Hash) && new_val.is_a?(Hash)
|
|
421
|
+
deep_merge(old_val, new_val)
|
|
422
|
+
else
|
|
423
|
+
new_val.nil? ? old_val : new_val
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
# Initialize instance attributes from merged configuration
|
|
429
|
+
def initialize_attributes
|
|
430
|
+
@root_path = @merged_config["root_path"]
|
|
431
|
+
@auto_navigate = @merged_config["auto_navigate"]
|
|
432
|
+
@tmux = @merged_config["tmux"]
|
|
433
|
+
@mise_trust_auto = @merged_config["mise_trust_auto"]
|
|
434
|
+
@task_config = @merged_config["task"] || {}
|
|
435
|
+
@pr_config = @merged_config["pr"] || {}
|
|
436
|
+
@branch_config = @merged_config["branch"] || {}
|
|
437
|
+
@cleanup_config = @merged_config["cleanup"] || {}
|
|
438
|
+
@hooks_config = @merged_config["hooks"] || {}
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
# Expand root path to absolute path
|
|
442
|
+
#
|
|
443
|
+
# @return [String] Absolute path
|
|
444
|
+
def expand_root_path
|
|
445
|
+
require_relative "../atoms/path_expander"
|
|
446
|
+
Atoms::PathExpander.expand(@root_path, @project_root)
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
# Apply template variables to a format string
|
|
450
|
+
#
|
|
451
|
+
# @param template [String] Template string with {variable} placeholders
|
|
452
|
+
# @param task_data [Hash] Task data hash from ace-task
|
|
453
|
+
# @return [String] Formatted string with variables replaced
|
|
454
|
+
def apply_template_variables(template, task_data)
|
|
455
|
+
formatted = template.dup
|
|
456
|
+
|
|
457
|
+
# Extract task number from ID for backward compatibility
|
|
458
|
+
task_id = extract_task_number(task_data)
|
|
459
|
+
|
|
460
|
+
# Available template variables
|
|
461
|
+
variables = {
|
|
462
|
+
"id" => task_id,
|
|
463
|
+
"task_id" => task_id,
|
|
464
|
+
"slug" => create_slug(task_data[:title] || "unknown-task")
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
# Replace each variable
|
|
468
|
+
variables.each do |key, value|
|
|
469
|
+
formatted = formatted.gsub("{#{key}}", value.to_s)
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
formatted
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
# Extract task number from task data
|
|
476
|
+
#
|
|
477
|
+
# @param task_data [Hash] Task data hash
|
|
478
|
+
# @return [String] Task number (e.g., "094")
|
|
479
|
+
def extract_task_number(task_data)
|
|
480
|
+
# Use shared extractor that preserves subtask IDs (e.g., "121.01")
|
|
481
|
+
Atoms::TaskIDExtractor.extract(task_data)
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
# Create URL-friendly slug from title
|
|
485
|
+
#
|
|
486
|
+
# @param title [String] Task title
|
|
487
|
+
# @return [String] URL-friendly slug
|
|
488
|
+
def create_slug(title)
|
|
489
|
+
return "unknown-task" unless title
|
|
490
|
+
|
|
491
|
+
# Convert to lowercase, replace spaces and special chars with hyphens
|
|
492
|
+
title.downcase
|
|
493
|
+
.gsub(/[^a-z0-9\s-]/, "") # Remove special chars except spaces and hyphens
|
|
494
|
+
.gsub(/\s+/, "-").squeeze("-") # Replace multiple hyphens with single
|
|
495
|
+
.gsub(/^-|-$/, "") # Remove leading/trailing hyphens
|
|
496
|
+
.tap { |slug| slug.empty? ? "unknown-task" : slug }
|
|
497
|
+
end
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
end
|
|
501
|
+
end
|
|
502
|
+
end
|