homesick 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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