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.
@@ -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