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.
- checksums.yaml +4 -4
- data/.rubocop.yml +19 -0
- data/.travis.yml +1 -0
- data/ChangeLog.markdown +4 -0
- data/Gemfile +2 -2
- data/README.markdown +11 -0
- data/bin/homesick +1 -1
- data/homesick.gemspec +14 -16
- data/lib/homesick.rb +9 -444
- data/lib/homesick/actions/file_actions.rb +91 -0
- data/lib/homesick/actions/git_actions.rb +94 -0
- data/lib/homesick/cli.rb +316 -0
- data/lib/homesick/shell.rb +3 -3
- data/lib/homesick/utils.rb +216 -0
- data/lib/homesick/version.rb +5 -3
- data/spec/homesick_cli_spec.rb +787 -0
- data/spec/spec_helper.rb +7 -2
- metadata +37 -61
- data/lib/homesick/actions.rb +0 -175
- data/spec/homesick_spec.rb +0 -621
@@ -0,0 +1,91 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
module Homesick
|
3
|
+
module Actions
|
4
|
+
# File-related helper methods for Homesick
|
5
|
+
module FileActions
|
6
|
+
def mv(source, destination, config = {})
|
7
|
+
source = Pathname.new(source)
|
8
|
+
destination = Pathname.new(destination + source.basename)
|
9
|
+
|
10
|
+
if destination.exist?
|
11
|
+
say_status :conflict, "#{destination} exists", :red
|
12
|
+
|
13
|
+
FileUtils.mv source, destination if (options[:force] || shell.file_collision(destination) { source }) && !options[:pretend]
|
14
|
+
else
|
15
|
+
# this needs some sort of message here.
|
16
|
+
FileUtils.mv source, destination unless options[:pretend]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def rm_rf(dir)
|
21
|
+
say_status "rm -rf #{dir}", '', :green
|
22
|
+
FileUtils.rm_r dir, force: true
|
23
|
+
end
|
24
|
+
|
25
|
+
def rm_link(target)
|
26
|
+
target = Pathname.new(target)
|
27
|
+
|
28
|
+
if target.symlink?
|
29
|
+
say_status :unlink, "#{target.expand_path}", :green
|
30
|
+
FileUtils.rm_rf target
|
31
|
+
else
|
32
|
+
say_status :conflict, "#{target} is not a symlink", :red
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def rm(file)
|
37
|
+
say_status "rm #{file}", '', :green
|
38
|
+
FileUtils.rm file, force: true
|
39
|
+
end
|
40
|
+
|
41
|
+
def rm_r(dir)
|
42
|
+
say_status "rm -r #{dir}", '', :green
|
43
|
+
FileUtils.rm_r dir
|
44
|
+
end
|
45
|
+
|
46
|
+
def ln_s(source, destination, config = {})
|
47
|
+
source = Pathname.new(source)
|
48
|
+
destination = Pathname.new(destination)
|
49
|
+
FileUtils.mkdir_p destination.dirname
|
50
|
+
|
51
|
+
action = if destination.symlink? && destination.readlink == source
|
52
|
+
:identical
|
53
|
+
elsif destination.symlink?
|
54
|
+
:symlink_conflict
|
55
|
+
elsif destination.exist?
|
56
|
+
:conflict
|
57
|
+
else
|
58
|
+
:success
|
59
|
+
end
|
60
|
+
|
61
|
+
handle_symlink_action action, source, destination
|
62
|
+
end
|
63
|
+
|
64
|
+
def handle_symlink_action(action, source, destination)
|
65
|
+
case action
|
66
|
+
when :identical
|
67
|
+
say_status :identical, destination.expand_path, :blue
|
68
|
+
when :symlink_conflict
|
69
|
+
say_status :conflict,
|
70
|
+
"#{destination} exists and points to #{destination.readlink}",
|
71
|
+
:red
|
72
|
+
|
73
|
+
FileUtils.rm destination
|
74
|
+
FileUtils.ln_s source, destination, force: true unless options[:pretend]
|
75
|
+
when :conflict
|
76
|
+
say_status :conflict, "#{destination} exists", :red
|
77
|
+
|
78
|
+
if collision_accepted?(destination)
|
79
|
+
FileUtils.rm_r destination, force: true unless options[:pretend]
|
80
|
+
FileUtils.ln_s source, destination, force: true unless options[:pretend]
|
81
|
+
end
|
82
|
+
else
|
83
|
+
say_status :symlink,
|
84
|
+
"#{source.expand_path} to #{destination.expand_path}",
|
85
|
+
:green
|
86
|
+
FileUtils.ln_s source, destination unless options[:pretend]
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
module Homesick
|
3
|
+
module Actions
|
4
|
+
# Git-related helper methods for Homesick
|
5
|
+
module GitActions
|
6
|
+
# TODO: move this to be more like thor's template, empty_directory, etc
|
7
|
+
def git_clone(repo, config = {})
|
8
|
+
config ||= {}
|
9
|
+
destination = config[:destination] || File.basename(repo, '.git')
|
10
|
+
|
11
|
+
destination = Pathname.new(destination) unless destination.kind_of?(Pathname)
|
12
|
+
FileUtils.mkdir_p destination.dirname
|
13
|
+
|
14
|
+
if destination.directory?
|
15
|
+
say_status :exist, destination.expand_path, :blue
|
16
|
+
else
|
17
|
+
say_status 'git clone',
|
18
|
+
"#{repo} to #{destination.expand_path}",
|
19
|
+
:green
|
20
|
+
system "git clone -q --config push.default=upstream --recursive #{repo} #{destination}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def git_init(path = '.')
|
25
|
+
path = Pathname.new(path)
|
26
|
+
|
27
|
+
inside path do
|
28
|
+
if path.join('.git').exist?
|
29
|
+
say_status 'git init', 'already initialized', :blue
|
30
|
+
else
|
31
|
+
say_status 'git init', ''
|
32
|
+
system 'git init >/dev/null'
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def git_remote_add(name, url)
|
38
|
+
existing_remote = `git config remote.#{name}.url`.chomp
|
39
|
+
existing_remote = nil if existing_remote == ''
|
40
|
+
|
41
|
+
if existing_remote
|
42
|
+
say_status 'git remote', "#{name} already exists", :blue
|
43
|
+
else
|
44
|
+
say_status 'git remote', "add #{name} #{url}"
|
45
|
+
system "git remote add #{name} #{url}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def git_submodule_init(config = {})
|
50
|
+
say_status 'git submodule', 'init', :green
|
51
|
+
system 'git submodule --quiet init'
|
52
|
+
end
|
53
|
+
|
54
|
+
def git_submodule_update(config = {})
|
55
|
+
say_status 'git submodule', 'update', :green
|
56
|
+
system 'git submodule --quiet update --init --recursive >/dev/null 2>&1'
|
57
|
+
end
|
58
|
+
|
59
|
+
def git_pull(config = {})
|
60
|
+
say_status 'git pull', '', :green
|
61
|
+
system 'git pull --quiet'
|
62
|
+
end
|
63
|
+
|
64
|
+
def git_push(config = {})
|
65
|
+
say_status 'git push', '', :green
|
66
|
+
system 'git push'
|
67
|
+
end
|
68
|
+
|
69
|
+
def git_commit_all(config = {})
|
70
|
+
say_status 'git commit all', '', :green
|
71
|
+
if config[:message]
|
72
|
+
system "git commit -a -m '#{config[:message]}'"
|
73
|
+
else
|
74
|
+
system 'git commit -v -a'
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def git_add(file, config = {})
|
79
|
+
say_status 'git add file', '', :green
|
80
|
+
system "git add '#{file}'"
|
81
|
+
end
|
82
|
+
|
83
|
+
def git_status(config = {})
|
84
|
+
say_status 'git status', '', :green
|
85
|
+
system 'git status'
|
86
|
+
end
|
87
|
+
|
88
|
+
def git_diff(config = {})
|
89
|
+
say_status 'git diff', '', :green
|
90
|
+
system 'git diff'
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
data/lib/homesick/cli.rb
ADDED
@@ -0,0 +1,316 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require 'thor'
|
3
|
+
|
4
|
+
module Homesick
|
5
|
+
# Homesick's command line interface
|
6
|
+
class CLI < Thor
|
7
|
+
include Thor::Actions
|
8
|
+
include Homesick::Actions::FileActions
|
9
|
+
include Homesick::Actions::GitActions
|
10
|
+
include Homesick::Version
|
11
|
+
include Homesick::Utils
|
12
|
+
|
13
|
+
add_runtime_options!
|
14
|
+
|
15
|
+
map '-v' => :version
|
16
|
+
map '--version' => :version
|
17
|
+
# Retain a mapped version of the symlink command for compatibility.
|
18
|
+
map symlink: :link
|
19
|
+
|
20
|
+
def initialize(args = [], options = {}, config = {})
|
21
|
+
super
|
22
|
+
self.shell = Homesick::Shell.new
|
23
|
+
end
|
24
|
+
|
25
|
+
desc 'clone URI', 'Clone +uri+ as a castle for homesick'
|
26
|
+
def clone(uri)
|
27
|
+
inside repos_dir do
|
28
|
+
destination = nil
|
29
|
+
if File.exist?(uri)
|
30
|
+
uri = Pathname.new(uri).expand_path
|
31
|
+
fail "Castle already cloned to #{uri}" if uri.to_s.start_with?(repos_dir.to_s)
|
32
|
+
|
33
|
+
destination = uri.basename
|
34
|
+
|
35
|
+
ln_s uri, destination
|
36
|
+
elsif uri =~ GITHUB_NAME_REPO_PATTERN
|
37
|
+
destination = Pathname.new(uri).basename
|
38
|
+
git_clone "https://github.com/#{Regexp.last_match[1]}.git",
|
39
|
+
destination: destination
|
40
|
+
elsif uri =~ /%r([^%r]*?)(\.git)?\Z/ || uri =~ /[^:]+:([^:]+)(\.git)?\Z/
|
41
|
+
destination = Pathname.new(Regexp.last_match[1])
|
42
|
+
git_clone uri
|
43
|
+
else
|
44
|
+
fail "Unknown URI format: #{uri}"
|
45
|
+
end
|
46
|
+
|
47
|
+
setup_castle(destination)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
desc 'rc CASTLE', 'Run the .homesickrc for the specified castle'
|
52
|
+
def rc(name = DEFAULT_CASTLE_NAME)
|
53
|
+
inside repos_dir do
|
54
|
+
destination = Pathname.new(name)
|
55
|
+
homesickrc = destination.join('.homesickrc').expand_path
|
56
|
+
if homesickrc.exist?
|
57
|
+
proceed = shell.yes?("#{name} has a .homesickrc. Proceed with evaling it? (This could be destructive)")
|
58
|
+
if proceed
|
59
|
+
say_status 'eval', homesickrc
|
60
|
+
inside destination do
|
61
|
+
eval homesickrc.read, binding, homesickrc.expand_path.to_s
|
62
|
+
end
|
63
|
+
else
|
64
|
+
say_status 'eval skip',
|
65
|
+
"not evaling #{homesickrc}, #{destination} may need manual configuration",
|
66
|
+
:blue
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
desc 'pull CASTLE', 'Update the specified castle'
|
73
|
+
method_option :all,
|
74
|
+
type: :boolean,
|
75
|
+
default: false,
|
76
|
+
required: false,
|
77
|
+
desc: 'Update all cloned castles'
|
78
|
+
def pull(name = DEFAULT_CASTLE_NAME)
|
79
|
+
if options[:all]
|
80
|
+
inside_each_castle do |castle|
|
81
|
+
say castle.to_s.gsub(repos_dir.to_s + '/', '') + ':'
|
82
|
+
update_castle castle
|
83
|
+
end
|
84
|
+
else
|
85
|
+
update_castle name
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
desc 'commit CASTLE MESSAGE', "Commit the specified castle's changes"
|
90
|
+
def commit(name = DEFAULT_CASTLE_NAME, message = nil)
|
91
|
+
commit_castle name, message
|
92
|
+
end
|
93
|
+
|
94
|
+
desc 'push CASTLE', 'Push the specified castle'
|
95
|
+
def push(name = DEFAULT_CASTLE_NAME)
|
96
|
+
push_castle name
|
97
|
+
end
|
98
|
+
|
99
|
+
desc 'unlink CASTLE', 'Unsymlinks all dotfiles from the specified castle'
|
100
|
+
def unlink(name = DEFAULT_CASTLE_NAME)
|
101
|
+
check_castle_existance(name, 'symlink')
|
102
|
+
|
103
|
+
inside castle_dir(name) do
|
104
|
+
subdirs = subdirs(name)
|
105
|
+
|
106
|
+
# unlink files
|
107
|
+
unsymlink_each(name, castle_dir(name), subdirs)
|
108
|
+
|
109
|
+
# unlink files in subdirs
|
110
|
+
subdirs.each do |subdir|
|
111
|
+
unsymlink_each(name, subdir, subdirs)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
desc 'link CASTLE', 'Symlinks all dotfiles from the specified castle'
|
117
|
+
method_option :force,
|
118
|
+
default: false,
|
119
|
+
desc: 'Overwrite existing conflicting symlinks without prompting.'
|
120
|
+
def link(name = DEFAULT_CASTLE_NAME)
|
121
|
+
check_castle_existance(name, 'symlink')
|
122
|
+
|
123
|
+
inside castle_dir(name) do
|
124
|
+
subdirs = subdirs(name)
|
125
|
+
|
126
|
+
# link files
|
127
|
+
symlink_each(name, castle_dir(name), subdirs)
|
128
|
+
|
129
|
+
# link files in subdirs
|
130
|
+
subdirs.each do |subdir|
|
131
|
+
symlink_each(name, subdir, subdirs)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
desc 'track FILE CASTLE', 'add a file to a castle'
|
137
|
+
def track(file, castle = DEFAULT_CASTLE_NAME)
|
138
|
+
castle = Pathname.new(castle)
|
139
|
+
file = Pathname.new(file.chomp('/'))
|
140
|
+
check_castle_existance(castle, 'track')
|
141
|
+
|
142
|
+
absolute_path = file.expand_path
|
143
|
+
relative_dir = absolute_path.relative_path_from(home_dir).dirname
|
144
|
+
castle_path = Pathname.new(castle_dir(castle)).join(relative_dir)
|
145
|
+
FileUtils.mkdir_p castle_path
|
146
|
+
|
147
|
+
# Are we already tracking this or anything inside it?
|
148
|
+
target = Pathname.new(castle_path.join(file.basename))
|
149
|
+
if target.exist?
|
150
|
+
if absolute_path.directory?
|
151
|
+
move_dir_contents(target, absolute_path)
|
152
|
+
absolute_path.rmtree
|
153
|
+
subdir_remove(castle, relative_dir + file.basename)
|
154
|
+
|
155
|
+
elsif more_recent? absolute_path, target
|
156
|
+
target.delete
|
157
|
+
mv absolute_path, castle_path
|
158
|
+
else
|
159
|
+
say_status(:track,
|
160
|
+
"#{target} already exists, and is more recent than #{file}. Run 'homesick SYMLINK CASTLE' to create symlinks.",
|
161
|
+
:blue)
|
162
|
+
end
|
163
|
+
else
|
164
|
+
mv absolute_path, castle_path
|
165
|
+
end
|
166
|
+
|
167
|
+
inside home_dir do
|
168
|
+
absolute_path = castle_path + file.basename
|
169
|
+
home_path = home_dir + relative_dir + file.basename
|
170
|
+
ln_s absolute_path, home_path
|
171
|
+
end
|
172
|
+
|
173
|
+
inside castle_path do
|
174
|
+
git_add absolute_path
|
175
|
+
end
|
176
|
+
|
177
|
+
# are we tracking something nested? Add the parent dir to the manifest
|
178
|
+
subdir_add(castle, relative_dir) unless relative_dir.eql?(Pathname.new('.'))
|
179
|
+
end
|
180
|
+
|
181
|
+
desc 'list', 'List cloned castles'
|
182
|
+
def list
|
183
|
+
inside_each_castle do |castle|
|
184
|
+
say_status castle.relative_path_from(repos_dir).to_s,
|
185
|
+
`git config remote.origin.url`.chomp,
|
186
|
+
:cyan
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
desc 'status CASTLE', 'Shows the git status of a castle'
|
191
|
+
def status(castle = DEFAULT_CASTLE_NAME)
|
192
|
+
check_castle_existance(castle, 'status')
|
193
|
+
inside repos_dir.join(castle) do
|
194
|
+
git_status
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
desc 'diff CASTLE', 'Shows the git diff of uncommitted changes in a castle'
|
199
|
+
def diff(castle = DEFAULT_CASTLE_NAME)
|
200
|
+
check_castle_existance(castle, 'diff')
|
201
|
+
inside repos_dir.join(castle) do
|
202
|
+
git_diff
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
desc 'show_path CASTLE', 'Prints the path of a castle'
|
207
|
+
def show_path(castle = DEFAULT_CASTLE_NAME)
|
208
|
+
check_castle_existance(castle, 'show_path')
|
209
|
+
say repos_dir.join(castle)
|
210
|
+
end
|
211
|
+
|
212
|
+
desc 'generate PATH', 'generate a homesick-ready git repo at PATH'
|
213
|
+
def generate(castle)
|
214
|
+
castle = Pathname.new(castle).expand_path
|
215
|
+
|
216
|
+
github_user = `git config github.user`.chomp
|
217
|
+
github_user = nil if github_user == ''
|
218
|
+
github_repo = castle.basename
|
219
|
+
|
220
|
+
empty_directory castle
|
221
|
+
inside castle do
|
222
|
+
git_init
|
223
|
+
if github_user
|
224
|
+
url = "git@github.com:#{github_user}/#{github_repo}.git"
|
225
|
+
git_remote_add 'origin', url
|
226
|
+
end
|
227
|
+
|
228
|
+
empty_directory 'home'
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
desc 'destroy CASTLE', 'Delete all symlinks and remove the cloned repository'
|
233
|
+
def destroy(name)
|
234
|
+
check_castle_existance name, 'destroy'
|
235
|
+
|
236
|
+
if shell.yes?('This will destroy your castle irreversible! Are you sure?')
|
237
|
+
unlink(name)
|
238
|
+
rm_rf repos_dir.join(name)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
desc 'cd CASTLE', 'Open a new shell in the root of the given castle'
|
243
|
+
def cd(castle = DEFAULT_CASTLE_NAME)
|
244
|
+
check_castle_existance castle, 'cd'
|
245
|
+
castle_dir = repos_dir.join(castle)
|
246
|
+
say_status "cd #{castle_dir.realpath}",
|
247
|
+
"Opening a new shell in castle '#{castle}'. To return to the original one exit from the new shell.",
|
248
|
+
:green
|
249
|
+
inside castle_dir do
|
250
|
+
system(ENV['SHELL'])
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
desc 'open CASTLE',
|
255
|
+
'Open your default editor in the root of the given castle'
|
256
|
+
def open(castle = DEFAULT_CASTLE_NAME)
|
257
|
+
unless ENV['EDITOR']
|
258
|
+
say_status :error,
|
259
|
+
'The $EDITOR environment variable must be set to use this command',
|
260
|
+
:red
|
261
|
+
|
262
|
+
exit(1)
|
263
|
+
end
|
264
|
+
check_castle_existance castle, 'open'
|
265
|
+
castle_dir = repos_dir.join(castle)
|
266
|
+
say_status "#{ENV['EDITOR']} #{castle_dir.realpath}",
|
267
|
+
"Opening the root directory of castle '#{castle}' in editor '#{ENV['EDITOR']}'.",
|
268
|
+
:green
|
269
|
+
inside castle_dir do
|
270
|
+
system(ENV['EDITOR'])
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
desc 'exec CASTLE COMMAND',
|
275
|
+
'Execute a single shell command inside the root of a castle'
|
276
|
+
def exec(castle, *args)
|
277
|
+
check_castle_existance castle, 'exec'
|
278
|
+
unless args.count > 0
|
279
|
+
say_status :error,
|
280
|
+
'You must pass a shell command to execute',
|
281
|
+
:red
|
282
|
+
exit(1)
|
283
|
+
end
|
284
|
+
full_command = args.join(' ')
|
285
|
+
say_status "exec '#{full_command}'",
|
286
|
+
"#{options[:pretend] ? 'Would execute' : 'Executing command'} '#{full_command}' in castle '#{castle}'",
|
287
|
+
:green
|
288
|
+
inside repos_dir.join(castle) do
|
289
|
+
system(full_command)
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
desc 'exec_all COMMAND',
|
294
|
+
'Execute a single shell command inside the root of every cloned castle'
|
295
|
+
def exec_all(*args)
|
296
|
+
unless args.count > 0
|
297
|
+
say_status :error,
|
298
|
+
'You must pass a shell command to execute',
|
299
|
+
:red
|
300
|
+
exit(1)
|
301
|
+
end
|
302
|
+
full_command = args.join(' ')
|
303
|
+
inside_each_castle do |castle|
|
304
|
+
say_status "exec '#{full_command}'",
|
305
|
+
"#{options[:pretend] ? 'Would execute' : 'Executing command'} '#{full_command}' in castle '#{castle}'",
|
306
|
+
:green
|
307
|
+
system(full_command)
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
desc 'version', 'Display the current version of homesick'
|
312
|
+
def version
|
313
|
+
say Homesick::Version::STRING
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|