homesick 1.0.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|