homesick 1.0.0 → 1.1.0

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