vim-update-bundles 0.6

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.
Files changed (6) hide show
  1. data/CHANGES +48 -0
  2. data/README.md +147 -0
  3. data/Rakefile +5 -0
  4. data/test.rb +660 -0
  5. data/vim-update-bundles +471 -0
  6. metadata +74 -0
@@ -0,0 +1,471 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Reads bundles to be installed from the .vimrc file then synchronizes
4
+ # .vim/bundles by downloading new repositories as needed. It also removes
5
+ # bundles that are no longer used.
6
+ #
7
+ # Specify a bundle in the .vimrc:
8
+ # " Bundle: https://git.wincent.com/command-t.git
9
+ # Specify a refspec (branch, tag, hash) at the end:
10
+ # " Bundle: https://github.com/vim-ruby/vim-ruby.git noisy
11
+ # " Bundle: https://github.com/bronson/vim-closebuffer.git 0.2
12
+ # " Bundle: https://github.com/tpope/vim-rails.git 42bb0699
13
+ #
14
+ # If the .vim folder is stored in a Git repository, add bundles as submodules
15
+ # by putting "submodule=true" in ~/.vim-update-bundles.conf.
16
+
17
+ require 'fileutils'
18
+ require 'open-uri'
19
+
20
+ Version = '0.6'
21
+
22
+
23
+ def dotvim config, *path
24
+ # Path to files inside the .vim directory, e.g., dotvim/autoload.
25
+ File.join config[:vimdir_path], *path
26
+ end
27
+
28
+
29
+ def ensure_dir dir
30
+ Dir.mkdir dir unless test ?d, dir
31
+ end
32
+
33
+
34
+ def download_file url, file
35
+ open(url) do |r|
36
+ File.open(file, 'w') do |w|
37
+ w.write(r.read)
38
+ end
39
+ end
40
+ end
41
+
42
+
43
+ def run *cmd
44
+ # Runs cmd, returns its stdout, and bails on error.
45
+ # Mostly a backport of Ruby 1.9's IO.popen for 1.8.
46
+ options = { :acceptable_exit_codes => [0] }
47
+ options.merge!(cmd.pop) if cmd.last.kind_of?(Hash)
48
+ puts "-> #{[cmd].join(" ")}" if $verbose
49
+ outr, outw = IO::pipe
50
+ pid = fork {
51
+ outr.close; STDOUT.reopen outw; outw.close
52
+ exec *cmd.flatten.map { |c| c.to_s }
53
+ }
54
+ outw.close
55
+ result = outr.read
56
+ outr.close
57
+ Process.waitpid pid
58
+ unless options[:acceptable_exit_codes].include?($?.exitstatus)
59
+ raise "'#{[cmd].join(" ")}' in #{Dir.pwd} exited with code #{$?.exitstatus}"
60
+ end
61
+ result
62
+ end
63
+
64
+
65
+ def git *cmd
66
+ if !$verbose && %w{checkout clone fetch pull}.include?(cmd.first.to_s)
67
+ cmd.insert 1, '-q'
68
+ end
69
+ run :git, *cmd
70
+ end
71
+
72
+
73
+ def describe_head dir
74
+ Dir.chdir(dir) do
75
+ # Don't want to use 'git describe --all' because branch names change too often.
76
+ # Use `` instead of git() so we don't error out if the git call fails.
77
+ # (might happen if there's a directory in .vim/bundle that isn't git-revisioned).
78
+ version = `git describe --tags 2>/dev/null`.chomp
79
+ version = `git rev-parse HEAD 2>/dev/null`[0..12] unless version =~ /\S/
80
+ version
81
+ end
82
+ end
83
+
84
+
85
+ def current_date
86
+ # Ruby's Time.now.to_s just doesn't produce very good output
87
+ $current_date ||= run(:date).chomp
88
+ end
89
+
90
+
91
+ def print_doc_header doc
92
+ doc.printf "%-34s %s\n\n\n", "*bundles* *bundles.txt*", "Installed Bundles"
93
+ doc.puts "Lists the currently installed bundles. Also see the |bundle-log|."
94
+ doc.puts "Last updated by vim-update-bundles on #{current_date}.\n\n"
95
+ doc.printf " %-32s %-22s %s\n", "PLUGIN", "VERSION", "RELEASE DATE"
96
+ doc.puts "-" * 72
97
+ end
98
+
99
+
100
+ def print_doc_entry dir, doc
101
+ version = describe_head dir
102
+ date = Dir.chdir(dir) { git(:log, '-1', '--pretty=format:%ai').chomp }
103
+ doc.printf " %-32s %-22s %s\n", "|#{dir}|", version, date.split(' ').first
104
+ end
105
+
106
+
107
+ def print_log_header log
108
+ log.printf "%-34s %s\n\n\n", "*bundle-log.txt*", "Bundle Install Log"
109
+ log.puts "Logs bundle install activity. Also see the list of installed |bundles|.\n\n"
110
+ end
111
+
112
+
113
+ def print_log_entry log, action, dir, rev, notes=""
114
+ log.printf " %-3s %-26s %-18s %s\n", action, "|#{dir}|", rev, notes
115
+ end
116
+
117
+
118
+ def ignore_doc_tags
119
+ exclude = File.read ".git/info/exclude"
120
+ if exclude !~ /doc\/tags/
121
+ File.open(".git/info/exclude", "w") { |f|
122
+ f.write exclude.chomp + "\ndoc/tags\n"
123
+ }
124
+ end
125
+ end
126
+
127
+
128
+ def in_git_root inpath=nil
129
+ # Submodules often require the CWD to be the Git root. If a path relative to
130
+ # the CWD is passed, the block receives it relative to the root.
131
+ path = File.join Dir.pwd, inpath if inpath
132
+ Dir.chdir("./" + git('rev-parse', '--show-cdup').chomp) do
133
+ path.sub! /^#{Dir.pwd}\/?/, '' if path
134
+ yield path
135
+ end rescue nil # Git deletes the bundle dir if it's empty.
136
+ end
137
+
138
+
139
+ def clone_bundle config, dir, url, tagstr, log
140
+ unless config[:submodule]
141
+ puts "cloning #{dir} from #{url}#{tagstr}"
142
+ git :clone, url, dir
143
+ else
144
+ puts "adding submodule #{dir} from #{url}#{tagstr}"
145
+ in_git_root(dir) { |mod| git :submodule, :add, url, mod }
146
+ end
147
+ print_log_entry log, 'Add', dir, describe_head(dir), "#{url}#{tagstr}"
148
+ end
149
+
150
+
151
+ def remove_bundle_to config, dir, destination
152
+ puts "Erasing #{dir}, find it in #{destination}"
153
+ FileUtils.mv dir, destination
154
+ if config[:submodule]
155
+ in_git_root(dir) do |mod|
156
+ git :rm, mod
157
+ ['.gitmodules', '.git/config'].each do |filename|
158
+ begin
159
+ text = File.read filename
160
+ File.open(filename, 'w+') do |file|
161
+ file.puts text.gsub(/\[submodule "#{mod}"\][^\[]+/m,'')
162
+ end
163
+ rescue
164
+ puts " Could not delete submodule entries from .gitmodules and .git/config"
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
170
+
171
+
172
+ def remove_bundle config, dir, log
173
+ print_log_entry log, 'Del', dir, describe_head(dir)
174
+ trash_dir = dotvim(config, "Trashed-Bundles")
175
+ ensure_dir trash_dir
176
+ 1.upto(100) do |i|
177
+ destination = "#{trash_dir}/#{dir}-#{'%02d' % i}"
178
+ unless test ?d, destination
179
+ remove_bundle_to config, dir, destination
180
+ return
181
+ end
182
+ end
183
+ raise "unable to remove #{dir}, please delete #{trash_dir}"
184
+ end
185
+
186
+
187
+ def update_bundle config, dir, tag, tagstr, previous_version, log
188
+ Dir.chdir(dir) do
189
+ git :checkout, tag || :master
190
+ if system 'git symbolic-ref HEAD -q >/dev/null'
191
+ # only pull if symbolic-ref is true, otherwise it's detached head
192
+ git :pull, '--ff-only', :origin, tag || :master
193
+ end
194
+ ignore_doc_tags
195
+ end
196
+
197
+ if previous_version
198
+ new_version = describe_head dir
199
+ if new_version != previous_version
200
+ print_log_entry log, 'up', dir, previous_version, "-> #{new_version}#{tagstr}"
201
+ end
202
+ end
203
+
204
+ in_git_root(dir) { |mod| git :add, mod } if config[:submodule]
205
+ end
206
+
207
+
208
+ def download_bundle config, dir, url, tag, doc, log
209
+ tagstr = " at #{tag}" if tag
210
+ previous_version = nil
211
+ only_updating = false
212
+
213
+ # fetch bundle
214
+ if test ?d, dir
215
+ remote = Dir.chdir(dir) { git(:config, '--get', 'remote.origin.url').chomp }
216
+ if remote == url
217
+ only_updating = true
218
+ unless config[:no_updates]
219
+ previous_version = describe_head dir
220
+ puts "updating #{dir} from #{url}#{tagstr}"
221
+ Dir.chdir(dir) { git :fetch }
222
+ end
223
+ else
224
+ puts "repo has changed from #{remote} to #{url}"
225
+ remove_bundle config, dir, log
226
+ ensure_dir dotvim(config, 'bundle')
227
+ clone_bundle config, dir, url, tagstr, log
228
+ end
229
+ else
230
+ clone_bundle config, dir, url, tagstr, log
231
+ end
232
+
233
+ # pull bundle
234
+ unless only_updating && config[:no_updates]
235
+ update_bundle config, dir, tag, tagstr, previous_version, log
236
+ end
237
+
238
+ print_doc_entry dir, doc
239
+ only_updating
240
+ end
241
+
242
+
243
+ def read_vimrc config
244
+ File.open(config[:vimrc_path]) do |file|
245
+ file.each_line { |line| yield line }
246
+ end
247
+ end
248
+
249
+
250
+ class BundleCommandError < RuntimeError
251
+ def exit_code; 47; end
252
+ end
253
+
254
+ def run_bundle_command dir, cmd
255
+ puts " running: #{cmd}"
256
+ status = Dir.chdir(dir) { system(cmd); $? }
257
+ unless status.success?
258
+ raise BundleCommandError.new("BundleCommand #{cmd} in #{Dir.pwd} failed!")
259
+ end
260
+ end
261
+
262
+
263
+ def update_bundles config, doc, log
264
+ existing_bundles = Dir['*']
265
+
266
+ # Ignore files in the bundle directory, e.g., READMEs.
267
+ existing_bundles.reject! { |path| FileTest.file? path }
268
+
269
+ dir = only_updating = nil
270
+ puts "# reading vimrc" if config[:verbose]
271
+ read_vimrc(config) do |line|
272
+ if line =~ /^\s*"\s*bundle:\s*(.*)$/i
273
+ url, tag = $1.split
274
+ puts "# processing '#{url}' at '#{tag}'" if config[:verbose]
275
+ dir = url.split('/').last.gsub(/^vim-|\.git$/, '')
276
+ if url.match /^[A-Za-z0-9-]+\/[A-Za-z0-9._-]+$/ # User/repository.
277
+ url = "https://github.com/#{url}.git"
278
+ end
279
+ if url.match /^[A-Za-z0-9._-]+$/ # Plain repository.
280
+ url = "https://github.com/vim-scripts/#{url}.git"
281
+ end
282
+ only_updating = download_bundle config, dir, url, tag, doc, log
283
+ existing_bundles.delete dir
284
+ elsif line =~ /^\s*"\s*bundle[ -]?command:\s*(.*)$/i
285
+ # Want BundleCommand but BUNDLE COMMAND and Bundle-Command used to be legal too
286
+ raise "BundleCommand must come after Bundle" if dir.nil?
287
+ run_bundle_command dir, $1 unless only_updating && config[:no_updates]
288
+ elsif line =~ /^\s*"\s*static:\s*(.*)$/i
289
+ dir = $1
290
+ puts " leaving #{dir} alone"
291
+ existing_bundles.delete dir
292
+ end
293
+ end
294
+ existing_bundles.each { |dir| remove_bundle config, dir, log }
295
+
296
+ if config[:submodule]
297
+ in_git_root do
298
+ puts " updating submodules"
299
+ git :submodule, :init
300
+ git :submodule, :update
301
+ end
302
+ end
303
+ end
304
+
305
+
306
+ def update_bundles_and_docs config
307
+ ensure_dir dotvim(config, 'doc')
308
+ bundle_dir = dotvim(config, 'bundle')
309
+ ensure_dir bundle_dir
310
+
311
+ File.open(dotvim(config, 'doc', 'bundles.txt'), "w") do |doc|
312
+ print_doc_header doc
313
+ logfile = dotvim(config, 'doc', 'bundle-log.txt')
314
+ log_already_exists = test ?f, logfile
315
+ File.open(logfile, 'a') do |log|
316
+ print_log_header log unless log_already_exists
317
+ log.puts "Updating on #{current_date}"
318
+ begin
319
+ Dir.chdir(bundle_dir) { update_bundles config, doc, log }
320
+ rescue Exception => e
321
+ message = e.is_a?(Interrupt) ? "Interrupted" : "Aborted: #{e.message}"
322
+ log.print " #{message}\n\n" # puts suppresses trailing newline
323
+ doc.puts message
324
+ STDERR.puts message
325
+ exit e.respond_to?(:exit_code) ? e.exit_code : 1
326
+ end
327
+ log.puts
328
+ end
329
+ doc.puts
330
+ end
331
+ end
332
+
333
+
334
+ def interpolate options, val, message, i
335
+ raise "Interpolation is now $#{$1} instead of ENV[#{$1}] #{message} #{i}" if val =~ /ENV\[['"]?([^\]]*)['"]?\]/
336
+ STDERR.puts "WARNING: quotes in a config item are probably a mistake #{message} #{i}" if val =~ /["']/
337
+
338
+ val.gsub(/\$([A-Za-z0-9_]+)/) { options[$1.to_sym] || ENV[$1] || raise("$#{$1} is not defined #{message} #{i}") }
339
+ end
340
+
341
+
342
+ def process_options options, args, message
343
+ args.each_with_index do |arg,i|
344
+ arg = arg.gsub /^\s*-?-?|\s*$/, '' # Leading dashes in front of options are optional.
345
+ return if arg == '' || arg =~ /^#/
346
+
347
+ k,v = arg.split /\s*=\s*/, 2
348
+ k = options[k.to_sym].to_s while options[k.to_sym].is_a? Symbol # expand 1-letter options, :v -> :verbose
349
+ k.gsub! '-', '_' # underscorize args, 'no-updates' -> 'no_updates'
350
+
351
+ unless options.has_key? k.to_sym
352
+ puts "Unknown option: #{k.inspect} #{message} #{i}"
353
+ puts "Usage: #{help}" if args.equal? ARGV
354
+ exit 1
355
+ end
356
+
357
+ v = options[k.to_sym].call(v) if options[k.to_sym].is_a? Proc
358
+ options[k.to_sym] = v ? interpolate(options,v,message,i).split("'").join("\\'") : true
359
+ end
360
+ end
361
+
362
+
363
+ # Returns the first path that exists or the last one if nothing exists.
364
+ def choose_file *paths
365
+ paths.find { |p| test ?f, p } || paths[-1]
366
+ end
367
+
368
+
369
+ def set_default_options opts
370
+ dotfiles = File.join(ENV['HOME'], '.dotfiles')
371
+ opts[:dotfiles_path] ||= dotfiles if test(?d, dotfiles)
372
+
373
+ if opts[:dotfiles_path]
374
+ raise "#{opts[:dotfiles_path]} doesn't exist!" unless test(?d, opts[:dotfiles_path])
375
+ opts[:vimdir_path] ||= File.join(opts[:dotfiles_path], 'vim')
376
+ opts[:vimrc_path] ||= choose_file(File.join([opts[:dotfiles_path], 'vim', 'vimrc']),
377
+ File.join([opts[:dotfiles_path], 'vimrc']))
378
+ else
379
+ opts[:vimdir_path] ||= File.join(ENV['HOME'], '.vim')
380
+ opts[:vimrc_path] ||= choose_file(File.join([ENV['HOME'], '.vim', 'vimrc']),
381
+ File.join([ENV['HOME'], '.vimrc']))
382
+ end
383
+ end
384
+
385
+
386
+ def ensure_vim_environment config
387
+ ensure_dir dotvim(config)
388
+ ensure_dir dotvim(config, 'autoload')
389
+
390
+ unless test ?f, dotvim(config, 'autoload', 'pathogen.vim')
391
+ puts "Downloading Pathogen..."
392
+ download_file config[:pathogen_url], dotvim(config, 'autoload', 'pathogen.vim')
393
+ end
394
+
395
+ unless test(?f, config[:vimrc_path])
396
+ puts "Downloading starter vimrc..."
397
+ download_file config[:starter_url], config[:vimrc_path]
398
+ end
399
+
400
+ run :ln, '-s', config[:vimdir_path], "#{ENV['HOME']}/.vim" unless test ?e, "#{ENV['HOME']}/.vim"
401
+ run :ln, '-s', config[:vimrc_path], "#{ENV['HOME']}/.vimrc" unless test ?e, "#{ENV['HOME']}/.vimrc"
402
+ end
403
+
404
+
405
+ def generate_helptags
406
+ puts "updating helptags..."
407
+ # Vim on a Mac often exits with 1, even when doing nothing.
408
+ run :vim, '-e', '-c', 'call pathogen#helptags()', '-c', 'q', :acceptable_exit_codes => [0, 1] unless ENV['TESTING']
409
+ end
410
+
411
+
412
+ def read_configuration config
413
+ conf_file = File.join ENV['HOME'], '.vim-update-bundles.conf'
414
+ process_options config, File.open(conf_file).readlines, "in #{conf_file} line" if test(?f, conf_file)
415
+ process_options config, ARGV, "in command line argument"
416
+
417
+ set_default_options config
418
+
419
+ config.keys.sort.each { |k| puts "# option #{k} = #{config[k].inspect}" } if config[:verbose]
420
+ config.delete :dotfiles_path # Ensure it is not used accidentally later.
421
+ end
422
+
423
+
424
+ def help
425
+ <<EOL
426
+ vim-update-bundles [options...]
427
+ Updates the installed Vim plugins.
428
+ -n --no-updates: don't update bundles, only add or delete (faster)
429
+ -h -? --help: print this message
430
+ -v --verbose: print exactly what's happening
431
+ optional configurations:
432
+ -s --submodule: store bundles as git submodules
433
+ --vimdir-path: path to ~/.vim directory
434
+ --vimrc-path: path to ~/.vimrc directory
435
+ --dotfiles-path: path to your dotfiles if different than $HOME.
436
+ EOL
437
+ end
438
+
439
+
440
+ config = {
441
+ :verbose => nil, # Git commands are quiet by default; set verbose=true to see everything.
442
+ :submodule => false, # If true then use Git submodules instead of cloning.
443
+ :no_updates => false, # If true then don't update repos, only add or delete.
444
+
445
+ :help => lambda { |v| puts help; exit },
446
+ :version => lambda { |v| puts "vim-update-bundles #{Version}"; exit },
447
+
448
+ # single-character aliases for command-line options
449
+ :v => :verbose, :s => :submodule, :n => :no_updates,
450
+ :h => :help, :'?' => :help, :V => :version,
451
+
452
+ :dotfiles_path => nil, # Full path to the dot files directory.
453
+ :vimdir_path => nil, # Full path to ~/.vim (creates symlink if not in $HOME).
454
+ :vimrc_path => nil, # Full path to ~/.vimrc (creates symlink if not in $HOME).
455
+
456
+ # Used when spinning up a new Vim environment.
457
+ :starter_url => "https://github.com/bronson/dotfiles/raw/master/.vimrc",
458
+ :pathogen_url => "https://github.com/tpope/vim-pathogen/raw/master/autoload/pathogen.vim",
459
+ }
460
+
461
+
462
+ unless $load_only # to read the version number
463
+ read_configuration config
464
+ $verbose = config[:verbose]
465
+
466
+ ensure_vim_environment config
467
+ update_bundles_and_docs config
468
+ generate_helptags
469
+ puts "done! Start Vim and type ':help bundles' to see what has been installed."
470
+ end
471
+