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,288 @@
1
+ require 'spec_helper'
2
+ require 'tmpdir'
3
+ require 'fileutils'
4
+ require 'pathname'
5
+
6
+ RSpec.describe 'Worktree Integration Tests' do
7
+ let(:original_dir) { Dir.pwd }
8
+ let(:test_id) { Time.now.to_i }
9
+ let(:test_dir) { Dir.mktmpdir("worktree_integration_#{test_id}") }
10
+ let(:main_repo_path) { File.join(test_dir, 'main-repo') }
11
+ let(:worktree_path) { File.join(test_dir, 'test-worktree') }
12
+ let(:branch_name) { "test/branch-#{test_id}" }
13
+ let(:hook_file) { File.join(main_repo_path, '.worktree.yml') }
14
+
15
+ around do |example|
16
+ # Save original directory
17
+ Dir.chdir(original_dir) do
18
+ # Create test Git repository
19
+ Dir.chdir(test_dir) do
20
+ `git init main-repo`
21
+ Dir.chdir('main-repo') do
22
+ `git config user.name "Test User"`
23
+ `git config user.email "test@example.com"`
24
+ File.write('README.md', '# Test Repository')
25
+ `git add README.md`
26
+ `git commit -m "Initial commit"`
27
+ end
28
+ end
29
+
30
+ # Create hook file
31
+ hook_config = <<~YAML
32
+ hooks:
33
+ pre_add:
34
+ commands:
35
+ - "echo \\"Test Hook: Starting worktree creation $WORKTREE_PATH\\""
36
+ post_add:
37
+ commands:
38
+ - "echo \\"Test Hook: Worktree creation completed $WORKTREE_PATH\\""
39
+ - "echo \\"Branch: $WORKTREE_BRANCH\\""
40
+ pre_remove:
41
+ commands:
42
+ - "echo \\"Test Hook: Starting worktree removal $WORKTREE_PATH\\""
43
+ post_remove:
44
+ commands:
45
+ - "echo \\"Test Hook: Worktree removal completed $WORKTREE_PATH\\""
46
+ YAML
47
+ File.write(hook_file, hook_config)
48
+
49
+ # Run example in main repository
50
+ Dir.chdir(main_repo_path) do
51
+ example.run
52
+ end
53
+ end
54
+ ensure
55
+ # Clean up test directory
56
+ FileUtils.rm_rf(test_dir) if Dir.exist?(test_dir)
57
+ end
58
+
59
+ describe 'Complete workflow tests' do
60
+ it 'works correctly from worktree creation to removal' do
61
+ manager = WorktreeManager::Manager.new(main_repo_path)
62
+ hook_manager = WorktreeManager::HookManager.new(main_repo_path)
63
+
64
+ # 1. Check initial state (main repository is also included as a worktree)
65
+ initial_worktrees = manager.list
66
+ initial_count = initial_worktrees.size
67
+
68
+ # 2. Create worktree with new branch
69
+ expect(hook_manager.execute_hook(:pre_add, path: worktree_path, branch: branch_name)).to be true
70
+
71
+ worktree = manager.add_with_new_branch(worktree_path, branch_name)
72
+ expect(worktree).not_to be_nil
73
+ expect(worktree.path).to eq(worktree_path)
74
+ expect(worktree.branch).to eq(branch_name)
75
+
76
+ expect(hook_manager.execute_hook(:post_add, path: worktree_path, branch: branch_name)).to be true
77
+
78
+ # 3. Verify created worktree
79
+ worktrees = manager.list
80
+ expect(worktrees.size).to eq(initial_count + 1)
81
+
82
+ new_worktree = worktrees.find do |w|
83
+ File.realpath(w.path) == File.realpath(worktree_path)
84
+ end
85
+ expect(new_worktree).not_to be_nil
86
+ expect(new_worktree.branch).to eq("refs/heads/#{branch_name}")
87
+
88
+ # 4. Verify worktree directory in file system
89
+ expect(Dir.exist?(worktree_path)).to be true
90
+ expect(File.exist?(File.join(worktree_path, '.git'))).to be true
91
+
92
+ # 5. Remove worktree with hooks
93
+ expect(hook_manager.execute_hook(:pre_remove, path: worktree_path, branch: branch_name)).to be true
94
+
95
+ expect(manager.remove(worktree_path)).to be true
96
+
97
+ expect(hook_manager.execute_hook(:post_remove, path: worktree_path, branch: branch_name)).to be true
98
+
99
+ # 6. Verify removal
100
+ expect(manager.list.size).to eq(initial_count)
101
+ expect(Dir.exist?(worktree_path)).to be false
102
+ end
103
+
104
+ it 'raises error when creating multiple times at the same path' do
105
+ manager = WorktreeManager::Manager.new(main_repo_path)
106
+
107
+ # First creation
108
+ worktree1 = manager.add_with_new_branch(worktree_path, branch_name)
109
+ expect(worktree1).not_to be_nil
110
+
111
+ # Try to create again at the same path
112
+ expect do
113
+ manager.add_with_new_branch(worktree_path, "#{branch_name}-2")
114
+ end.to raise_error(WorktreeManager::Error, /already exists/)
115
+
116
+ # Cleanup
117
+ manager.remove(worktree_path)
118
+ end
119
+
120
+ it 'raises error when removing non-existent worktree' do
121
+ manager = WorktreeManager::Manager.new(main_repo_path)
122
+
123
+ expect do
124
+ manager.remove('/non/existent/path')
125
+ end.to raise_error(WorktreeManager::Error, /is not a working tree/)
126
+ end
127
+ end
128
+
129
+ describe 'Hook system integration tests' do
130
+ it 'handles errors during hook execution properly' do
131
+ # Set up hook that causes error
132
+ error_hook_file = File.join(main_repo_path, '.worktree.yml')
133
+ error_hook_config = <<~YAML
134
+ hooks:
135
+ pre_add:
136
+ commands:
137
+ - "exit 1"
138
+ stop_on_error: true
139
+ YAML
140
+ File.write(error_hook_file, error_hook_config)
141
+
142
+ hook_manager = WorktreeManager::HookManager.new(main_repo_path)
143
+
144
+ # Hook execution returns false
145
+ expect(hook_manager.execute_hook(:pre_add, path: worktree_path, branch: branch_name)).to be false
146
+ end
147
+
148
+ it 'passes environment variables correctly to hooks' do
149
+ # Set up hook that outputs environment variables
150
+ env_hook_file = File.join(main_repo_path, '.worktree.yml')
151
+ env_hook_config = <<~YAML
152
+ hooks:
153
+ pre_add:
154
+ commands:
155
+ - "echo \\"PATH=$WORKTREE_PATH BRANCH=$WORKTREE_BRANCH ROOT=$WORKTREE_MAIN\\""
156
+ YAML
157
+ File.write(env_hook_file, env_hook_config)
158
+
159
+ hook_manager = WorktreeManager::HookManager.new(main_repo_path)
160
+
161
+ # Verify environment variables during hook execution
162
+ expect do
163
+ hook_manager.execute_hook(:pre_add, path: worktree_path, branch: branch_name)
164
+ end.to output(%r{PATH=.*test-worktree.*BRANCH=.*test/branch.*ROOT=.*main-repo}).to_stdout
165
+ end
166
+ end
167
+
168
+ describe 'CLI integration tests' do
169
+ it 'complete workflow works correctly through CLI' do
170
+ cli = WorktreeManager::CLI.new
171
+
172
+ # Set up CLI environment
173
+ allow(cli).to receive(:main_repository?).and_return(true)
174
+ # Stub exit to prevent actual process termination
175
+ allow(cli).to receive(:exit)
176
+
177
+ # Execute add command
178
+ expect do
179
+ cli.invoke(:add, [worktree_path], { branch: branch_name })
180
+ end.to output(/Worktree created:.*test-worktree/).to_stdout
181
+
182
+ # Execute list command
183
+ expect do
184
+ cli.list
185
+ end.to output(/test-worktree/).to_stdout
186
+
187
+ # Execute remove command
188
+ # Git stores relative paths, so remove using relative path
189
+ relative_path = Pathname.new(worktree_path).relative_path_from(Pathname.new(main_repo_path)).to_s
190
+ expect do
191
+ cli.remove(relative_path)
192
+ end.to output(/Worktree removed:.*test-worktree/).to_stdout
193
+ end
194
+ end
195
+
196
+ describe 'worktrees_dir feature integration tests' do
197
+ let(:worktrees_dir) { '../worktrees' }
198
+ let(:worktree_name) { "feature-branch-#{test_id}" }
199
+ let(:expected_path) { File.expand_path(File.join(worktrees_dir, worktree_name), main_repo_path) }
200
+
201
+ before do
202
+ # Add worktrees_dir configuration
203
+ config_with_dir = <<~YAML
204
+ worktrees_dir: "#{worktrees_dir}"
205
+ hooks:
206
+ post_add:
207
+ commands:
208
+ - "echo 'Worktree created at $WORKTREE_ABSOLUTE_PATH'"
209
+ YAML
210
+ File.write(hook_file, config_with_dir)
211
+ end
212
+
213
+ it 'creates worktree according to worktrees_dir setting' do
214
+ cli = WorktreeManager::CLI.new
215
+ allow(cli).to receive(:main_repository?).and_return(true)
216
+ allow(cli).to receive(:exit)
217
+
218
+ # Add worktree with name only
219
+ expect do
220
+ cli.invoke(:add, [worktree_name], { branch: worktree_name })
221
+ end.to output(/Worktree created:.*#{worktree_name}/).to_stdout
222
+
223
+ # Verify created at correct location
224
+ expect(Dir.exist?(expected_path)).to be true
225
+
226
+ # Cleanup
227
+ manager = WorktreeManager::Manager.new(main_repo_path)
228
+ manager.remove(expected_path)
229
+ end
230
+
231
+ it 'removes worktree using worktrees_dir setting' do
232
+ # Create worktree first
233
+ manager = WorktreeManager::Manager.new(main_repo_path)
234
+ FileUtils.mkdir_p(File.dirname(expected_path))
235
+ manager.add_with_new_branch(expected_path, worktree_name)
236
+
237
+ cli = WorktreeManager::CLI.new
238
+ allow(cli).to receive(:main_repository?).and_return(true)
239
+ allow(cli).to receive(:exit)
240
+
241
+ # Remove worktree with name only
242
+ expect do
243
+ cli.remove(worktree_name)
244
+ end.to output(/Worktree removed:.*#{worktree_name}/).to_stdout
245
+
246
+ expect(Dir.exist?(expected_path)).to be false
247
+ end
248
+
249
+ it 'processes relative path based on repository' do
250
+ cli = WorktreeManager::CLI.new
251
+ allow(cli).to receive(:main_repository?).and_return(true)
252
+ allow(cli).to receive(:exit)
253
+
254
+ relative_path = '../custom/worktree'
255
+ expected_custom_path = File.expand_path(relative_path, main_repo_path)
256
+
257
+ # Add worktree with relative path
258
+ expect do
259
+ cli.invoke(:add, [relative_path], { branch: 'custom-branch' })
260
+ end.to output(%r{Worktree created:.*custom/worktree}).to_stdout
261
+
262
+ expect(Dir.exist?(expected_custom_path)).to be true
263
+
264
+ # Cleanup
265
+ manager = WorktreeManager::Manager.new(main_repo_path)
266
+ manager.remove(expected_custom_path)
267
+ end
268
+
269
+ it 'uses absolute path as is when provided' do
270
+ cli = WorktreeManager::CLI.new
271
+ allow(cli).to receive(:main_repository?).and_return(true)
272
+ allow(cli).to receive(:exit)
273
+
274
+ absolute_path = File.join(test_dir, 'absolute-worktree')
275
+
276
+ # Add worktree with absolute path
277
+ expect do
278
+ cli.invoke(:add, [absolute_path], { branch: 'absolute-branch' })
279
+ end.to output(/Worktree created:.*absolute-worktree/).to_stdout
280
+
281
+ expect(Dir.exist?(absolute_path)).to be true
282
+
283
+ # Cleanup
284
+ manager = WorktreeManager::Manager.new(main_repo_path)
285
+ manager.remove(absolute_path)
286
+ end
287
+ end
288
+ end
@@ -0,0 +1,34 @@
1
+ require 'bundler/setup'
2
+ require 'worktree_manager'
3
+ require 'worktree_manager/cli'
4
+ require 'worktree_manager/hook_manager'
5
+ require 'worktree_manager/manager'
6
+ require 'worktree_manager/worktree'
7
+ require 'worktree_manager/config_manager'
8
+ require 'tty-prompt'
9
+
10
+ RSpec.configure do |config|
11
+ config.expect_with :rspec do |c|
12
+ c.syntax = :expect
13
+ end
14
+ end
15
+
16
+ # SystemExit handling in tests
17
+ #
18
+ # This project's tests often involve CLI commands that call exit.
19
+ # There are two approaches to handle this:
20
+ #
21
+ # 1. Integration tests (spec/integration/)
22
+ # - Stub the exit method on CLI instance: `allow(cli).to receive(:exit)`
23
+ # - This prevents exit from doing anything, allowing continued execution
24
+ # - Useful for testing complete workflows
25
+ #
26
+ # 2. Unit tests (spec/worktree_manager/)
27
+ # - Individually stub in tests that expect SystemExit
28
+ # - Example: stub_exit helper method in cli_spec.rb
29
+ # - `allow(cli).to receive(:exit) { |code| raise SystemExit.new(code) }`
30
+ # - This enables `expect { }.to raise_error(SystemExit)` tests
31
+ #
32
+ # Important notes:
33
+ # - Don't stub exit globally (tests may terminate early)
34
+ # - Choose the appropriate method based on test purpose
@@ -0,0 +1,154 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe WorktreeManager::CLI do
4
+ describe 'help functionality' do
5
+ let(:help_output) { capture_stdout { WorktreeManager::CLI.start(args) } }
6
+
7
+ context "when using 'wm help add'" do
8
+ let(:args) { %w[help add] }
9
+
10
+ it 'displays help for the add command' do
11
+ expect(help_output).to include('Usage:')
12
+ expect(help_output).to include('add NAME_OR_PATH [BRANCH]')
13
+ expect(help_output).to include('Description:')
14
+ expect(help_output).to include('creates a new git worktree')
15
+ expect(help_output).to include('--branch')
16
+ expect(help_output).to include('--track')
17
+ expect(help_output).to include('--force')
18
+ end
19
+ end
20
+
21
+ context "when using 'wm add --help'" do
22
+ let(:args) { ['add', '--help'] }
23
+
24
+ it 'displays help for the add command instead of treating --help as an argument' do
25
+ expect(help_output).to include('Usage:')
26
+ expect(help_output).to include('add NAME_OR_PATH [BRANCH]')
27
+ expect(help_output).to include('Description:')
28
+ expect(help_output).to include('creates a new git worktree')
29
+ expect(help_output).not_to include('Error')
30
+ expect(help_output).not_to include('invalid reference: --help')
31
+ end
32
+ end
33
+
34
+ context "when using 'wm add -h'" do
35
+ let(:args) { ['add', '-h'] }
36
+
37
+ it 'displays help for the add command' do
38
+ expect(help_output).to include('Usage:')
39
+ expect(help_output).to include('add NAME_OR_PATH [BRANCH]')
40
+ end
41
+ end
42
+
43
+ context "when using 'wm add -?'" do
44
+ let(:args) { ['add', '-?'] }
45
+
46
+ it 'displays help for the add command' do
47
+ expect(help_output).to include('Usage:')
48
+ expect(help_output).to include('add NAME_OR_PATH [BRANCH]')
49
+ end
50
+ end
51
+
52
+ context "when using 'wm add --usage'" do
53
+ let(:args) { ['add', '--usage'] }
54
+
55
+ it 'displays help for the add command' do
56
+ expect(help_output).to include('Usage:')
57
+ expect(help_output).to include('add NAME_OR_PATH [BRANCH]')
58
+ end
59
+ end
60
+
61
+ context "when using 'wm remove --help'" do
62
+ let(:args) { ['remove', '--help'] }
63
+
64
+ it 'displays help for the remove command' do
65
+ expect(help_output).to include('Usage:')
66
+ expect(help_output).to include('remove [NAME_OR_PATH]')
67
+ expect(help_output).to include('Remove an existing worktree')
68
+ end
69
+ end
70
+
71
+ context "when using 'wm list --help'" do
72
+ let(:args) { ['list', '--help'] }
73
+
74
+ it 'displays help for the list command' do
75
+ expect(help_output).to include('Usage:')
76
+ expect(help_output).to include('list')
77
+ expect(help_output).to include('List all worktrees')
78
+ end
79
+ end
80
+
81
+ context "when using just 'wm help'" do
82
+ let(:args) { ['help'] }
83
+
84
+ it 'displays general help with all commands' do
85
+ expect(help_output).to include('Commands:')
86
+ expect(help_output).to include('add NAME_OR_PATH [BRANCH]')
87
+ expect(help_output).to include('remove [NAME_OR_PATH]')
88
+ expect(help_output).to include('list')
89
+ expect(help_output).to include('version')
90
+ end
91
+ end
92
+
93
+ context "when using just 'wm --help'" do
94
+ let(:args) { ['--help'] }
95
+
96
+ it 'displays general help with all commands' do
97
+ expect(help_output).to include('Commands:')
98
+ expect(help_output).to include('add NAME_OR_PATH [BRANCH]')
99
+ expect(help_output).to include('remove [NAME_OR_PATH]')
100
+ end
101
+ end
102
+
103
+ # Edge case tests as recommended by o3
104
+ context 'edge cases' do
105
+ context 'when help flag is at the end with other options' do
106
+ let(:args) { ['add', 'foo', '--no-hooks', '-h'] }
107
+
108
+ it 'displays help for the add command' do
109
+ expect(help_output).to include('Usage:')
110
+ expect(help_output).to include('add NAME_OR_PATH [BRANCH]')
111
+ expect(help_output).not_to include('Error')
112
+ end
113
+ end
114
+
115
+ context 'when using help with unknown command' do
116
+ let(:args) { ['foo', '--help'] }
117
+
118
+ it 'shows error for unknown command' do
119
+ expect { help_output }.to output(/Could not find command "foo"/).to_stderr
120
+ end
121
+ end
122
+
123
+ context 'when using help command with unknown command' do
124
+ let(:args) { %w[help foo] }
125
+
126
+ it 'shows error for unknown command' do
127
+ expect { help_output }.to output(/Could not find command "foo"/).to_stderr
128
+ end
129
+ end
130
+
131
+ context 'when multiple help flags are provided' do
132
+ let(:args) { ['add', '--help', '-h', '-?'] }
133
+
134
+ it 'displays help for the add command only once' do
135
+ expect(help_output).to include('Usage:')
136
+ expect(help_output).to include('add NAME_OR_PATH [BRANCH]')
137
+ # Ensure help is not duplicated
138
+ expect(help_output.scan('Usage:').count).to eq(1)
139
+ end
140
+ end
141
+ end
142
+
143
+ private
144
+
145
+ def capture_stdout
146
+ original_stdout = $stdout
147
+ $stdout = StringIO.new
148
+ yield
149
+ $stdout.string
150
+ ensure
151
+ $stdout = original_stdout
152
+ end
153
+ end
154
+ end