worktree_manager 0.2.1
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/.github/workflows/test.yml +52 -0
- data/.gitignore +37 -0
- data/.mise.toml +9 -0
- data/.rspec +3 -0
- data/.version +1 -0
- data/.worktree.yml.example +45 -0
- data/CHANGELOG.md +50 -0
- data/CLAUDE.md +17 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +53 -0
- data/LICENSE +9 -0
- data/README.md +391 -0
- data/Rakefile +6 -0
- data/bin/wm +6 -0
- data/bin/worktree_manager +6 -0
- data/lib/worktree_manager/cli.rb +794 -0
- data/lib/worktree_manager/config_manager.rb +64 -0
- data/lib/worktree_manager/hook_manager.rb +269 -0
- data/lib/worktree_manager/manager.rb +126 -0
- data/lib/worktree_manager/version.rb +3 -0
- data/lib/worktree_manager/worktree.rb +56 -0
- data/lib/worktree_manager.rb +11 -0
- data/mise/release.sh +120 -0
- data/pkg/worktree_manager-0.1.0.gem +0 -0
- data/spec/integration/worktree_integration_spec.rb +288 -0
- data/spec/spec_helper.rb +34 -0
- data/spec/worktree_manager/cli_help_spec.rb +154 -0
- data/spec/worktree_manager/cli_spec.rb +976 -0
- data/spec/worktree_manager/config_manager_spec.rb +172 -0
- data/spec/worktree_manager/hook_manager_spec.rb +581 -0
- data/spec/worktree_manager/manager_spec.rb +95 -0
- data/spec/worktree_manager/worktree_spec.rb +83 -0
- data/spec/worktree_manager_spec.rb +14 -0
- data/worktree_manager.gemspec +33 -0
- metadata +136 -0
@@ -0,0 +1,581 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'tempfile'
|
3
|
+
require 'tmpdir'
|
4
|
+
|
5
|
+
RSpec.describe WorktreeManager::HookManager do
|
6
|
+
let(:temp_dir) { Dir.mktmpdir }
|
7
|
+
let(:hook_manager) { WorktreeManager::HookManager.new(temp_dir) }
|
8
|
+
|
9
|
+
after do
|
10
|
+
FileUtils.rm_rf(temp_dir)
|
11
|
+
end
|
12
|
+
|
13
|
+
describe '#initialize' do
|
14
|
+
it 'creates a hook manager instance' do
|
15
|
+
expect(hook_manager).to be_a(WorktreeManager::HookManager)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '#execute_hook' do
|
20
|
+
context 'when no hook file exists' do
|
21
|
+
it 'returns true for any hook type' do
|
22
|
+
expect(hook_manager.execute_hook(:pre_add)).to be true
|
23
|
+
expect(hook_manager.execute_hook(:post_add)).to be true
|
24
|
+
expect(hook_manager.execute_hook(:pre_remove)).to be true
|
25
|
+
expect(hook_manager.execute_hook(:post_remove)).to be true
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
context 'when hook file exists' do
|
30
|
+
let(:hook_file) { File.join(temp_dir, '.worktree.yml') }
|
31
|
+
|
32
|
+
context 'with string command' do
|
33
|
+
before do
|
34
|
+
File.write(hook_file, YAML.dump({
|
35
|
+
'pre_add' => "echo 'Pre-add hook executed'"
|
36
|
+
}))
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'executes the command and returns true' do
|
40
|
+
expect(hook_manager.execute_hook(:pre_add)).to be true
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'executes the command with context' do
|
44
|
+
context = { path: '/test/path', branch: 'main' }
|
45
|
+
expect(hook_manager.execute_hook(:pre_add, context)).to be true
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
context 'with array of commands' do
|
50
|
+
before do
|
51
|
+
File.write(hook_file, YAML.dump({
|
52
|
+
'pre_add' => ["echo 'First command'", "echo 'Second command'"]
|
53
|
+
}))
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'executes all commands and returns true if all succeed' do
|
57
|
+
expect(hook_manager.execute_hook(:pre_add)).to be true
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
context 'with hash configuration' do
|
62
|
+
before do
|
63
|
+
File.write(hook_file, YAML.dump({
|
64
|
+
'pre_add' => {
|
65
|
+
'command' => "echo 'Hook with config'",
|
66
|
+
'stop_on_error' => true
|
67
|
+
}
|
68
|
+
}))
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'executes the command from hash config' do
|
72
|
+
expect(hook_manager.execute_hook(:pre_add)).to be true
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
context 'when command fails' do
|
77
|
+
before do
|
78
|
+
File.write(hook_file, YAML.dump({
|
79
|
+
'pre_add' => 'exit 1'
|
80
|
+
}))
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'returns false when command fails' do
|
84
|
+
expect(hook_manager.execute_hook(:pre_add)).to be false
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
context 'with invalid hook type' do
|
89
|
+
it 'returns true for invalid hook types' do
|
90
|
+
expect(hook_manager.execute_hook(:invalid_hook)).to be true
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
context 'with malformed YAML' do
|
96
|
+
let(:hook_file) { File.join(temp_dir, '.worktree.yml') }
|
97
|
+
|
98
|
+
before do
|
99
|
+
File.write(hook_file, 'invalid: yaml: content: [')
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'handles YAML parsing errors gracefully' do
|
103
|
+
expect { hook_manager.execute_hook(:pre_add) }.not_to raise_error
|
104
|
+
expect(hook_manager.execute_hook(:pre_add)).to be true
|
105
|
+
end
|
106
|
+
|
107
|
+
context 'with new config structure' do
|
108
|
+
before do
|
109
|
+
File.write(hook_file, YAML.dump({
|
110
|
+
'hooks' => {
|
111
|
+
'pre_add' => {
|
112
|
+
'commands' => ["echo 'Command 1'", "echo 'Command 2'"]
|
113
|
+
}
|
114
|
+
}
|
115
|
+
}))
|
116
|
+
end
|
117
|
+
|
118
|
+
it 'executes commands from new structure' do
|
119
|
+
expect(hook_manager.execute_hook(:pre_add)).to be true
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
context 'with pwd configuration' do
|
124
|
+
let(:test_dir) { File.join(temp_dir, 'test_work_dir') }
|
125
|
+
|
126
|
+
before do
|
127
|
+
Dir.mkdir(test_dir)
|
128
|
+
File.write(hook_file, YAML.dump({
|
129
|
+
'hooks' => {
|
130
|
+
'pre_add' => {
|
131
|
+
'commands' => ['pwd > pwd.txt'],
|
132
|
+
'pwd' => test_dir
|
133
|
+
}
|
134
|
+
}
|
135
|
+
}))
|
136
|
+
end
|
137
|
+
|
138
|
+
it 'executes commands in specified working directory' do
|
139
|
+
hook_manager.execute_hook(:pre_add)
|
140
|
+
pwd_file = File.join(test_dir, 'pwd.txt')
|
141
|
+
expect(File.exist?(pwd_file)).to be true
|
142
|
+
expect(File.realpath(File.read(pwd_file).strip)).to eq(File.realpath(test_dir))
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
context 'with environment variable substitution in pwd' do
|
147
|
+
let(:test_worktree) { File.join(temp_dir, 'test-worktree') }
|
148
|
+
|
149
|
+
before do
|
150
|
+
Dir.mkdir(test_worktree)
|
151
|
+
File.write(hook_file, YAML.dump({
|
152
|
+
'hooks' => {
|
153
|
+
'post_add' => {
|
154
|
+
'commands' => ['pwd > pwd.txt'],
|
155
|
+
'pwd' => '$WORKTREE_ABSOLUTE_PATH'
|
156
|
+
}
|
157
|
+
}
|
158
|
+
}))
|
159
|
+
end
|
160
|
+
|
161
|
+
it 'substitutes environment variables in pwd' do
|
162
|
+
context = { path: test_worktree }
|
163
|
+
hook_manager.execute_hook(:post_add, context)
|
164
|
+
|
165
|
+
pwd_file = File.join(test_worktree, 'pwd.txt')
|
166
|
+
expect(File.exist?(pwd_file)).to be true
|
167
|
+
expect(File.realpath(File.read(pwd_file).strip)).to eq(File.realpath(test_worktree))
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
describe '#has_hook?' do
|
174
|
+
context 'when hook file exists' do
|
175
|
+
let(:hook_file) { File.join(temp_dir, '.worktree.yml') }
|
176
|
+
|
177
|
+
before do
|
178
|
+
File.write(hook_file, YAML.dump({
|
179
|
+
'pre_add' => "echo 'test'",
|
180
|
+
'post_add' => nil
|
181
|
+
}))
|
182
|
+
end
|
183
|
+
|
184
|
+
it 'returns true for existing hooks' do
|
185
|
+
expect(hook_manager.has_hook?(:pre_add)).to be true
|
186
|
+
end
|
187
|
+
|
188
|
+
it 'returns false for hooks with nil value' do
|
189
|
+
expect(hook_manager.has_hook?(:post_add)).to be false
|
190
|
+
end
|
191
|
+
|
192
|
+
it 'returns false for non-existent hooks' do
|
193
|
+
expect(hook_manager.has_hook?(:pre_remove)).to be false
|
194
|
+
end
|
195
|
+
|
196
|
+
it 'returns false for invalid hook types' do
|
197
|
+
expect(hook_manager.has_hook?(:invalid_hook)).to be false
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
describe '#list_hooks' do
|
203
|
+
context 'when hook file exists' do
|
204
|
+
let(:hook_file) { File.join(temp_dir, '.worktree.yml') }
|
205
|
+
|
206
|
+
before do
|
207
|
+
File.write(hook_file, YAML.dump({
|
208
|
+
'pre_add' => "echo 'pre-add'",
|
209
|
+
'post_add' => "echo 'post-add'",
|
210
|
+
'pre_remove' => nil,
|
211
|
+
'invalid_hook' => "echo 'invalid'"
|
212
|
+
}))
|
213
|
+
end
|
214
|
+
|
215
|
+
it 'returns only valid hooks with non-nil values' do
|
216
|
+
hooks = hook_manager.list_hooks
|
217
|
+
expect(hooks.keys).to contain_exactly('pre_add', 'post_add')
|
218
|
+
expect(hooks['pre_add']).to eq("echo 'pre-add'")
|
219
|
+
expect(hooks['post_add']).to eq("echo 'post-add'")
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
context 'when no hook file exists' do
|
224
|
+
it 'returns empty hash' do
|
225
|
+
expect(hook_manager.list_hooks).to eq({})
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
describe 'environment variable handling' do
|
231
|
+
let(:hook_file) { File.join(temp_dir, '.worktree_hooks.yml') }
|
232
|
+
|
233
|
+
before do
|
234
|
+
File.write(hook_file, YAML.dump({
|
235
|
+
'pre_add' => 'echo "PATH: $WORKTREE_PATH, BRANCH: $WORKTREE_BRANCH, ROOT: $WORKTREE_MANAGER_ROOT"'
|
236
|
+
}))
|
237
|
+
end
|
238
|
+
|
239
|
+
it 'passes context as environment variables' do
|
240
|
+
context = { path: '/test/path', branch: 'feature' }
|
241
|
+
|
242
|
+
# Actually needs stdout capture but only checking success here
|
243
|
+
expect(hook_manager.execute_hook(:pre_add, context)).to be true
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
describe 'hook file discovery' do
|
248
|
+
context 'with .worktree.yml in root' do
|
249
|
+
let(:hook_file) { File.join(temp_dir, '.worktree.yml') }
|
250
|
+
|
251
|
+
before do
|
252
|
+
File.write(hook_file, YAML.dump({ 'hooks' => { 'pre_add' => { 'commands' => ["echo 'root hook'"] } } }))
|
253
|
+
end
|
254
|
+
|
255
|
+
it 'finds and uses the hook file' do
|
256
|
+
expect(hook_manager.has_hook?(:pre_add)).to be true
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
context 'with .git/.worktree.yml' do
|
261
|
+
let(:git_dir) { File.join(temp_dir, '.git') }
|
262
|
+
let(:hook_file) { File.join(git_dir, '.worktree.yml') }
|
263
|
+
|
264
|
+
before do
|
265
|
+
Dir.mkdir(git_dir)
|
266
|
+
File.write(hook_file, YAML.dump({ 'hooks' => { 'pre_add' => { 'commands' => ["echo 'git hook'"] } } }))
|
267
|
+
end
|
268
|
+
|
269
|
+
it 'finds and uses the git hook file' do
|
270
|
+
expect(hook_manager.has_hook?(:pre_add)).to be true
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
context 'with both hook files present' do
|
275
|
+
let(:root_hook_file) { File.join(temp_dir, '.worktree.yml') }
|
276
|
+
let(:git_dir) { File.join(temp_dir, '.git') }
|
277
|
+
let(:git_hook_file) { File.join(git_dir, '.worktree.yml') }
|
278
|
+
|
279
|
+
before do
|
280
|
+
File.write(root_hook_file, YAML.dump({ 'hooks' => { 'pre_add' => { 'commands' => ["echo 'root hook'"] } } }))
|
281
|
+
Dir.mkdir(git_dir)
|
282
|
+
File.write(git_hook_file, YAML.dump({ 'hooks' => { 'pre_remove' => { 'commands' => ["echo 'git hook'"] } } }))
|
283
|
+
end
|
284
|
+
|
285
|
+
it 'prioritizes .worktree.yml over .git/.worktree.yml' do
|
286
|
+
expect(hook_manager.has_hook?(:pre_add)).to be true
|
287
|
+
expect(hook_manager.has_hook?(:pre_remove)).to be false # git file not loaded
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
describe 'stop_on_error configuration' do
|
293
|
+
let(:hook_file) { File.join(temp_dir, '.worktree.yml') }
|
294
|
+
|
295
|
+
context 'when stop_on_error is false' do
|
296
|
+
before do
|
297
|
+
File.write(hook_file, YAML.dump({
|
298
|
+
'hooks' => {
|
299
|
+
'pre_add' => {
|
300
|
+
'commands' => [
|
301
|
+
'exit 1', # This will fail
|
302
|
+
"echo 'This should still execute'"
|
303
|
+
],
|
304
|
+
'stop_on_error' => false
|
305
|
+
}
|
306
|
+
}
|
307
|
+
}))
|
308
|
+
end
|
309
|
+
|
310
|
+
it 'continues executing commands even after failure' do
|
311
|
+
expect(hook_manager.execute_hook(:pre_add)).to be true
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
context 'when stop_on_error is true (default)' do
|
316
|
+
before do
|
317
|
+
File.write(hook_file, YAML.dump({
|
318
|
+
'hooks' => {
|
319
|
+
'pre_add' => {
|
320
|
+
'commands' => [
|
321
|
+
'exit 1', # This will fail
|
322
|
+
"echo 'This should NOT execute'"
|
323
|
+
]
|
324
|
+
}
|
325
|
+
}
|
326
|
+
}))
|
327
|
+
end
|
328
|
+
|
329
|
+
it 'stops executing commands after failure' do
|
330
|
+
expect(hook_manager.execute_hook(:pre_add)).to be false
|
331
|
+
end
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
describe 'all environment variables' do
|
336
|
+
let(:hook_file) { File.join(temp_dir, '.worktree.yml') }
|
337
|
+
let(:output_file) { File.join(temp_dir, 'env_vars.txt') }
|
338
|
+
|
339
|
+
before do
|
340
|
+
File.write(hook_file, YAML.dump({
|
341
|
+
'hooks' => {
|
342
|
+
'post_add' => {
|
343
|
+
'commands' => [
|
344
|
+
"echo \"MAIN=$WORKTREE_MAIN\" > #{output_file}",
|
345
|
+
"echo \"ROOT=$WORKTREE_MANAGER_ROOT\" >> #{output_file}",
|
346
|
+
"echo \"PATH=$WORKTREE_PATH\" >> #{output_file}",
|
347
|
+
"echo \"ABSOLUTE=$WORKTREE_ABSOLUTE_PATH\" >> #{output_file}",
|
348
|
+
"echo \"BRANCH=$WORKTREE_BRANCH\" >> #{output_file}",
|
349
|
+
"echo \"FORCE=$WORKTREE_FORCE\" >> #{output_file}",
|
350
|
+
"echo \"SUCCESS=$WORKTREE_SUCCESS\" >> #{output_file}"
|
351
|
+
],
|
352
|
+
'pwd' => temp_dir
|
353
|
+
}
|
354
|
+
}
|
355
|
+
}))
|
356
|
+
end
|
357
|
+
|
358
|
+
it 'provides all documented environment variables' do
|
359
|
+
context = {
|
360
|
+
path: '../test-worktree',
|
361
|
+
branch: 'feature/test',
|
362
|
+
force: true,
|
363
|
+
success: true
|
364
|
+
}
|
365
|
+
|
366
|
+
hook_manager.execute_hook(:post_add, context)
|
367
|
+
|
368
|
+
content = File.read(output_file)
|
369
|
+
expect(content).to include("MAIN=#{temp_dir}")
|
370
|
+
expect(content).to include("ROOT=#{temp_dir}")
|
371
|
+
expect(content).to include('PATH=../test-worktree')
|
372
|
+
expect(content).to include("ABSOLUTE=#{File.expand_path('../test-worktree', temp_dir)}")
|
373
|
+
expect(content).to include('BRANCH=feature/test')
|
374
|
+
expect(content).to include('FORCE=true')
|
375
|
+
expect(content).to include('SUCCESS=true')
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
describe 'legacy configuration format' do
|
380
|
+
let(:hook_file) { File.join(temp_dir, '.worktree.yml') }
|
381
|
+
|
382
|
+
context 'with single string command at top level' do
|
383
|
+
before do
|
384
|
+
File.write(hook_file, YAML.dump({
|
385
|
+
'pre_add' => "echo 'Legacy single command'"
|
386
|
+
}))
|
387
|
+
end
|
388
|
+
|
389
|
+
it 'executes legacy single command format' do
|
390
|
+
expect(hook_manager.execute_hook(:pre_add)).to be true
|
391
|
+
end
|
392
|
+
end
|
393
|
+
|
394
|
+
context 'with array of commands at top level' do
|
395
|
+
before do
|
396
|
+
File.write(hook_file, YAML.dump({
|
397
|
+
'post_add' => [
|
398
|
+
"echo 'Legacy command 1'",
|
399
|
+
"echo 'Legacy command 2'"
|
400
|
+
]
|
401
|
+
}))
|
402
|
+
end
|
403
|
+
|
404
|
+
it 'executes legacy array format' do
|
405
|
+
expect(hook_manager.execute_hook(:post_add)).to be true
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
context 'with hash configuration at top level' do
|
410
|
+
before do
|
411
|
+
File.write(hook_file, YAML.dump({
|
412
|
+
'pre_remove' => {
|
413
|
+
'command' => "echo 'Legacy hash command'",
|
414
|
+
'stop_on_error' => false
|
415
|
+
}
|
416
|
+
}))
|
417
|
+
end
|
418
|
+
|
419
|
+
it 'executes legacy hash format' do
|
420
|
+
expect(hook_manager.execute_hook(:pre_remove)).to be true
|
421
|
+
end
|
422
|
+
end
|
423
|
+
end
|
424
|
+
|
425
|
+
describe 'practical examples from documentation' do
|
426
|
+
let(:hook_file) { File.join(temp_dir, '.worktree.yml') }
|
427
|
+
let(:worktree_path) { File.join(temp_dir, 'test-worktree') }
|
428
|
+
|
429
|
+
before do
|
430
|
+
Dir.mkdir(worktree_path)
|
431
|
+
end
|
432
|
+
|
433
|
+
context 'with development environment setup' do
|
434
|
+
let(:gemfile) { File.join(worktree_path, 'Gemfile') }
|
435
|
+
let(:env_example) { File.join(worktree_path, '.env.example') }
|
436
|
+
let(:env_file) { File.join(worktree_path, '.env') }
|
437
|
+
|
438
|
+
before do
|
439
|
+
File.write(gemfile, "source 'https://rubygems.org'\ngem 'rake'")
|
440
|
+
File.write(env_example, 'DATABASE_URL=postgres://localhost/dev')
|
441
|
+
|
442
|
+
File.write(hook_file, YAML.dump({
|
443
|
+
'hooks' => {
|
444
|
+
'post_add' => {
|
445
|
+
'commands' => [
|
446
|
+
'touch Gemfile.lock', # Simulate bundle install
|
447
|
+
'cp .env.example .env || true'
|
448
|
+
]
|
449
|
+
}
|
450
|
+
}
|
451
|
+
}))
|
452
|
+
end
|
453
|
+
|
454
|
+
it 'sets up development environment after worktree creation' do
|
455
|
+
context = { path: worktree_path }
|
456
|
+
expect(hook_manager.execute_hook(:post_add, context)).to be true
|
457
|
+
|
458
|
+
expect(File.exist?(File.join(worktree_path, 'Gemfile.lock'))).to be true
|
459
|
+
expect(File.exist?(env_file)).to be true
|
460
|
+
expect(File.read(env_file)).to eq(File.read(env_example))
|
461
|
+
end
|
462
|
+
end
|
463
|
+
|
464
|
+
context 'with automatic backup before removal' do
|
465
|
+
before do
|
466
|
+
File.write(hook_file, YAML.dump({
|
467
|
+
'hooks' => {
|
468
|
+
'pre_remove' => {
|
469
|
+
'commands' => [
|
470
|
+
"echo 'Simulating git add -A'",
|
471
|
+
"echo 'Simulating git stash push'"
|
472
|
+
]
|
473
|
+
}
|
474
|
+
}
|
475
|
+
}))
|
476
|
+
end
|
477
|
+
|
478
|
+
it 'backs up changes before removal' do
|
479
|
+
context = { path: worktree_path, branch: 'feature/test' }
|
480
|
+
expect(hook_manager.execute_hook(:pre_remove, context)).to be true
|
481
|
+
end
|
482
|
+
end
|
483
|
+
|
484
|
+
context 'with custom pwd absolute path' do
|
485
|
+
let(:custom_dir) { File.join(temp_dir, 'custom_work_dir') }
|
486
|
+
|
487
|
+
before do
|
488
|
+
Dir.mkdir(custom_dir)
|
489
|
+
File.write(hook_file, YAML.dump({
|
490
|
+
'hooks' => {
|
491
|
+
'post_add' => {
|
492
|
+
'commands' => ['pwd > current_dir.txt'],
|
493
|
+
'pwd' => custom_dir
|
494
|
+
}
|
495
|
+
}
|
496
|
+
}))
|
497
|
+
end
|
498
|
+
|
499
|
+
it 'executes commands in custom absolute directory' do
|
500
|
+
context = { path: worktree_path }
|
501
|
+
hook_manager.execute_hook(:post_add, context)
|
502
|
+
|
503
|
+
pwd_file = File.join(custom_dir, 'current_dir.txt')
|
504
|
+
expect(File.exist?(pwd_file)).to be true
|
505
|
+
expect(File.realpath(File.read(pwd_file).strip)).to eq(File.realpath(custom_dir))
|
506
|
+
end
|
507
|
+
end
|
508
|
+
end
|
509
|
+
|
510
|
+
describe 'default working directories' do
|
511
|
+
let(:hook_file) { File.join(temp_dir, '.worktree.yml') }
|
512
|
+
let(:worktree_path) { File.join(temp_dir, 'test-worktree') }
|
513
|
+
|
514
|
+
before do
|
515
|
+
Dir.mkdir(worktree_path)
|
516
|
+
end
|
517
|
+
|
518
|
+
context 'post_add hook' do
|
519
|
+
before do
|
520
|
+
File.write(hook_file, YAML.dump({
|
521
|
+
'hooks' => {
|
522
|
+
'post_add' => {
|
523
|
+
'commands' => ['pwd > current_dir.txt']
|
524
|
+
}
|
525
|
+
}
|
526
|
+
}))
|
527
|
+
end
|
528
|
+
|
529
|
+
it 'executes in worktree directory by default' do
|
530
|
+
context = { path: worktree_path }
|
531
|
+
hook_manager.execute_hook(:post_add, context)
|
532
|
+
|
533
|
+
pwd_file = File.join(worktree_path, 'current_dir.txt')
|
534
|
+
expect(File.exist?(pwd_file)).to be true
|
535
|
+
expect(File.realpath(File.read(pwd_file).strip)).to eq(File.realpath(worktree_path))
|
536
|
+
end
|
537
|
+
end
|
538
|
+
|
539
|
+
context 'pre_remove hook' do
|
540
|
+
before do
|
541
|
+
File.write(hook_file, YAML.dump({
|
542
|
+
'hooks' => {
|
543
|
+
'pre_remove' => {
|
544
|
+
'commands' => ['pwd > current_dir.txt']
|
545
|
+
}
|
546
|
+
}
|
547
|
+
}))
|
548
|
+
end
|
549
|
+
|
550
|
+
it 'executes in worktree directory by default' do
|
551
|
+
context = { path: worktree_path }
|
552
|
+
hook_manager.execute_hook(:pre_remove, context)
|
553
|
+
|
554
|
+
pwd_file = File.join(worktree_path, 'current_dir.txt')
|
555
|
+
expect(File.exist?(pwd_file)).to be true
|
556
|
+
expect(File.realpath(File.read(pwd_file).strip)).to eq(File.realpath(worktree_path))
|
557
|
+
end
|
558
|
+
end
|
559
|
+
|
560
|
+
context 'pre_add hook' do
|
561
|
+
before do
|
562
|
+
File.write(hook_file, YAML.dump({
|
563
|
+
'hooks' => {
|
564
|
+
'pre_add' => {
|
565
|
+
'commands' => ['pwd > current_dir.txt']
|
566
|
+
}
|
567
|
+
}
|
568
|
+
}))
|
569
|
+
end
|
570
|
+
|
571
|
+
it 'executes in main repository by default' do
|
572
|
+
context = { path: worktree_path }
|
573
|
+
hook_manager.execute_hook(:pre_add, context)
|
574
|
+
|
575
|
+
pwd_file = File.join(temp_dir, 'current_dir.txt')
|
576
|
+
expect(File.exist?(pwd_file)).to be true
|
577
|
+
expect(File.realpath(File.read(pwd_file).strip)).to eq(File.realpath(temp_dir))
|
578
|
+
end
|
579
|
+
end
|
580
|
+
end
|
581
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe WorktreeManager::Manager do
|
4
|
+
let(:manager) { described_class.new }
|
5
|
+
|
6
|
+
describe '#initialize' do
|
7
|
+
it 'accepts a repository path' do
|
8
|
+
expect { described_class.new('.') }.not_to raise_error
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
describe '#list' do
|
13
|
+
it 'returns an array of worktrees' do
|
14
|
+
result = manager.list
|
15
|
+
expect(result).to be_an(Array)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '#add' do
|
20
|
+
it 'adds a new worktree' do
|
21
|
+
expect { manager.add('test_path', 'test_branch') }.to raise_error(WorktreeManager::Error)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe '#remove' do
|
26
|
+
it 'removes a worktree' do
|
27
|
+
expect { manager.remove('test_path') }.to raise_error(WorktreeManager::Error)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe '#prune' do
|
32
|
+
it 'prunes worktrees' do
|
33
|
+
expect { manager.prune }.not_to raise_error
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe '#add_tracking_branch' do
|
38
|
+
let(:path) { '../test-worktree' }
|
39
|
+
let(:local_branch) { 'pr-123' }
|
40
|
+
let(:remote_branch) { 'origin/pr-123' }
|
41
|
+
|
42
|
+
context 'when remote branch exists' do
|
43
|
+
before do
|
44
|
+
allow(manager).to receive(:execute_git_command)
|
45
|
+
.with('fetch origin pr-123')
|
46
|
+
.and_return(['', double(success?: true)])
|
47
|
+
|
48
|
+
allow(manager).to receive(:execute_git_command)
|
49
|
+
.with('worktree add -b pr-123 ../test-worktree origin/pr-123')
|
50
|
+
.and_return(['Preparing worktree', double(success?: true)])
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'fetches the remote branch and creates worktree' do
|
54
|
+
expect(manager).to receive(:execute_git_command).with('fetch origin pr-123')
|
55
|
+
expect(manager).to receive(:execute_git_command).with('worktree add -b pr-123 ../test-worktree origin/pr-123')
|
56
|
+
|
57
|
+
result = manager.add_tracking_branch(path, local_branch, remote_branch)
|
58
|
+
expect(result.path).to eq(path)
|
59
|
+
expect(result.branch).to eq(local_branch)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
context 'when fetch fails' do
|
64
|
+
before do
|
65
|
+
allow(manager).to receive(:execute_git_command)
|
66
|
+
.with('fetch origin pr-123')
|
67
|
+
.and_return(["fatal: couldn't find remote ref pr-123", double(success?: false)])
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'raises an error' do
|
71
|
+
expect do
|
72
|
+
manager.add_tracking_branch(path, local_branch, remote_branch)
|
73
|
+
end.to raise_error(WorktreeManager::Error, /Failed to fetch remote branch/)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
context 'with force option' do
|
78
|
+
before do
|
79
|
+
allow(manager).to receive(:execute_git_command)
|
80
|
+
.with('fetch origin pr-123')
|
81
|
+
.and_return(['', double(success?: true)])
|
82
|
+
|
83
|
+
allow(manager).to receive(:execute_git_command)
|
84
|
+
.with('worktree add --force -b pr-123 ../test-worktree origin/pr-123')
|
85
|
+
.and_return(['Preparing worktree', double(success?: true)])
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'includes --force flag' do
|
89
|
+
expect(manager).to receive(:execute_git_command).with('worktree add --force -b pr-123 ../test-worktree origin/pr-123')
|
90
|
+
|
91
|
+
manager.add_tracking_branch(path, local_branch, remote_branch, force: true)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|