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.
- checksums.yaml +5 -5
- data/.github/dependabot.yml +9 -0
- data/.github/workflows/ci.yml +67 -0
- data/.github/workflows/mutant-nightly.yml +31 -0
- data/.github/workflows/release.yml +27 -0
- data/.gitignore +52 -0
- data/.mutant.yml +26 -0
- data/.rubocop.yml +37 -14
- data/ChangeLog.markdown +38 -6
- data/Gemfile +3 -34
- data/Gemfile.lock +128 -0
- data/Guardfile +6 -4
- data/README.markdown +10 -14
- data/Rakefile +8 -62
- data/bin/homesick +1 -0
- data/homesick.gemspec +28 -99
- data/lib/homesick/actions/file_actions.rb +31 -37
- data/lib/homesick/actions/git_actions.rb +18 -15
- data/lib/homesick/cli.rb +40 -62
- data/lib/homesick/rc.rb +30 -0
- data/lib/homesick/utils.rb +96 -54
- data/lib/homesick/version.rb +5 -4
- data/lib/homesick.rb +19 -1
- data/spec/homesick_cli_spec.rb +94 -33
- data/spec/homesick_rc_spec.rb +59 -0
- data/spec/spec_helper.rb +1 -2
- metadata +37 -120
- data/.travis.yml +0 -6
data/lib/homesick/utils.rb
CHANGED
|
@@ -1,29 +1,14 @@
|
|
|
1
|
-
#
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
157
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
186
|
-
|
|
164
|
+
destination
|
|
165
|
+
end
|
|
187
166
|
|
|
188
|
-
|
|
189
|
-
|
|
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
|
data/lib/homesick/version.rb
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
#
|
|
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 =
|
|
7
|
-
MINOR =
|
|
8
|
-
PATCH =
|
|
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
|
-
#
|
|
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
|
data/spec/homesick_cli_spec.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
#
|
|
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
|
-
|
|
162
|
-
|
|
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
|
|
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
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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
|
-
|
|
576
|
+
.with('git submodule --quiet init')
|
|
514
577
|
allow(homesick).to receive(:system).exactly(2).times
|
|
515
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(:
|
|
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(:
|
|
770
|
+
allow(ENV).to receive(:fetch).with('EDITOR', nil).and_return(nil)
|
|
710
771
|
expect(homesick).to receive('say_status').once
|
|
711
|
-
|
|
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(:
|
|
778
|
+
allow(ENV).to receive(:fetch).with('EDITOR', nil).and_return('vim')
|
|
718
779
|
allow(homesick).to receive('say_status').once
|
|
719
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
822
|
+
.with(be_a(String), match(/.*Would execute.*/), :green)
|
|
762
823
|
expect(homesick).to receive('system').never
|
|
763
|
-
Capture.stdout { homesick.invoke 'exec', %w
|
|
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
|
-
|
|
772
|
-
Capture.stdout { homesick.invoke 'exec', %w
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
820
|
-
Capture.stdout { homesick.invoke 'exec_all', %w
|
|
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