-
Notifications
You must be signed in to change notification settings - Fork 13
/
Copy pathvim-update-bundles
executable file
·545 lines (441 loc) · 16.3 KB
/
vim-update-bundles
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
#!/usr/bin/env ruby
# Reads bundles to be installed from the .vimrc file then synchronizes
# .vim/bundles by downloading new repositories as needed. It also removes
# bundles that are no longer used.
# This software is covered by the MIT License.
#
# TODO: add a --trace option, get rid of TRACE=1, and print note in rescue
# TODO: read .gvimrc too
# TODO: update on launch so removing Bundle directive takes effect immediatley
# TODO: simpler usecase? https://github.com/taq/vim-bundles-updater
# TODO: nice: https://github.com/edkolev/poor-mans-plugin-downloader.vim
# TODO: how does YCM automatically call git init submodules? automatic vundle thing?
require 'fileutils'
require 'open-uri'
Version = '0.8.3'
def ensure_dir dir
Dir.mkdir dir unless test ?d, dir
end
def download_file url, file
open(url) do |r|
File.open(file, 'w') do |w|
w.write(r.read)
end
end
end
def run *cmd
# Runs cmd, returns its stdout, and bails on error.
# Mostly a backport of Ruby 1.9's IO.popen for 1.8.
options = { :acceptable_exit_codes => [0], :stderr => nil }
options.merge!(cmd.pop) if cmd.last.kind_of?(Hash)
puts "-> #{[cmd].join(" ")}" if $verbose
outr, outw = IO::pipe
pid = fork {
outr.close; STDOUT.reopen outw; outw.close
STDERR.reopen '/dev/null', 'w' if options[:stderr] == :suppress
STDERR.reopen STDOUT if options[:stderr] == :merge
exec *cmd.flatten.map { |c| c.to_s }
}
outw.close
result = outr.read
outr.close
Process.waitpid pid
if options[:acceptable_exit_codes] != :any && !options[:acceptable_exit_codes].include?($?.exitstatus)
raise "'#{[cmd].join(" ")}' in #{Dir.pwd} exited with code #{$?.exitstatus}"
end
puts "RESULT #{$?.exitstatus}: <<#{result}>>" if $verbose && $verbose.to_i >= 2
result
end
def git *cmd
# submodule is so 'submodule add' doesn't print 'Cloning into bundle/repo'
if !$verbose && %w{checkout clone fetch pull submodule}.include?(cmd.first.to_s)
cmd.insert 1, '-q'
end
run :git, *cmd
end
def describe_head
# Don't want to use 'git describe --all' because branch names change too often.
# This will fail if there's a directory in .vim/bundle that isn't git-revisioned.
version = git(:describe, '--tags', :acceptable_exit_codes => :any, :stderr => :suppress).chomp
version = git('rev-parse', 'HEAD', :acceptable_exit_codes => :any, :stderr => :suppress)[0..12] unless version =~ /\S/
version
end
def current_date
# Ruby's Time.now.to_s just doesn't produce very good output
$current_date ||= run(:date).chomp
end
def print_doc_header doc
doc.printf "%-34s %s\n\n\n", "*bundles* *bundles.txt*", "Installed Bundles"
doc.puts "Lists the currently installed bundles. Also see the |bundle-log|."
doc.puts "Last updated by vim-update-bundles on #{current_date}.\n\n"
doc.printf " %-32s %-22s %s\n", "PLUGIN", "VERSION", "RELEASE DATE"
doc.puts "-" * 72
end
def print_doc_entry dir, doc
version = describe_head
date = git(:log, '-1', '--pretty=format:%ai').chomp
doc.printf " %-32s %-22s %s\n", "|#{dir}|", version, date.split(' ').first
end
def print_log_header log
log.printf "%-34s %s\n\n\n", "*bundle-log.txt*", "Bundle Install Log"
log.puts "Logs bundle install activity. Also see the list of installed |bundles|.\n\n"
end
def print_log_entry log, action, dir, rev, notes=""
message = " %-3s %-26s %-18s %s" % [action, "|#{dir}|", rev, notes]
log.puts message.sub /\s+$/, ''
end
def log_error log, message
log.print " #{message}\n\n" # puts suppresses trailing newline
STDERR.puts message
end
def write_exclude_file contents=""
if contents !~ /doc\/tags/
File.open(".git/info/exclude", "w") { |f|
f.write(contents.chomp+"\n\n") if contents
f.write "# added by vim-update-bundles\ndoc/tags\n"
}
end
end
def ignore_doc_tags
if test(?f, ".git/info/exclude")
write_exclude_file File.read(".git/info/exclude")
elsif test(?d, ".git")
ensure_dir ".git/info"
write_exclude_file
else
# it's a submodule, no need
end
end
# Work around Ruby's useless "conflicting chdir during another chdir block" warning
# A better_chdir block can contain a Dir.chdir block,
# but a Dir.chdir block can't contain better_chdir.
def better_chdir dir
orig = Dir.pwd
begin
Dir.chdir dir
yield
ensure
# If the last bundle is removed, git will remove ~/.vim/bundle too.
ensure_dir orig
Dir.chdir orig
end
end
# If running in a submodule environment, chdirs to the submodule root
# and then calls the block with inpath converted to be relative to the
# subodule root. If not running with submodules, returns false.
def in_submodule_root inpath=nil
path = File.join Dir.pwd, inpath if inpath
parent = git 'rev-parse', '--show-cdup', :acceptable_exit_codes => :any, :stderr => :suppress
if $?.exitstatus == 0
better_chdir("./" + parent.chomp) do
path.sub! /^#{Dir.pwd}\/?/, '' if path
yield path
end
return true
end
return false
end
def clone_bundle config, dir, url, tagstr, log
if in_submodule_root(dir) { |mod|
puts "adding submodule #{dir} from #{url}#{tagstr}"
git :submodule, :add, url, mod
}
else
puts "cloning #{dir} from #{url}#{tagstr}"
git :clone, url, dir
end
Dir.chdir(dir) { print_log_entry log, 'Add', dir, describe_head, "#{url}#{tagstr}" }
$bundles_added += 1
end
def remove_bundle_to config, dir, destination
puts "Removing #{dir}, find it in #{destination}"
FileUtils.mv dir, destination
in_submodule_root(dir) do |mod|
git :rm, mod
fn = nil
['.gitmodules', '.git/config'].each do |filename|
begin
fn = filename
text = File.read filename
File.open(filename, 'w+') do |file|
file.puts text.gsub(/\[submodule "#{mod}"\][^\[]+/m,'')
end
rescue Exception => e
raise "could not delete #{dir} from #{fn}: #{e}"
end
end
end
end
def remove_bundle config, dir, log
Dir.chdir(dir) { print_log_entry log, 'Del', dir, describe_head }
trash_dir = "#{config[:vimdir_path]}/Trashed-Bundles"
ensure_dir trash_dir
suffixes = [''] + (1..99).map { |i| "-#{"%02d" % i}" }
suffixes.each do |suffix|
destination = "#{trash_dir}/#{dir}#{suffix}"
unless test ?d, destination
remove_bundle_to config, dir, destination
$bundles_removed += 1
return
end
end
raise "unable to remove #{dir}, please delete #{trash_dir}"
end
def reset_bundle config, dir, url, tagstr, log
remove_bundle config, dir, log
ensure_dir "#{config[:vimdir_path]}/bundle"
clone_bundle config, dir, url, tagstr, log
end
def pull_bundle dir, tag, log
git :fetch, :origin, :stderr => :suppress # git prints some needless warnings during fetch
git :checkout, tag || :master
# if it's a branch, we need to merge in upstream changes
if system 'git symbolic-ref HEAD -q >/dev/null'
output = git(:merge, '--ff-only', "origin/#{tag || :master}", :acceptable_exit_codes => :any, :stderr => :merge)
unless $?.success?
log_error log, output.gsub(/\s+/, ' ')
return false # bundle is not OK and needs to be reset
end
end
return true # bundle is good, let's continue
end
def install_bundle config, dir, url, tag, doc, log
tagstr = " at #{tag}" if tag
previous_version = nil
only_updating = false
if url.match /^[A-Za-z0-9-]+\/[A-Za-z0-9._-]+$/ # User/repository.
url = "https://github.com/#{url}.git"
end
if url.match /^[A-Za-z0-9._-]+$/ # Plain repository.
url = "https://github.com/vim-scripts/#{url}.git"
end
# fetch bundle
if test ?d, dir
remote = Dir.chdir(dir) { git(:config, '--get', 'remote.origin.url').chomp }
if remote == url
only_updating = true
unless config[:no_updates]
Dir.chdir(dir) { previous_version = describe_head }
puts "updating #{dir} from #{url}#{tagstr}"
end
else
log_error log, "bundle for #{dir} changed from #{remote} to #{url}"
reset_bundle config, dir, url, tagstr, log
end
else
clone_bundle config, dir, url, tagstr, log
end
# pull bundle
unless only_updating && config[:no_updates]
unless Dir.chdir(dir) { pull_bundle dir, tag, log }
reset_bundle config, dir, url, tagstr, log
end
Dir.chdir(dir) do
ignore_doc_tags
if previous_version
new_version = describe_head
if new_version != previous_version
print_log_entry log, 'up', dir, "#{new_version}#{tagstr}", "<- #{previous_version}"
$bundles_updated += 1 if only_updating
end
end
end
end
Dir.chdir(dir) { print_doc_entry dir, doc }
in_submodule_root(dir) { |mod| git :add, mod }
end
def read_vimrc config
File.open(config[:vimrc_path]) do |file|
file.each_line { |line| yield line }
end
end
class BundleCommandError < RuntimeError
def exit_code; 47; end
end
def run_bundle_command dir, cmd
puts "BundleCommand: #{cmd}"
status = Dir.chdir(dir) { system(cmd); $? }
unless status.success?
raise BundleCommandError.new("BundleCommand #{cmd} in #{Dir.pwd} failed!")
end
end
def vim_string str
# quick & dirty, parse a single or double quoted string the way Vim would
if str.slice(0,1) == "'"
str =~ /^\s*'(.*)'\s*$/
return $1.gsub "''", "'"
elsif str.slice(0,1) == '"'
str =~ /^\s*"(.*)"\s*$/
return $1 # could do escape substitution here
else
return str
end
end
def update_bundles config, doc, log
existing_bundles = Dir['*']
updated_bundles = {}
# Ignore files in the bundle directory, e.g., READMEs.
existing_bundles.reject! { |path| FileTest.file? path }
puts "# reading vimrc" if config[:verbose]
string_re = %q{'([^']+|'')*'|"[^"]*"} # captures single and double quoted Vim strings
read_vimrc(config) do |line|
if line =~ /^\s*"\s*bundle:\s*(.*)$/i ||
line =~ /^\s*Bundle\s*(#{string_re})/
url, tag = vim_string($1).split
puts "# processing '#{url}' at '#{tag}'" if config[:verbose]
dir = url.split('/').last.sub(/\.git$/, '')
# quick sanity check
raise "duplicate entry for #{url}" if updated_bundles[dir] == url
raise "urls map to the same bundle: #{updated_bundles[dir]} and #{url}" if updated_bundles[dir]
install_bundle config, dir, url, tag, doc, log
updated_bundles[dir] = url
existing_bundles.delete dir
elsif line =~ /^\s*"\s*bundle[ -]?command:\s*(.*)$/i ||
line =~ /^\s*BundleCommand\s*(#{string_re})$/
# Want BundleCommand but BUNDLE COMMAND and Bundle-Command used to be legal too
run_bundle_command "#{config[:vimdir_path]}/bundle", vim_string($1)
elsif line =~ /^\s*"\s*static:\s*(.*)$/i ||
line =~ /^\s*Bundle!\s*(#{string_re})$/i
dir = vim_string $1
puts " leaving #{dir} alone"
existing_bundles.delete dir
end
end
existing_bundles.each { |dir| remove_bundle config, dir, log }
in_submodule_root do
puts " updating submodules"
git :submodule, :init
git :submodule, :update
end
end
def bundleize count
"#{count} bundle#{count != 1 ? 's' : ''}"
end
def bundle_count_string
str = []
str << "#{bundleize $bundles_added} added" if $bundles_added > 0
str << "#{bundleize $bundles_removed} removed" if $bundles_removed > 0
str << "#{bundleize $bundles_updated} updated" if $bundles_updated > 0
return "no updates" if str.empty?
str[-1] = "and #{str[-1]}" if str.size > 2
str.join(", ")
end
def update_bundles_and_docs config
ensure_dir "#{config[:vimdir_path]}/doc"
bundle_dir = "#{config[:vimdir_path]}/bundle"
ensure_dir bundle_dir
$bundles_added = $bundles_removed = $bundles_updated = 0
File.open("#{config[:vimdir_path]}/doc/bundles.txt", "w") do |doc|
print_doc_header doc
logfile = "#{config[:vimdir_path]}/doc/bundle-log.txt"
log_already_exists = test ?f, logfile
File.open(logfile, 'a') do |log|
print_log_header log unless log_already_exists
log.puts "#{current_date} by vim-update-bundles #{Version}"
begin
better_chdir(bundle_dir) { update_bundles config, doc, log }
rescue Exception => e
message = e.is_a?(Interrupt) ? "Interrupted" : "Aborted: #{e.message}"
log_error log, message
doc.puts message
STDERR.puts e.backtrace if ENV['TRACE']
exit e.respond_to?(:exit_code) ? e.exit_code : 1
end
log.puts " " + bundle_count_string
log.puts
end
doc.puts
end
end
def interpolate options, val, message, i
raise "Interpolation is now $#{$1} instead of ENV[#{$1}] #{message} #{i}" if val =~ /ENV\[['"]?([^\]]*)['"]?\]/
STDERR.puts "WARNING: putting quotes in a config item is probably a mistake #{message} #{i}" if val =~ /["']/
val.gsub(/\$([A-Za-z0-9_]+)/) { options[$1.to_sym] || ENV[$1] || raise("$#{$1} is not defined #{message} #{i}") }
end
def process_options options, args, message
args.each_with_index do |arg,i|
arg = arg.gsub /^\s*-?-?|\s*$/, '' # Leading dashes in front of options are optional.
return if arg == '' || arg =~ /^#/
k,v = arg.split /\s*=\s*/, 2
k = options[k.to_sym].to_s while options[k.to_sym].is_a? Symbol # expand 1-letter options, :v -> :verbose
k.gsub! '-', '_' # underscorize args, 'no-updates' -> 'no_updates'
unless options.has_key? k.to_sym
STDERR.puts "Unknown option: #{k.inspect} #{message} #{i}"
puts "Usage: #{help}" if args.equal? ARGV
exit 1
end
v = options[k.to_sym].call(v) if options[k.to_sym].is_a? Proc
options[k.to_sym] = v ? interpolate(options,v,message,i).split("'").join("\\'") : 1 + (options[k.to_sym] || 0)
end
end
# Returns the first path that exists or the last one if nothing exists.
def choose_path *paths
paths.find { |p| test ?e, p } || paths[-1]
end
def generate_helptags
puts "updating helptags..."
# Vim on a Mac often exits with 1, even when doing nothing.
run :vim, '-e', '-c', 'call pathogen#helptags()', '-c', 'q', :acceptable_exit_codes => [0, 1] unless ENV['TESTING']
end
def locate_vim_files config
vimdir_guess = choose_path "#{ENV['HOME']}/.dotfiles/vim", "#{ENV['HOME']}/.vim"
vimrc_guesses = []
if config[:vimdir_path]
vimrc_guesses.push "#{config[:vimdir_path]}/.vimrc", "#{config[:vimdir_path]}/vimrc"
end
vimrc_guesses.push "#{ENV['HOME']}/.dotfiles/vimrc", "#{ENV['HOME']}/.vimrc"
vimrc_guess = choose_path *vimrc_guesses
config[:vimdir_path] ||= vimdir_guess
config[:vimrc_path] ||= vimrc_guess
end
def read_configuration config
process_options config, ARGV, "in command line argument"
locate_vim_files config
actual_keys = config.keys.reject { |k| config[k].is_a? Proc or config[k].is_a? Symbol }
actual_keys.map { |k| k.to_s }.sort.each do |k|
puts "# option #{k} = #{config[k.to_sym].inspect}"
end if config[:verbose]
end
def help
<<EOL
vim-update-bundles [options...]
Updates the installed Vim plugins.
-n --no-updates: don't update bundles, only add or delete (faster)
-h -? --help: print this message
-v --verbose: print what's happening (multiple -v for more verbose)
optional configurations:
--vimdir-path: path to ~/.vim directory
--vimrc-path: path to ~/.vimrc directory
EOL
end
config = {
:verbose => nil, # Git commands are quiet by default; set verbose=true to see everything.
:no_updates => false, # If true then don't update repos, only add or delete.
:help => lambda { |v| puts help; exit },
:version => lambda { |v| puts "vim-update-bundles #{Version}"; exit },
# single-character aliases for command-line options
:v => :verbose, :n => :no_updates,
:h => :help, :'?' => :help, :V => :version,
:vimdir_path => nil, # Full path to ~/.vim
:vimrc_path => nil, # Full path to ~/.vimrc
# Used when spinning up a new Vim environment.
:starter_url => "https://github.com/bronson/dotfiles/raw/master/.vimrc",
}
unless $load_only # to read the version number
read_configuration config
$verbose = config[:verbose]
# warning for now, remove after a few releases
if test ?f, "#{config[:vimdir_path]}/autoload/pathogen.vim"
puts "Please remove #{config[:vimdir_path]}/autoload/pathogen.vim."
puts "See 'Runtime Path' in the vim-update-bundles README for why."
exit 1
end
ensure_dir config[:vimdir_path]
unless test(?f, config[:vimrc_path])
puts "Downloading starter vimrc..."
download_file config[:starter_url], config[:vimrc_path]
end
update_bundles_and_docs config
generate_helptags
puts "done! Start Vim and type ':help bundles' to see what has been installed."
end