homesick 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,7 +1,8 @@
1
- class Homesick
1
+ require 'thor'
2
+
3
+ module Homesick
2
4
  # Hack in support for diffing symlinks
3
5
  class Shell < Thor::Shell::Color
4
-
5
6
  def show_diff(destination, content)
6
7
  destination = Pathname.new(destination)
7
8
 
@@ -12,6 +13,5 @@ class Homesick
12
13
  super
13
14
  end
14
15
  end
15
-
16
16
  end
17
17
  end
@@ -0,0 +1,216 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module Homesick
3
+ # Various utility methods that are used by Homesick
4
+ module Utils
5
+ QUIETABLE = ['say_status']
6
+
7
+ PRETENDABLE = ['system']
8
+
9
+ QUIETABLE.each do |method_name|
10
+ define_method(method_name) do |*args|
11
+ super(*args) unless options[:quiet]
12
+ end
13
+ end
14
+
15
+ PRETENDABLE.each do |method_name|
16
+ define_method(method_name) do |*args|
17
+ super(*args) unless options[:pretend]
18
+ end
19
+ end
20
+
21
+ protected
22
+
23
+ def home_dir
24
+ @home_dir ||= Pathname.new(ENV['HOME'] || '~').expand_path
25
+ end
26
+
27
+ def repos_dir
28
+ @repos_dir ||= home_dir.join('.homesick', 'repos').expand_path
29
+ end
30
+
31
+ def castle_dir(name)
32
+ repos_dir.join(name, 'home')
33
+ end
34
+
35
+ def check_castle_existance(name, action)
36
+ unless castle_dir(name).exist?
37
+ say_status :error,
38
+ "Could not #{action} #{name}, expected #{castle_dir(name)} exist and contain dotfiles",
39
+ :red
40
+ exit(1)
41
+ end
42
+ end
43
+
44
+ def all_castles
45
+ dirs = Pathname.glob("#{repos_dir}/**/.git", File::FNM_DOTMATCH)
46
+ # reject paths that lie inside another castle, like git submodules
47
+ dirs.reject do |dir|
48
+ dirs.any? do |other|
49
+ dir != other && dir.fnmatch(other.parent.join('*').to_s)
50
+ end
51
+ end
52
+ end
53
+
54
+ def inside_each_castle(&block)
55
+ all_castles.each do |git_dir|
56
+ castle = git_dir.dirname
57
+ Dir.chdir castle do # so we can call git config from the right contxt
58
+ yield castle
59
+ end
60
+ end
61
+ end
62
+
63
+ def update_castle(castle)
64
+ check_castle_existance(castle, 'pull')
65
+ inside repos_dir.join(castle) do
66
+ git_pull
67
+ git_submodule_init
68
+ git_submodule_update
69
+ end
70
+ end
71
+
72
+ def commit_castle(castle, message)
73
+ check_castle_existance(castle, 'commit')
74
+ inside repos_dir.join(castle) do
75
+ git_commit_all message: message
76
+ end
77
+ end
78
+
79
+ def push_castle(castle)
80
+ check_castle_existance(castle, 'push')
81
+ inside repos_dir.join(castle) do
82
+ git_push
83
+ end
84
+ end
85
+
86
+ def subdir_file(castle)
87
+ repos_dir.join(castle, SUBDIR_FILENAME)
88
+ end
89
+
90
+ def subdirs(castle)
91
+ subdir_filepath = subdir_file(castle)
92
+ subdirs = []
93
+ if subdir_filepath.exist?
94
+ subdir_filepath.readlines.each do |subdir|
95
+ subdirs.push(subdir.chomp)
96
+ end
97
+ end
98
+ subdirs
99
+ end
100
+
101
+ def subdir_add(castle, path)
102
+ subdir_filepath = subdir_file(castle)
103
+ File.open(subdir_filepath, 'a+') do |subdir|
104
+ subdir.puts path unless subdir.readlines.reduce(false) do |memo, line|
105
+ line.eql?("#{path}\n") || memo
106
+ end
107
+ end
108
+
109
+ inside castle_dir(castle) do
110
+ git_add subdir_filepath
111
+ end
112
+ end
113
+
114
+ def subdir_remove(castle, path)
115
+ subdir_filepath = subdir_file(castle)
116
+ if subdir_filepath.exist?
117
+ lines = IO.readlines(subdir_filepath).delete_if do |line|
118
+ line == "#{path}\n"
119
+ end
120
+ File.open(subdir_filepath, 'w') { |manfile| manfile.puts lines }
121
+ end
122
+
123
+ inside castle_dir(castle) do
124
+ git_add subdir_filepath
125
+ end
126
+ end
127
+
128
+ def move_dir_contents(target, dir_path)
129
+ child_files = dir_path.children
130
+ child_files.each do |child|
131
+
132
+ target_path = target.join(child.basename)
133
+ if target_path.exist?
134
+ if more_recent?(child, target_path) && target.file?
135
+ target_path.delete
136
+ mv child, target
137
+ end
138
+ next
139
+ end
140
+
141
+ mv child, target
142
+ end
143
+ end
144
+
145
+ def more_recent?(first, second)
146
+ first_p = Pathname.new(first)
147
+ second_p = Pathname.new(second)
148
+ first_p.mtime > second_p.mtime && !first_p.symlink?
149
+ end
150
+
151
+ def collision_accepted?(destination)
152
+ fail "Argument must be an instance of Pathname, #{destination.class.name} given" unless destination.instance_of?(Pathname)
153
+ options[:force] || shell.file_collision(destination) { source }
154
+ end
155
+
156
+ def each_file(castle, basedir, subdirs)
157
+ absolute_basedir = Pathname.new(basedir).expand_path
158
+ inside basedir do
159
+ files = Pathname.glob('{.*,*}').reject do |a|
160
+ ['.', '..'].include?(a.to_s)
161
+ end
162
+ files.each do |path|
163
+ absolute_path = path.expand_path
164
+ castle_home = castle_dir(castle)
165
+
166
+ # make ignore dirs
167
+ ignore_dirs = []
168
+ subdirs.each do |subdir|
169
+ # ignore all parent of each line in subdir file
170
+ Pathname.new(subdir).ascend do |p|
171
+ ignore_dirs.push(p)
172
+ end
173
+ end
174
+
175
+ # ignore dirs written in subdir file
176
+ matched = false
177
+ ignore_dirs.uniq.each do |ignore_dir|
178
+ if absolute_path == castle_home.join(ignore_dir)
179
+ matched = true
180
+ break
181
+ end
182
+ end
183
+ next if matched
184
+
185
+ relative_dir = absolute_basedir.relative_path_from(castle_home)
186
+ home_path = home_dir.join(relative_dir).join(path)
187
+
188
+ yield(absolute_path, home_path)
189
+ end
190
+ end
191
+ end
192
+
193
+ def unsymlink_each(castle, basedir, subdirs)
194
+ each_file(castle, basedir, subdirs) do |absolute_path, home_path|
195
+ rm_link home_path
196
+ end
197
+ end
198
+
199
+ def symlink_each(castle, basedir, subdirs)
200
+ each_file(castle, basedir, subdirs) do |absolute_path, home_path|
201
+ ln_s absolute_path, home_path
202
+ end
203
+ end
204
+
205
+ def setup_castle(path)
206
+ if path.join('.gitmodules').exist?
207
+ inside path do
208
+ git_submodule_init
209
+ git_submodule_update
210
+ end
211
+ end
212
+
213
+ rc(path)
214
+ end
215
+ end
216
+ end
@@ -1,10 +1,12 @@
1
1
  # -*- encoding : utf-8 -*-
2
- class Homesick
2
+ module Homesick
3
+ # A representation of Homesick's version number in constants, including a
4
+ # String of the entire version number
3
5
  module Version
4
6
  MAJOR = 1
5
- MINOR = 0
7
+ MINOR = 1
6
8
  PATCH = 0
7
9
 
8
- STRING = "#{MAJOR}.#{MINOR}.#{PATCH}"
10
+ STRING = [MAJOR, MINOR, PATCH].compact.join('.')
9
11
  end
10
12
  end
@@ -0,0 +1,787 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'spec_helper'
3
+ require 'capture-output'
4
+
5
+ describe Homesick::CLI do
6
+ let(:home) { create_construct }
7
+ after { home.destroy! }
8
+
9
+ let(:castles) { home.directory('.homesick/repos') }
10
+
11
+ let(:homesick) { Homesick::CLI.new }
12
+
13
+ before { allow(homesick).to receive(:repos_dir).and_return(castles) }
14
+
15
+ describe 'clone' do
16
+ context 'has a .homesickrc' do
17
+ it 'runs the .homesickrc' do
18
+ somewhere = create_construct
19
+ local_repo = somewhere.directory('some_repo')
20
+ local_repo.file('.homesickrc') do |file|
21
+ file << "File.open(Dir.pwd + '/testing', 'w') do |f|
22
+ f.print 'testing'
23
+ end"
24
+ end
25
+
26
+ expect_any_instance_of(Thor::Shell::Basic).to receive(:yes?).with(be_a(String)).and_return(true)
27
+ expect(homesick).to receive(:say_status).with('eval', kind_of(Pathname))
28
+ homesick.clone local_repo
29
+
30
+ expect(castles.join('some_repo').join('testing')).to exist
31
+ end
32
+ end
33
+
34
+ context 'of a file' do
35
+ it 'symlinks existing directories' do
36
+ somewhere = create_construct
37
+ local_repo = somewhere.directory('wtf')
38
+
39
+ homesick.clone local_repo
40
+
41
+ expect(castles.join('wtf').readlink).to eq(local_repo)
42
+ end
43
+
44
+ context 'when it exists in a repo directory' do
45
+ before do
46
+ existing_castle = given_castle('existing_castle')
47
+ @existing_dir = existing_castle.parent
48
+ end
49
+
50
+ it 'raises an error' do
51
+ expect(homesick).not_to receive(:git_clone)
52
+ expect { homesick.clone @existing_dir.to_s }.to raise_error(/already cloned/i)
53
+ end
54
+ end
55
+ end
56
+
57
+ it 'clones git repo like file:///path/to.git' do
58
+ bare_repo = File.join(create_construct.to_s, 'dotfiles.git')
59
+ system "git init --bare #{bare_repo} >/dev/null 2>&1"
60
+
61
+ # Capture stderr to suppress message about cloning an empty repo.
62
+ Capture.stderr do
63
+ homesick.clone "file://#{bare_repo}"
64
+ end
65
+ expect(File.directory?(File.join(home.to_s, '.homesick/repos/dotfiles')))
66
+ .to be_true
67
+ end
68
+
69
+ it 'clones git repo like git://host/path/to.git' do
70
+ expect(homesick).to receive(:git_clone)
71
+ .with('git://github.com/technicalpickles/pickled-vim.git')
72
+
73
+ homesick.clone 'git://github.com/technicalpickles/pickled-vim.git'
74
+ end
75
+
76
+ it 'clones git repo like git@host:path/to.git' do
77
+ expect(homesick).to receive(:git_clone)
78
+ .with('git@github.com:technicalpickles/pickled-vim.git')
79
+
80
+ homesick.clone 'git@github.com:technicalpickles/pickled-vim.git'
81
+ end
82
+
83
+ it 'clones git repo like http://host/path/to.git' do
84
+ expect(homesick).to receive(:git_clone)
85
+ .with('http://github.com/technicalpickles/pickled-vim.git')
86
+
87
+ homesick.clone 'http://github.com/technicalpickles/pickled-vim.git'
88
+ end
89
+
90
+ it 'clones git repo like http://host/path/to' do
91
+ expect(homesick).to receive(:git_clone)
92
+ .with('http://github.com/technicalpickles/pickled-vim')
93
+
94
+ homesick.clone 'http://github.com/technicalpickles/pickled-vim'
95
+ end
96
+
97
+ it 'clones git repo like host-alias:repos.git' do
98
+ expect(homesick).to receive(:git_clone).with('gitolite:pickled-vim.git')
99
+
100
+ homesick.clone 'gitolite:pickled-vim.git'
101
+ end
102
+
103
+ it 'throws an exception when trying to clone a malformed uri like malformed' do
104
+ expect(homesick).not_to receive(:git_clone)
105
+ expect { homesick.clone 'malformed' }.to raise_error
106
+ end
107
+
108
+ it 'clones a github repo' do
109
+ expect(homesick).to receive(:git_clone)
110
+ .with('https://github.com/wfarr/dotfiles.git',
111
+ destination: Pathname.new('dotfiles'))
112
+
113
+ homesick.clone 'wfarr/dotfiles'
114
+ end
115
+ end
116
+
117
+ describe 'rc' do
118
+ let(:castle) { given_castle('glencairn') }
119
+
120
+ context 'when told to do so' do
121
+ before do
122
+ expect_any_instance_of(Thor::Shell::Basic).to receive(:yes?).with(be_a(String)).and_return(true)
123
+ end
124
+
125
+ it 'executes the .homesickrc' do
126
+ castle.file('.homesickrc') do |file|
127
+ file << "File.open(Dir.pwd + '/testing', 'w') do |f|
128
+ f.print 'testing'
129
+ end"
130
+ end
131
+
132
+ expect(homesick).to receive(:say_status).with('eval', kind_of(Pathname))
133
+ homesick.rc castle
134
+
135
+ expect(castle.join('testing')).to exist
136
+ end
137
+ end
138
+
139
+ context 'when told not to do so' do
140
+ before do
141
+ expect_any_instance_of(Thor::Shell::Basic).to receive(:yes?).with(be_a(String)).and_return(false)
142
+ end
143
+
144
+ it 'does not execute the .homesickrc' do
145
+ castle.file('.homesickrc') do |file|
146
+ file << "File.open(Dir.pwd + '/testing', 'w') do |f|
147
+ f.print 'testing'
148
+ end"
149
+ end
150
+
151
+ expect(homesick).to receive(:say_status).with('eval skip', /not evaling.+/, :blue)
152
+ homesick.rc castle
153
+
154
+ expect(castle.join('testing')).not_to exist
155
+ end
156
+ end
157
+ end
158
+
159
+ describe 'link' do
160
+ let(:castle) { given_castle('glencairn') }
161
+
162
+ it 'links dotfiles from a castle to the home folder' do
163
+ dotfile = castle.file('.some_dotfile')
164
+
165
+ homesick.link('glencairn')
166
+
167
+ expect(home.join('.some_dotfile').readlink).to eq(dotfile)
168
+ end
169
+
170
+ it 'links non-dotfiles from a castle to the home folder' do
171
+ dotfile = castle.file('bin')
172
+
173
+ homesick.link('glencairn')
174
+
175
+ expect(home.join('bin').readlink).to eq(dotfile)
176
+ end
177
+
178
+ context 'when forced' do
179
+ let(:homesick) { Homesick::CLI.new [], force: true }
180
+
181
+ it 'can override symlinks to directories' do
182
+ somewhere_else = create_construct
183
+ existing_dotdir_link = home.join('.vim')
184
+ FileUtils.ln_s somewhere_else, existing_dotdir_link
185
+
186
+ dotdir = castle.directory('.vim')
187
+
188
+ homesick.link('glencairn')
189
+
190
+ expect(existing_dotdir_link.readlink).to eq(dotdir)
191
+ end
192
+
193
+ it 'can override existing directory' do
194
+ existing_dotdir = home.directory('.vim')
195
+
196
+ dotdir = castle.directory('.vim')
197
+
198
+ homesick.link('glencairn')
199
+
200
+ expect(existing_dotdir.readlink).to eq(dotdir)
201
+ end
202
+ end
203
+
204
+ context "with '.config' in .homesick_subdir" do
205
+ let(:castle) { given_castle('glencairn', ['.config']) }
206
+ it 'can symlink in sub directory' do
207
+ dotdir = castle.directory('.config')
208
+ dotfile = dotdir.file('.some_dotfile')
209
+
210
+ homesick.link('glencairn')
211
+
212
+ home_dotdir = home.join('.config')
213
+ expect(home_dotdir.symlink?).to eq(false)
214
+ expect(home_dotdir.join('.some_dotfile').readlink).to eq(dotfile)
215
+ end
216
+ end
217
+
218
+ context "with '.config/appA' in .homesick_subdir" do
219
+ let(:castle) { given_castle('glencairn', ['.config/appA']) }
220
+ it 'can symlink in nested sub directory' do
221
+ dotdir = castle.directory('.config').directory('appA')
222
+ dotfile = dotdir.file('.some_dotfile')
223
+
224
+ homesick.link('glencairn')
225
+
226
+ home_dotdir = home.join('.config').join('appA')
227
+ expect(home_dotdir.symlink?).to eq(false)
228
+ expect(home_dotdir.join('.some_dotfile').readlink).to eq(dotfile)
229
+ end
230
+ end
231
+
232
+ context "with '.config' and '.config/someapp' in .homesick_subdir" do
233
+ let(:castle) do
234
+ given_castle('glencairn', ['.config', '.config/someapp'])
235
+ end
236
+ it 'can symlink under both of .config and .config/someapp' do
237
+ config_dir = castle.directory('.config')
238
+ config_dotfile = config_dir.file('.some_dotfile')
239
+ someapp_dir = config_dir.directory('someapp')
240
+ someapp_dotfile = someapp_dir.file('.some_appfile')
241
+
242
+ homesick.link('glencairn')
243
+
244
+ home_config_dir = home.join('.config')
245
+ home_someapp_dir = home_config_dir.join('someapp')
246
+ expect(home_config_dir.symlink?).to eq(false)
247
+ expect(home_config_dir.join('.some_dotfile').readlink)
248
+ .to eq(config_dotfile)
249
+ expect(home_someapp_dir.symlink?).to eq(false)
250
+ expect(home_someapp_dir.join('.some_appfile').readlink)
251
+ .to eq(someapp_dotfile)
252
+ end
253
+ end
254
+
255
+ context 'when call with no castle name' do
256
+ let(:castle) { given_castle('dotfiles') }
257
+ it 'using default castle name: "dotfiles"' do
258
+ dotfile = castle.file('.some_dotfile')
259
+
260
+ homesick.link
261
+
262
+ expect(home.join('.some_dotfile').readlink).to eq(dotfile)
263
+ end
264
+ end
265
+ end
266
+
267
+ describe 'unlink' do
268
+ let(:castle) { given_castle('glencairn') }
269
+
270
+ it 'unlinks dotfiles in the home folder' do
271
+ castle.file('.some_dotfile')
272
+
273
+ homesick.link('glencairn')
274
+ homesick.unlink('glencairn')
275
+
276
+ expect(home.join('.some_dotfile')).not_to exist
277
+ end
278
+
279
+ it 'unlinks non-dotfiles from the home folder' do
280
+ castle.file('bin')
281
+
282
+ homesick.link('glencairn')
283
+ homesick.unlink('glencairn')
284
+
285
+ expect(home.join('bin')).not_to exist
286
+ end
287
+
288
+ context "with '.config' in .homesick_subdir" do
289
+ let(:castle) { given_castle('glencairn', ['.config']) }
290
+
291
+ it 'can unlink sub directories' do
292
+ castle.directory('.config').file('.some_dotfile')
293
+
294
+ homesick.link('glencairn')
295
+ homesick.unlink('glencairn')
296
+
297
+ home_dotdir = home.join('.config')
298
+ expect(home_dotdir).to exist
299
+ expect(home_dotdir.join('.some_dotfile')).not_to exist
300
+ end
301
+ end
302
+
303
+ context "with '.config/appA' in .homesick_subdir" do
304
+ let(:castle) { given_castle('glencairn', ['.config/appA']) }
305
+
306
+ it 'can unsymlink in nested sub directory' do
307
+ castle.directory('.config').directory('appA').file('.some_dotfile')
308
+
309
+ homesick.link('glencairn')
310
+ homesick.unlink('glencairn')
311
+
312
+ home_dotdir = home.join('.config').join('appA')
313
+ expect(home_dotdir).to exist
314
+ expect(home_dotdir.join('.some_dotfile')).not_to exist
315
+ end
316
+ end
317
+
318
+ context "with '.config' and '.config/someapp' in .homesick_subdir" do
319
+ let(:castle) do
320
+ given_castle('glencairn', ['.config', '.config/someapp'])
321
+ end
322
+
323
+ it 'can unsymlink under both of .config and .config/someapp' do
324
+ config_dir = castle.directory('.config')
325
+ config_dir.file('.some_dotfile')
326
+ config_dir.directory('someapp').file('.some_appfile')
327
+
328
+ homesick.link('glencairn')
329
+ homesick.unlink('glencairn')
330
+
331
+ home_config_dir = home.join('.config')
332
+ home_someapp_dir = home_config_dir.join('someapp')
333
+ expect(home_config_dir).to exist
334
+ expect(home_config_dir.join('.some_dotfile')).not_to exist
335
+ expect(home_someapp_dir).to exist
336
+ expect(home_someapp_dir.join('.some_appfile')).not_to exist
337
+ end
338
+ end
339
+
340
+ context 'when call with no castle name' do
341
+ let(:castle) { given_castle('dotfiles') }
342
+
343
+ it 'using default castle name: "dotfiles"' do
344
+ castle.file('.some_dotfile')
345
+
346
+ homesick.link
347
+ homesick.unlink
348
+
349
+ expect(home.join('.some_dotfile')).not_to exist
350
+ end
351
+ end
352
+ end
353
+
354
+ describe 'list' do
355
+ it 'says each castle in the castle directory' do
356
+ given_castle('zomg')
357
+ given_castle('wtf/zomg')
358
+
359
+ expect(homesick).to receive(:say_status)
360
+ .with('zomg',
361
+ 'git://github.com/technicalpickles/zomg.git',
362
+ :cyan)
363
+ expect(homesick).to receive(:say_status)
364
+ .with('wtf/zomg',
365
+ 'git://github.com/technicalpickles/zomg.git',
366
+ :cyan)
367
+
368
+ homesick.list
369
+ end
370
+ end
371
+
372
+ describe 'status' do
373
+ it 'says "nothing to commit" when there are no changes' do
374
+ given_castle('castle_repo')
375
+ text = Capture.stdout { homesick.status('castle_repo') }
376
+ expect(text).to match(/nothing to commit \(create\/copy files and use "git add" to track\)$/)
377
+ end
378
+
379
+ it 'says "Changes to be committed" when there are changes' do
380
+ given_castle('castle_repo')
381
+ some_rc_file = home.file '.some_rc_file'
382
+ homesick.track(some_rc_file.to_s, 'castle_repo')
383
+ text = Capture.stdout { homesick.status('castle_repo') }
384
+ expect(text).to match(
385
+ /Changes to be committed:.*new file:\s*home\/.some_rc_file/m
386
+ )
387
+ end
388
+ end
389
+
390
+ describe 'diff' do
391
+ it 'outputs an empty message when there are no changes to commit' do
392
+ given_castle('castle_repo')
393
+ some_rc_file = home.file '.some_rc_file'
394
+ homesick.track(some_rc_file.to_s, 'castle_repo')
395
+ Capture.stdout do
396
+ homesick.commit 'castle_repo', 'Adding a file to the test'
397
+ end
398
+ text = Capture.stdout { homesick.diff('castle_repo') }
399
+ expect(text).to eq('')
400
+ end
401
+
402
+ it 'outputs a diff message when there are changes to commit' do
403
+ given_castle('castle_repo')
404
+ some_rc_file = home.file '.some_rc_file'
405
+ homesick.track(some_rc_file.to_s, 'castle_repo')
406
+ Capture.stdout do
407
+ homesick.commit 'castle_repo', 'Adding a file to the test'
408
+ end
409
+ File.open(some_rc_file.to_s, 'w') do |file|
410
+ file.puts 'Some test text'
411
+ end
412
+ text = Capture.stdout { homesick.diff('castle_repo') }
413
+ expect(text).to match(/diff --git.+Some test text$/m)
414
+ end
415
+ end
416
+
417
+ describe 'show_path' do
418
+ it 'says the path of a castle' do
419
+ castle = given_castle('castle_repo')
420
+
421
+ expect(homesick).to receive(:say).with(castle.dirname)
422
+
423
+ homesick.show_path('castle_repo')
424
+ end
425
+ end
426
+
427
+ describe 'pull' do
428
+ it 'performs a pull, submodule init and update when the given castle exists' do
429
+ given_castle('castle_repo')
430
+ allow(homesick).to receive(:system).once.with('git pull --quiet')
431
+ allow(homesick).to receive(:system).once.with('git submodule --quiet init')
432
+ allow(homesick).to receive(:system).once.with('git submodule --quiet update --init --recursive >/dev/null 2>&1')
433
+ homesick.pull 'castle_repo'
434
+ end
435
+
436
+ it 'prints an error message when trying to pull a non-existant castle' do
437
+ expect(homesick).to receive('say_status').once
438
+ .with(:error,
439
+ /Could not pull castle_repo, expected .* exist and contain dotfiles/,
440
+ :red)
441
+ expect { homesick.pull 'castle_repo' }.to raise_error(SystemExit)
442
+ end
443
+
444
+ describe '--all' do
445
+ it 'pulls each castle when invoked with --all' do
446
+ given_castle('castle_repo')
447
+ given_castle('glencairn')
448
+ allow(homesick).to receive(:system).exactly(2).times.with('git pull --quiet')
449
+ allow(homesick).to receive(:system).exactly(2).times
450
+ .with('git submodule --quiet init')
451
+ allow(homesick).to receive(:system).exactly(2).times
452
+ .with('git submodule --quiet update --init --recursive >/dev/null 2>&1')
453
+ Capture.stdout do
454
+ Capture.stderr { homesick.invoke 'pull', [], all: true }
455
+ end
456
+ end
457
+ end
458
+
459
+ end
460
+
461
+ describe 'push' do
462
+ it 'performs a git push on the given castle' do
463
+ given_castle('castle_repo')
464
+ allow(homesick).to receive(:system).once.with('git push')
465
+ homesick.push 'castle_repo'
466
+ end
467
+
468
+ it 'prints an error message when trying to push a non-existant castle' do
469
+ expect(homesick).to receive('say_status').once
470
+ .with(:error,
471
+ /Could not push castle_repo, expected .* exist and contain dotfiles/,
472
+ :red)
473
+ expect { homesick.push 'castle_repo' }.to raise_error(SystemExit)
474
+ end
475
+ end
476
+
477
+ describe 'track' do
478
+ it 'moves the tracked file into the castle' do
479
+ castle = given_castle('castle_repo')
480
+
481
+ some_rc_file = home.file '.some_rc_file'
482
+
483
+ homesick.track(some_rc_file.to_s, 'castle_repo')
484
+
485
+ tracked_file = castle.join('.some_rc_file')
486
+ expect(tracked_file).to exist
487
+
488
+ expect(some_rc_file.readlink).to eq(tracked_file)
489
+ end
490
+
491
+ it 'handles files with parens' do
492
+ castle = given_castle('castle_repo')
493
+
494
+ some_rc_file = home.file 'Default (Linux).sublime-keymap'
495
+
496
+ homesick.track(some_rc_file.to_s, 'castle_repo')
497
+
498
+ tracked_file = castle.join('Default (Linux).sublime-keymap')
499
+ expect(tracked_file).to exist
500
+
501
+ expect(some_rc_file.readlink).to eq(tracked_file)
502
+ end
503
+
504
+ it 'tracks a file in nested folder structure' do
505
+ castle = given_castle('castle_repo')
506
+
507
+ some_nested_file = home.file('some/nested/file.txt')
508
+ homesick.track(some_nested_file.to_s, 'castle_repo')
509
+
510
+ tracked_file = castle.join('some/nested/file.txt')
511
+ expect(tracked_file).to exist
512
+ expect(some_nested_file.readlink).to eq(tracked_file)
513
+ end
514
+
515
+ it 'tracks a nested directory' do
516
+ castle = given_castle('castle_repo')
517
+
518
+ some_nested_dir = home.directory('some/nested/directory/')
519
+ homesick.track(some_nested_dir.to_s, 'castle_repo')
520
+
521
+ tracked_file = castle.join('some/nested/directory/')
522
+ expect(tracked_file).to exist
523
+ expect(some_nested_dir.realpath).to eq(tracked_file.realpath)
524
+ end
525
+
526
+ context 'when call with no castle name' do
527
+ it 'using default castle name: "dotfiles"' do
528
+ castle = given_castle('dotfiles')
529
+
530
+ some_rc_file = home.file '.some_rc_file'
531
+
532
+ homesick.track(some_rc_file.to_s)
533
+
534
+ tracked_file = castle.join('.some_rc_file')
535
+ expect(tracked_file).to exist
536
+
537
+ expect(some_rc_file.readlink).to eq(tracked_file)
538
+ end
539
+ end
540
+
541
+ describe 'commit' do
542
+ it 'has a commit message when the commit succeeds' do
543
+ given_castle('castle_repo')
544
+ some_rc_file = home.file '.a_random_rc_file'
545
+ homesick.track(some_rc_file.to_s, 'castle_repo')
546
+ text = Capture.stdout do
547
+ homesick.commit('castle_repo', 'Test message')
548
+ end
549
+ expect(text).to match(/^\[master \(root-commit\) \w+\] Test message/)
550
+ end
551
+ end
552
+
553
+ # Note that this is a test for the subdir_file related feature of track,
554
+ # not for the subdir_file method itself.
555
+ describe 'subdir_file' do
556
+
557
+ it 'adds the nested files parent to the subdir_file' do
558
+ castle = given_castle('castle_repo')
559
+
560
+ some_nested_file = home.file('some/nested/file.txt')
561
+ homesick.track(some_nested_file.to_s, 'castle_repo')
562
+
563
+ subdir_file = castle.parent.join(Homesick::SUBDIR_FILENAME)
564
+ File.open(subdir_file, 'r') do |f|
565
+ expect(f.readline).to eq("some/nested\n")
566
+ end
567
+ end
568
+
569
+ it 'does NOT add anything if the files parent is already listed' do
570
+ castle = given_castle('castle_repo')
571
+
572
+ some_nested_file = home.file('some/nested/file.txt')
573
+ other_nested_file = home.file('some/nested/other.txt')
574
+ homesick.track(some_nested_file.to_s, 'castle_repo')
575
+ homesick.track(other_nested_file.to_s, 'castle_repo')
576
+
577
+ subdir_file = castle.parent.join(Homesick::SUBDIR_FILENAME)
578
+ File.open(subdir_file, 'r') do |f|
579
+ expect(f.readlines.size).to eq(1)
580
+ end
581
+ end
582
+
583
+ it 'removes the parent of a tracked file from the subdir_file if the parent itself is tracked' do
584
+ castle = given_castle('castle_repo')
585
+
586
+ some_nested_file = home.file('some/nested/file.txt')
587
+ nested_parent = home.directory('some/nested/')
588
+ homesick.track(some_nested_file.to_s, 'castle_repo')
589
+ homesick.track(nested_parent.to_s, 'castle_repo')
590
+
591
+ subdir_file = castle.parent.join(Homesick::SUBDIR_FILENAME)
592
+ File.open(subdir_file, 'r') do |f|
593
+ f.each_line { |line| expect(line).not_to eq("some/nested\n") }
594
+ end
595
+ end
596
+ end
597
+ end
598
+
599
+ describe 'destroy' do
600
+ it 'removes the symlink files' do
601
+ expect_any_instance_of(Thor::Shell::Basic).to receive(:yes?).and_return('y')
602
+ given_castle('stronghold')
603
+ some_rc_file = home.file '.some_rc_file'
604
+ homesick.track(some_rc_file.to_s, 'stronghold')
605
+ homesick.destroy('stronghold')
606
+
607
+ expect(some_rc_file).not_to be_exist
608
+ end
609
+
610
+ it 'deletes the cloned repository' do
611
+ expect_any_instance_of(Thor::Shell::Basic).to receive(:yes?).and_return('y')
612
+ castle = given_castle('stronghold')
613
+ some_rc_file = home.file '.some_rc_file'
614
+ homesick.track(some_rc_file.to_s, 'stronghold')
615
+ homesick.destroy('stronghold')
616
+
617
+ expect(castle).not_to be_exist
618
+ end
619
+ end
620
+
621
+ describe 'cd' do
622
+ it "cd's to the root directory of the given castle" do
623
+ given_castle('castle_repo')
624
+ expect(homesick).to receive('inside').once.with(kind_of(Pathname)).and_yield
625
+ expect(homesick).to receive('system').once.with(ENV['SHELL'])
626
+ Capture.stdout { homesick.cd 'castle_repo' }
627
+ end
628
+
629
+ it 'returns an error message when the given castle does not exist' do
630
+ expect(homesick).to receive('say_status').once
631
+ .with(:error,
632
+ /Could not cd castle_repo, expected .* exist and contain dotfiles/,
633
+ :red)
634
+ expect { homesick.cd 'castle_repo' }.to raise_error(SystemExit)
635
+ end
636
+ end
637
+
638
+ describe 'open' do
639
+ it 'opens the system default editor in the root of the given castle' do
640
+ # Make sure calls to ENV use default values for most things...
641
+ allow(ENV).to receive(:[]).and_call_original
642
+ # Set a default value for 'EDITOR' just in case none is set
643
+ allow(ENV).to receive(:[]).with('EDITOR').and_return('vim')
644
+ given_castle 'castle_repo'
645
+ expect(homesick).to receive('inside').once.with(kind_of(Pathname)).and_yield
646
+ expect(homesick).to receive('system').once.with('vim')
647
+ Capture.stdout { homesick.open 'castle_repo' }
648
+ end
649
+
650
+ it 'returns an error message when the $EDITOR environment variable is not set' do
651
+ # Set the default editor to make sure it fails.
652
+ allow(ENV).to receive(:[]).with('EDITOR').and_return(nil)
653
+ expect(homesick).to receive('say_status').once
654
+ .with(:error,
655
+ 'The $EDITOR environment variable must be set to use this command',
656
+ :red)
657
+ expect { homesick.open 'castle_repo' }.to raise_error(SystemExit)
658
+ end
659
+
660
+ it 'returns an error message when the given castle does not exist' do
661
+ # Set a default just in case none is set
662
+ allow(ENV).to receive(:[]).with('EDITOR').and_return('vim')
663
+ allow(homesick).to receive('say_status').once
664
+ .with(:error,
665
+ /Could not open castle_repo, expected .* exist and contain dotfiles/,
666
+ :red)
667
+ expect { homesick.open 'castle_repo' }.to raise_error(SystemExit)
668
+ end
669
+ end
670
+
671
+ describe 'version' do
672
+ it 'prints the current version of homesick' do
673
+ text = Capture.stdout { homesick.version }
674
+ expect(text.chomp).to match(/\d+\.\d+\.\d+/)
675
+ end
676
+ end
677
+
678
+ describe 'exec' do
679
+ before do
680
+ given_castle 'castle_repo'
681
+ end
682
+ it 'executes a single command with no arguments inside a given castle' do
683
+ allow(homesick).to receive('inside').once.with(kind_of(Pathname)).and_yield
684
+ allow(homesick).to receive('say_status').once
685
+ .with(be_a(String),
686
+ be_a(String),
687
+ :green)
688
+ allow(homesick).to receive('system').once.with('ls')
689
+ Capture.stdout { homesick.exec 'castle_repo', 'ls' }
690
+ end
691
+
692
+ it 'executes a single command with arguments inside a given castle' do
693
+ allow(homesick).to receive('inside').once.with(kind_of(Pathname)).and_yield
694
+ allow(homesick).to receive('say_status').once
695
+ .with(be_a(String),
696
+ be_a(String),
697
+ :green)
698
+ allow(homesick).to receive('system').once.with('ls -la')
699
+ Capture.stdout { homesick.exec 'castle_repo', 'ls', '-la' }
700
+ end
701
+
702
+ it 'raises an error when the method is called without a command' do
703
+ allow(homesick).to receive('say_status').once
704
+ .with(:error,
705
+ be_a(String),
706
+ :red)
707
+ allow(homesick).to receive('exit').once.with(1)
708
+ Capture.stdout { homesick.exec 'castle_repo' }
709
+ end
710
+
711
+ context 'pretend' do
712
+ it 'does not execute a command when the pretend option is passed' do
713
+ allow(homesick).to receive('say_status').once
714
+ .with(be_a(String),
715
+ match(/.*Would execute.*/),
716
+ :green)
717
+ expect(homesick).to receive('system').never
718
+ Capture.stdout { homesick.invoke 'exec', %w(castle_repo ls -la), pretend: true }
719
+ end
720
+ end
721
+
722
+ context 'quiet' do
723
+ it 'does not print status information when quiet is passed' do
724
+ expect(homesick).to receive('say_status').never
725
+ allow(homesick).to receive('system').once
726
+ .with('ls -la')
727
+ Capture.stdout { homesick.invoke 'exec', %w(castle_repo ls -la), quiet: true }
728
+ end
729
+ end
730
+ end
731
+
732
+ describe 'exec_all' do
733
+ before do
734
+ given_castle 'castle_repo'
735
+ given_castle 'another_castle_repo'
736
+ end
737
+
738
+ it 'executes a command without arguments inside the root of each cloned castle' do
739
+ allow(homesick).to receive('inside_each_castle').exactly(:twice).and_yield('castle_repo').and_yield('another_castle_repo')
740
+ allow(homesick).to receive('say_status').at_least(:once)
741
+ .with(be_a(String),
742
+ be_a(String),
743
+ :green)
744
+ allow(homesick).to receive('system').at_least(:once).with('ls')
745
+ Capture.stdout { homesick.exec_all 'ls' }
746
+ end
747
+
748
+ it 'executes a command with arguments inside the root of each cloned castle' do
749
+ allow(homesick).to receive('inside_each_castle').exactly(:twice).and_yield('castle_repo').and_yield('another_castle_repo')
750
+ allow(homesick).to receive('say_status').at_least(:once)
751
+ .with(be_a(String),
752
+ be_a(String),
753
+ :green)
754
+ allow(homesick).to receive('system').at_least(:once).with('ls -la')
755
+ Capture.stdout { homesick.exec_all 'ls', '-la' }
756
+ end
757
+
758
+ it 'raises an error when the method is called without a command' do
759
+ allow(homesick).to receive('say_status').once
760
+ .with(:error,
761
+ be_a(String),
762
+ :red)
763
+ allow(homesick).to receive('exit').once.with(1)
764
+ Capture.stdout { homesick.exec_all }
765
+ end
766
+
767
+ context 'pretend' do
768
+ it 'does not execute a command when the pretend option is passed' do
769
+ allow(homesick).to receive('say_status').at_least(:once)
770
+ .with(be_a(String),
771
+ match(/.*Would execute.*/),
772
+ :green)
773
+ expect(homesick).to receive('system').never
774
+ Capture.stdout { homesick.invoke 'exec_all', %w(ls -la), pretend: true }
775
+ end
776
+ end
777
+
778
+ context 'quiet' do
779
+ it 'does not print status information when quiet is passed' do
780
+ expect(homesick).to receive('say_status').never
781
+ allow(homesick).to receive('system').at_least(:once)
782
+ .with('ls -la')
783
+ Capture.stdout { homesick.invoke 'exec_all', %w(ls -la), quiet: true }
784
+ end
785
+ end
786
+ end
787
+ end