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.
@@ -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
@@ -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