homesick 1.1.5 → 2.0.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,29 +1,14 @@
1
- # -*- encoding : utf-8 -*-
1
+ # frozen_string_literal: true
2
+
2
3
  require 'pathname'
3
4
 
4
5
  module Homesick
5
6
  # Various utility methods that are used by Homesick
6
7
  module Utils
7
- QUIETABLE = [:say_status]
8
-
9
- PRETENDABLE = [:system]
10
-
11
- QUIETABLE.each do |method_name|
12
- define_method(method_name) do |*args|
13
- super(*args) unless options[:quiet]
14
- end
15
- end
16
-
17
- PRETENDABLE.each do |method_name|
18
- define_method(method_name) do |*args|
19
- super(*args) unless options[:pretend]
20
- end
21
- end
22
-
23
8
  protected
24
9
 
25
10
  def home_dir
26
- @home_dir ||= Pathname.new(ENV['HOME'] || '~').realpath
11
+ @home_dir ||= Pathname.new(Dir.home).realpath
27
12
  end
28
13
 
29
14
  def repos_dir
@@ -36,8 +21,9 @@ module Homesick
36
21
 
37
22
  def check_castle_existance(name, action)
38
23
  return if castle_dir(name).exist?
24
+
39
25
  say_status :error,
40
- "Could not #{action} #{name}, expected #{castle_dir(name)} exist and contain dotfiles",
26
+ "Could not #{action} #{name}, expected #{castle_dir(name)} to exist and contain dotfiles",
41
27
  :red
42
28
  exit(1)
43
29
  end
@@ -115,7 +101,7 @@ module Homesick
115
101
  def subdir_remove(castle, path)
116
102
  subdir_filepath = subdir_file(castle)
117
103
  if subdir_filepath.exist?
118
- lines = IO.readlines(subdir_filepath).delete_if do |line|
104
+ lines = File.readlines(subdir_filepath).delete_if do |line|
119
105
  line == "#{path}\n"
120
106
  end
121
107
  File.open(subdir_filepath, 'w') { |manfile| manfile.puts lines }
@@ -149,44 +135,48 @@ module Homesick
149
135
  end
150
136
 
151
137
  def collision_accepted?(destination, source)
152
- fail "Arguments must be instances of Pathname, #{destination.class.name} and #{source.class.name} given" unless destination.instance_of?(Pathname) && source.instance_of?(Pathname)
153
- options[:force] || shell.file_collision(destination) { File.binread(source) }
154
- end
138
+ unless destination.instance_of?(Pathname) && source.instance_of?(Pathname)
139
+ raise 'Arguments must be instances of Pathname, ' \
140
+ "#{destination.class.name} and #{source.class.name} given"
141
+ end
155
142
 
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
143
+ options[:force] || shell.file_collision(destination) { source }
144
+ end
174
145
 
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
146
+ def clone_from_uri(uri, destination)
147
+ if File.exist?(uri)
148
+ uri_path = Pathname.new(uri).expand_path
149
+ raise "Castle already cloned to #{uri_path}" if uri_path.to_s.start_with?(repos_dir.to_s)
150
+
151
+ destination = uri_path.basename if destination.nil?
152
+ ln_s uri_path, destination
153
+ elsif uri =~ GITHUB_NAME_REPO_PATTERN
154
+ destination = Pathname.new(uri).basename if destination.nil?
155
+ git_clone "https://github.com/#{Regexp.last_match[1]}.git",
156
+ destination: destination
157
+ elsif uri =~ /%r([^%r]*?)(\.git)?\Z/ || uri =~ /[^:]+:([^:]+)(\.git)?\Z/
158
+ destination = Pathname.new(Regexp.last_match[1].gsub(/\.git$/, '')).basename if destination.nil?
159
+ git_clone uri, destination: destination
160
+ else
161
+ raise "Unknown URI format: #{uri}"
162
+ end
184
163
 
185
- relative_dir = absolute_basedir.relative_path_from(castle_home)
186
- home_path = home_dir.join(relative_dir).join(path)
164
+ destination
165
+ end
187
166
 
188
- yield(absolute_path, home_path)
189
- end
167
+ def handle_existing_track_target(castle, absolute_path, castle_path, relative_dir, file, target)
168
+ if absolute_path.directory?
169
+ move_dir_contents(target, absolute_path)
170
+ absolute_path.rmtree
171
+ subdir_remove(castle, relative_dir + file.basename)
172
+ elsif more_recent? absolute_path, target
173
+ target.delete
174
+ mv absolute_path, castle_path
175
+ else
176
+ say_status(:track,
177
+ "#{target} already exists, and is more recent than #{file}. " \
178
+ "Run 'homesick SYMLINK CASTLE' to create symlinks.",
179
+ :blue)
190
180
  end
191
181
  end
192
182
 
@@ -212,5 +202,57 @@ module Homesick
212
202
 
213
203
  rc(path)
214
204
  end
205
+
206
+ def each_file(castle, basedir, subdirs)
207
+ absolute_basedir = Pathname.new(basedir).expand_path
208
+ castle_home = castle_dir(castle)
209
+ inside basedir do |destination_root|
210
+ FileUtils.cd(destination_root) unless destination_root == FileUtils.pwd
211
+ files = Pathname.glob('*', File::FNM_DOTMATCH)
212
+ .reject { |a| ['.', '..'].include?(a.to_s) }
213
+ .reject { |path| matches_ignored_dir? castle_home, path.expand_path, subdirs }
214
+ files.each do |path|
215
+ absolute_path = path.expand_path
216
+
217
+ relative_dir = absolute_basedir.relative_path_from(castle_home)
218
+ home_path = home_dir.join(relative_dir).join(path)
219
+
220
+ yield(absolute_path, home_path)
221
+ end
222
+ end
223
+ end
224
+
225
+ def matches_ignored_dir?(castle_home, absolute_path, subdirs)
226
+ # make ignore dirs
227
+ ignore_dirs = []
228
+ subdirs.each do |subdir|
229
+ # ignore all parent of each line in subdir file
230
+ Pathname.new(subdir).ascend do |p|
231
+ ignore_dirs.push(p)
232
+ end
233
+ end
234
+
235
+ # ignore dirs written in subdir file
236
+ ignore_dirs.uniq.each do |ignore_dir|
237
+ return true if absolute_path == castle_home.join(ignore_dir)
238
+ end
239
+ false
240
+ end
241
+
242
+ def configure_symlinks_diff
243
+ # Hack in support for diffing symlinks
244
+ # Also adds support for checking if destination or content is a directory
245
+ shell_metaclass = class << shell; self; end
246
+ shell_metaclass.send(:define_method, :show_diff) do |destination, source|
247
+ destination = Pathname.new(destination)
248
+ source = Pathname.new(source)
249
+ return 'Unable to create diff: destination or content is a directory' \
250
+ if destination.directory? || source.directory?
251
+ return super(destination, File.binread(source)) unless destination.symlink?
252
+
253
+ say "- #{destination.readlink}", :red, true
254
+ say "+ #{source.expand_path}", :green, true
255
+ end
256
+ end
215
257
  end
216
258
  end
@@ -1,11 +1,12 @@
1
- # -*- encoding : utf-8 -*-
1
+ # frozen_string_literal: true
2
+
2
3
  module Homesick
3
4
  # A representation of Homesick's version number in constants, including a
4
5
  # String of the entire version number
5
6
  module Version
6
- MAJOR = 1
7
- MINOR = 1
8
- PATCH = 5
7
+ MAJOR = 2
8
+ MINOR = 0
9
+ PATCH = 0
9
10
 
10
11
  STRING = [MAJOR, MINOR, PATCH].compact.join('.')
11
12
  end
data/lib/homesick.rb CHANGED
@@ -1,9 +1,12 @@
1
- # -*- encoding : utf-8 -*-
1
+ # frozen_string_literal: true
2
+
2
3
  require 'homesick/actions/file_actions'
3
4
  require 'homesick/actions/git_actions'
4
5
  require 'homesick/version'
6
+ require 'homesick/rc'
5
7
  require 'homesick/utils'
6
8
  require 'homesick/cli'
9
+ require 'fileutils'
7
10
 
8
11
  # Homesick's top-level module
9
12
  module Homesick
@@ -11,4 +14,19 @@ module Homesick
11
14
  SUBDIR_FILENAME = '.homesick_subdir'
12
15
 
13
16
  DEFAULT_CASTLE_NAME = 'dotfiles'
17
+ QUIETABLE = [:say_status].freeze
18
+
19
+ PRETENDABLE = [:system].freeze
20
+
21
+ QUIETABLE.each do |method_name|
22
+ define_method(method_name) do |*args|
23
+ super(*args) unless options[:quiet]
24
+ end
25
+ end
26
+
27
+ PRETENDABLE.each do |method_name|
28
+ define_method(method_name) do |*args|
29
+ super(*args) unless options[:pretend]
30
+ end
31
+ end
14
32
  end
@@ -1,4 +1,5 @@
1
- # -*- encoding : utf-8 -*-
1
+ # frozen_string_literal: true
2
+
2
3
  require 'spec_helper'
3
4
  require 'capture-output'
4
5
  require 'pathname'
@@ -158,8 +159,8 @@ describe Homesick::CLI do
158
159
 
159
160
  it 'accepts a destination', :focus do
160
161
  expect(homesick).to receive(:git_clone)
161
- .with('https://github.com/wfarr/dotfiles.git',
162
- destination: Pathname.new('other-name'))
162
+ .with('https://github.com/wfarr/dotfiles.git',
163
+ destination: Pathname.new('other-name'))
163
164
 
164
165
  homesick.clone 'wfarr/dotfiles', 'other-name'
165
166
  end
@@ -168,6 +169,14 @@ describe Homesick::CLI do
168
169
  describe 'rc' do
169
170
  let(:castle) { given_castle('glencairn') }
170
171
 
172
+ context 'when no .homesickrc is present' do
173
+ it 'does nothing' do
174
+ expect_any_instance_of(Thor::Shell::Basic).not_to receive(:yes?)
175
+ expect(homesick).not_to receive(:say_status).with('eval', anything)
176
+ homesick.rc castle
177
+ end
178
+ end
179
+
171
180
  context 'when told to do so' do
172
181
  before do
173
182
  expect_any_instance_of(Thor::Shell::Basic).to receive(:yes?).with(be_a(String)).and_return(true)
@@ -246,6 +255,34 @@ describe Homesick::CLI do
246
255
  expect(home.join('bin').readlink).to eq(dotfile)
247
256
  end
248
257
 
258
+ context 'when a conflict exists' do
259
+ it 'does not replace the existing file when collision is declined' do
260
+ dotfile = castle.file('.some_dotfile')
261
+ existing = home.file('.some_dotfile', 'original content')
262
+
263
+ allow(homesick.shell).to receive(:file_collision).and_return(false)
264
+
265
+ homesick.link('glencairn')
266
+
267
+ expect(existing.symlink?).to eq(false)
268
+ expect(existing.read).to eq('original content')
269
+ _ = dotfile # ensure castle file exists
270
+ end
271
+
272
+ it 'replaces the existing file when collision is accepted' do
273
+ dotfile = castle.file('.some_dotfile')
274
+ existing = home.file('.some_dotfile', 'original content')
275
+
276
+ allow(homesick.shell).to receive(:file_collision).and_return(true)
277
+
278
+ homesick.link('glencairn')
279
+
280
+ expect(home.join('.some_dotfile').symlink?).to eq(true)
281
+ expect(home.join('.some_dotfile').readlink).to eq(dotfile)
282
+ _ = existing # ensure conflicting file was created
283
+ end
284
+ end
285
+
249
286
  context 'when forced' do
250
287
  let(:homesick) { Homesick::CLI.new [], force: true }
251
288
 
@@ -331,6 +368,32 @@ describe Homesick::CLI do
331
368
  expect(home.join('.some_dotfile').readlink).to eq(dotfile)
332
369
  end
333
370
  end
371
+
372
+ context 'when call and some files conflict' do
373
+ it 'shows differences for conflicting text files' do
374
+ contents = { castle: 'castle has new content', home: 'home already has content' }
375
+
376
+ dotfile = castle.file('text')
377
+ File.write(dotfile.to_s, contents[:castle])
378
+ File.write(home.join('text').to_s, contents[:home])
379
+ message = Capture.stdout { homesick.shell.show_diff(home.join('text'), dotfile) }
380
+ expect(message.b).to match(/- ?#{contents[:home]}\n.*\+ ?#{contents[:castle]}$/m)
381
+ end
382
+ it 'shows message or differences for conflicting binary files' do
383
+ # content which contains NULL character, without any parentheses, braces, ...
384
+ contents = { castle: (0..255).step(30).map(&:chr).join, home: (0..255).step(30).reverse_each.map(&:chr).join }
385
+
386
+ dotfile = castle.file('binary')
387
+ File.write(dotfile.to_s, contents[:castle])
388
+ File.write(home.join('binary').to_s, contents[:home])
389
+ message = Capture.stdout { homesick.shell.show_diff(home.join('binary'), dotfile) }
390
+ if homesick.shell.is_a?(Thor::Shell::Color)
391
+ expect(message.b).to match(/- ?#{contents[:home]}\n.*\+ ?#{contents[:castle]}$/m)
392
+ elsif homesick.shell.is_a?(Thor::Shell::Basic)
393
+ expect(message.b).to match(/^Binary files .+ differ$/)
394
+ end
395
+ end
396
+ end
334
397
  end
335
398
 
336
399
  describe 'unlink' do
@@ -446,7 +509,7 @@ describe Homesick::CLI do
446
509
  some_rc_file = home.file '.some_rc_file'
447
510
  homesick.track(some_rc_file.to_s, 'castle_repo')
448
511
  text = Capture.stdout { homesick.status('castle_repo') }
449
- expect(text).to match(%r{Changes to be committed:.*new file:\s*home\/.some_rc_file}m)
512
+ expect(text).to match(%r{Changes to be committed:.*new file:\s*home/.some_rc_file}m)
450
513
  end
451
514
  end
452
515
 
@@ -498,9 +561,9 @@ describe Homesick::CLI do
498
561
 
499
562
  it 'prints an error message when trying to pull a non-existant castle' do
500
563
  expect(homesick).to receive('say_status').once
501
- .with(:error,
502
- /Could not pull castle_repo, expected .* exist and contain dotfiles/,
503
- :red)
564
+ .with(:error,
565
+ /Could not pull castle_repo, expected .* to exist and contain dotfiles/,
566
+ :red)
504
567
  expect { homesick.pull 'castle_repo' }.to raise_error(SystemExit)
505
568
  end
506
569
 
@@ -510,9 +573,9 @@ describe Homesick::CLI do
510
573
  given_castle('glencairn')
511
574
  allow(homesick).to receive(:system).exactly(2).times.with('git pull --quiet')
512
575
  allow(homesick).to receive(:system).exactly(2).times
513
- .with('git submodule --quiet init')
576
+ .with('git submodule --quiet init')
514
577
  allow(homesick).to receive(:system).exactly(2).times
515
- .with('git submodule --quiet update --init --recursive >/dev/null 2>&1')
578
+ .with('git submodule --quiet update --init --recursive >/dev/null 2>&1')
516
579
  Capture.stdout do
517
580
  Capture.stderr { homesick.invoke 'pull', [], all: true }
518
581
  end
@@ -529,7 +592,7 @@ describe Homesick::CLI do
529
592
 
530
593
  it 'prints an error message when trying to push a non-existant castle' do
531
594
  expect(homesick).to receive('say_status').once
532
- .with(:error, /Could not push castle_repo, expected .* exist and contain dotfiles/, :red)
595
+ .with(:error, /Could not push castle_repo, expected .* to exist and contain dotfiles/, :red)
533
596
  expect { homesick.push 'castle_repo' }.to raise_error(SystemExit)
534
597
  end
535
598
  end
@@ -681,23 +744,21 @@ describe Homesick::CLI do
681
744
  it "cd's to the root directory of the given castle" do
682
745
  given_castle('castle_repo')
683
746
  expect(homesick).to receive('inside').once.with(kind_of(Pathname)).and_yield
684
- expect(homesick).to receive('system').once.with(ENV['SHELL'])
747
+ expect(homesick).to receive('system').once.with(ENV.fetch('SHELL', nil))
685
748
  Capture.stdout { homesick.cd 'castle_repo' }
686
749
  end
687
750
 
688
751
  it 'returns an error message when the given castle does not exist' do
689
752
  expect(homesick).to receive('say_status').once
690
- .with(:error, /Could not cd castle_repo, expected .* exist and contain dotfiles/, :red)
753
+ .with(:error, /Could not cd castle_repo, expected .* to exist and contain dotfiles/, :red)
691
754
  expect { homesick.cd 'castle_repo' }.to raise_error(SystemExit)
692
755
  end
693
756
  end
694
757
 
695
758
  describe 'open' do
696
759
  it 'opens the system default editor in the root of the given castle' do
697
- # Make sure calls to ENV use default values for most things...
698
- allow(ENV).to receive(:[]).and_call_original
699
760
  # Set a default value for 'EDITOR' just in case none is set
700
- allow(ENV).to receive(:[]).with('EDITOR').and_return('vim')
761
+ allow(ENV).to receive(:fetch).with('EDITOR', nil).and_return('vim')
701
762
  given_castle 'castle_repo'
702
763
  expect(homesick).to receive('inside').once.with(kind_of(Pathname)).and_yield
703
764
  expect(homesick).to receive('system').once.with('vim .')
@@ -706,17 +767,17 @@ describe Homesick::CLI do
706
767
 
707
768
  it 'returns an error message when the $EDITOR environment variable is not set' do
708
769
  # Set the default editor to make sure it fails.
709
- allow(ENV).to receive(:[]).with('EDITOR').and_return(nil)
770
+ allow(ENV).to receive(:fetch).with('EDITOR', nil).and_return(nil)
710
771
  expect(homesick).to receive('say_status').once
711
- .with(:error, 'The $EDITOR environment variable must be set to use this command', :red)
772
+ .with(:error, 'The $EDITOR environment variable must be set to use this command', :red)
712
773
  expect { homesick.open 'castle_repo' }.to raise_error(SystemExit)
713
774
  end
714
775
 
715
776
  it 'returns an error message when the given castle does not exist' do
716
777
  # Set a default just in case none is set
717
- allow(ENV).to receive(:[]).with('EDITOR').and_return('vim')
778
+ allow(ENV).to receive(:fetch).with('EDITOR', nil).and_return('vim')
718
779
  allow(homesick).to receive('say_status').once
719
- .with(:error, /Could not open castle_repo, expected .* exist and contain dotfiles/, :red)
780
+ .with(:error, /Could not open castle_repo, expected .* to exist and contain dotfiles/, :red)
720
781
  expect { homesick.open 'castle_repo' }.to raise_error(SystemExit)
721
782
  end
722
783
  end
@@ -735,7 +796,7 @@ describe Homesick::CLI do
735
796
  it 'executes a single command with no arguments inside a given castle' do
736
797
  allow(homesick).to receive('inside').once.with(kind_of(Pathname)).and_yield
737
798
  allow(homesick).to receive('say_status').once
738
- .with(be_a(String), be_a(String), :green)
799
+ .with(be_a(String), be_a(String), :green)
739
800
  allow(homesick).to receive('system').once.with('ls')
740
801
  Capture.stdout { homesick.exec 'castle_repo', 'ls' }
741
802
  end
@@ -743,14 +804,14 @@ describe Homesick::CLI do
743
804
  it 'executes a single command with arguments inside a given castle' do
744
805
  allow(homesick).to receive('inside').once.with(kind_of(Pathname)).and_yield
745
806
  allow(homesick).to receive('say_status').once
746
- .with(be_a(String), be_a(String), :green)
807
+ .with(be_a(String), be_a(String), :green)
747
808
  allow(homesick).to receive('system').once.with('ls -la')
748
809
  Capture.stdout { homesick.exec 'castle_repo', 'ls', '-la' }
749
810
  end
750
811
 
751
812
  it 'raises an error when the method is called without a command' do
752
813
  allow(homesick).to receive('say_status').once
753
- .with(:error, be_a(String), :red)
814
+ .with(:error, be_a(String), :red)
754
815
  allow(homesick).to receive('exit').once.with(1)
755
816
  Capture.stdout { homesick.exec 'castle_repo' }
756
817
  end
@@ -758,9 +819,9 @@ describe Homesick::CLI do
758
819
  context 'pretend' do
759
820
  it 'does not execute a command when the pretend option is passed' do
760
821
  allow(homesick).to receive('say_status').once
761
- .with(be_a(String), match(/.*Would execute.*/), :green)
822
+ .with(be_a(String), match(/.*Would execute.*/), :green)
762
823
  expect(homesick).to receive('system').never
763
- Capture.stdout { homesick.invoke 'exec', %w(castle_repo ls -la), pretend: true }
824
+ Capture.stdout { homesick.invoke 'exec', %w[castle_repo ls -la], pretend: true }
764
825
  end
765
826
  end
766
827
 
@@ -768,8 +829,8 @@ describe Homesick::CLI do
768
829
  it 'does not print status information when quiet is passed' do
769
830
  expect(homesick).to receive('say_status').never
770
831
  allow(homesick).to receive('system').once
771
- .with('ls -la')
772
- Capture.stdout { homesick.invoke 'exec', %w(castle_repo ls -la), quiet: true }
832
+ .with('ls -la')
833
+ Capture.stdout { homesick.invoke 'exec', %w[castle_repo ls -la], quiet: true }
773
834
  end
774
835
  end
775
836
  end
@@ -783,7 +844,7 @@ describe Homesick::CLI do
783
844
  it 'executes a command without arguments inside the root of each cloned castle' do
784
845
  allow(homesick).to receive('inside_each_castle').exactly(:twice).and_yield('castle_repo').and_yield('another_castle_repo')
785
846
  allow(homesick).to receive('say_status').at_least(:once)
786
- .with(be_a(String), be_a(String), :green)
847
+ .with(be_a(String), be_a(String), :green)
787
848
  allow(homesick).to receive('system').at_least(:once).with('ls')
788
849
  Capture.stdout { homesick.exec_all 'ls' }
789
850
  end
@@ -791,14 +852,14 @@ describe Homesick::CLI do
791
852
  it 'executes a command with arguments inside the root of each cloned castle' do
792
853
  allow(homesick).to receive('inside_each_castle').exactly(:twice).and_yield('castle_repo').and_yield('another_castle_repo')
793
854
  allow(homesick).to receive('say_status').at_least(:once)
794
- .with(be_a(String), be_a(String), :green)
855
+ .with(be_a(String), be_a(String), :green)
795
856
  allow(homesick).to receive('system').at_least(:once).with('ls -la')
796
857
  Capture.stdout { homesick.exec_all 'ls', '-la' }
797
858
  end
798
859
 
799
860
  it 'raises an error when the method is called without a command' do
800
861
  allow(homesick).to receive('say_status').once
801
- .with(:error, be_a(String), :red)
862
+ .with(:error, be_a(String), :red)
802
863
  allow(homesick).to receive('exit').once.with(1)
803
864
  Capture.stdout { homesick.exec_all }
804
865
  end
@@ -806,9 +867,9 @@ describe Homesick::CLI do
806
867
  context 'pretend' do
807
868
  it 'does not execute a command when the pretend option is passed' do
808
869
  allow(homesick).to receive('say_status').at_least(:once)
809
- .with(be_a(String), match(/.*Would execute.*/), :green)
870
+ .with(be_a(String), match(/.*Would execute.*/), :green)
810
871
  expect(homesick).to receive('system').never
811
- Capture.stdout { homesick.invoke 'exec_all', %w(ls -la), pretend: true }
872
+ Capture.stdout { homesick.invoke 'exec_all', %w[ls -la], pretend: true }
812
873
  end
813
874
  end
814
875
 
@@ -816,8 +877,8 @@ describe Homesick::CLI do
816
877
  it 'does not print status information when quiet is passed' do
817
878
  expect(homesick).to receive('say_status').never
818
879
  allow(homesick).to receive('system').at_least(:once)
819
- .with('ls -la')
820
- Capture.stdout { homesick.invoke 'exec_all', %w(ls -la), quiet: true }
880
+ .with('ls -la')
881
+ Capture.stdout { homesick.invoke 'exec_all', %w[ls -la], quiet: true }
821
882
  end
822
883
  end
823
884
  end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'tmpdir'
5
+ require 'pathname'
6
+
7
+ describe Homesick::RC::Context do
8
+ let(:tmpdir) { Pathname.new(Dir.mktmpdir) }
9
+ let(:home) { tmpdir } # satisfies spec_helper's global ENV['HOME'] hook
10
+ after { tmpdir.rmtree }
11
+
12
+ # RC::Context does not emit say_status; suppress the global silence! hook.
13
+ def silence!; end
14
+
15
+ subject(:context) { described_class.new(tmpdir) }
16
+
17
+ describe '#castle_path' do
18
+ it 'returns a Pathname equal to the given castle path' do
19
+ expect(context.castle_path).to eq(tmpdir)
20
+ end
21
+
22
+ it 'returns a Pathname instance' do
23
+ expect(context.castle_path).to be_a(Pathname)
24
+ end
25
+
26
+ it 'accepts a string path and coerces it to Pathname' do
27
+ ctx = described_class.new(tmpdir.to_s)
28
+ expect(ctx.castle_path).to eq(tmpdir)
29
+ end
30
+ end
31
+
32
+ describe '#run' do
33
+ it 'executes the given shell command and returns true on success' do
34
+ marker = tmpdir.join('ran')
35
+ result = context.run("touch #{marker}")
36
+ expect(result).to be(true)
37
+ expect(marker).to exist
38
+ end
39
+
40
+ it 'returns false when the command fails' do
41
+ result = context.run('false')
42
+ expect(result).to be(false)
43
+ end
44
+ end
45
+
46
+ describe 'instance_eval isolation' do
47
+ it 'does not expose Homesick::CLI methods to the script' do
48
+ # check_castle_existance is a CLI method; the context should not have it
49
+ expect { context.instance_eval('check_castle_existance("x", "y")', __FILE__, __LINE__) }
50
+ .to raise_error(NoMethodError)
51
+ end
52
+
53
+ it 'allows standard Ruby file operations' do
54
+ marker = tmpdir.join('from_script')
55
+ context.instance_eval { File.write(marker.to_s, 'ok') }
56
+ expect(marker.read).to eq('ok')
57
+ end
58
+ end
59
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,5 +1,4 @@
1
- require 'coveralls'
2
- Coveralls.wear!
1
+ # frozen_string_literal: true
3
2
 
4
3
  $LOAD_PATH.unshift(File.dirname(__FILE__))
5
4
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))