diff options
Diffstat (limited to 'misc/expand_tabs.rb')
| -rwxr-xr-x | misc/expand_tabs.rb | 178 |
1 files changed, 178 insertions, 0 deletions
diff --git a/misc/expand_tabs.rb b/misc/expand_tabs.rb new file mode 100755 index 0000000000..d26568eefc --- /dev/null +++ b/misc/expand_tabs.rb @@ -0,0 +1,178 @@ +#!/usr/bin/env ruby --disable-gems +# Add the following line to your `.git/hooks/pre-commit`: +# +# $ ruby --disable-gems misc/expand_tabs.rb +# + +require 'shellwords' +require 'tmpdir' +ENV['LC_ALL'] = 'C' + +class Git + def initialize(oldrev, newrev) + @oldrev = oldrev + @newrev = newrev + end + + # ["foo/bar.c", "baz.h", ...] + def updated_paths + with_clean_env do + IO.popen(['git', 'diff', '--cached', '--name-only', @newrev], &:readlines).each(&:chomp!) + end + end + + # [0, 1, 4, ...] + def updated_lines(file) + lines = [] + revs_pattern = ("0"*40) + " " + with_clean_env { IO.popen(['git', 'blame', '-l', '--', file], &:readlines) }.each_with_index do |line, index| + if line.b.start_with?(revs_pattern) + lines << index + end + end + lines + end + + def add(file) + git('add', file) + end + + def toplevel + IO.popen(['git', 'rev-parse', '--show-toplevel'], &:read).chomp + end + + private + + def git(*args) + cmd = ['git', *args].shelljoin + unless with_clean_env { system(cmd) } + abort "Failed to run: #{cmd}" + end + end + + def with_clean_env + git_dir = ENV.delete('GIT_DIR') # this overcomes '-C' or pwd + yield + ensure + ENV['GIT_DIR'] = git_dir if git_dir + end +end + +DEFAULT_GEM_LIBS = %w[ + bundler + delegate + did_you_mean + english + erb + error_highlight + fileutils + find + forwardable + ipaddr + net-http + net-protocol + open3 + open-uri + optparse + ostruct + pp + prettyprint + prism + resolv + rubygems + securerandom + shellwords + singleton + tempfile + time + timeout + tmpdir + un + tsort + uri + weakref + yaml +] + +DEFAULT_GEM_EXTS = %w[ + date + digest + etc + fcntl + io-console + io-nonblock + io-wait + json + openssl + pathname + psych + stringio + strscan + zlib +] + +EXPANDTAB_IGNORED_FILES = [ + # default gems whose master is GitHub + %r{\Abin/(?!erb)\w+\z}, + *DEFAULT_GEM_LIBS.flat_map { |lib| + [ + %r{\Alib/#{lib}/}, + %r{\Alib/#{lib}\.gemspec\z}, + %r{\Alib/#{lib}\.rb\z}, + %r{\Atest/#{lib}/}, + ] + }, + *DEFAULT_GEM_EXTS.flat_map { |ext| + [ + %r{\Aext/#{ext}/}, + %r{\Atest/#{ext}/}, + ] + }, + + # vendoring (ccan) + %r{\Accan/}, + + # vendoring (onigmo) + %r{\Aenc/}, + %r{\Ainclude/ruby/onigmo\.h\z}, + %r{\Areg.+\.(c|h)\z}, + + # explicit or implicit `c-file-style: "linux"` + %r{\Aaddr2line\.c\z}, + %r{\Amissing/}, + %r{\Astrftime\.c\z}, + %r{\Avsnprintf\.c\z}, +] + +git = Git.new('HEAD^', 'HEAD') + +Dir.chdir(git.toplevel) do + paths = git.updated_paths + paths.select! {|f| + (f.end_with?('.c') || f.end_with?('.h') || f == 'insns.def') && EXPANDTAB_IGNORED_FILES.all? { |re| !f.match(re) } + } + files = paths.select {|n| File.file?(n)} + exit if files.empty? + + files.each do |f| + src = File.binread(f) rescue next + + expanded = false + updated_lines = git.updated_lines(f) + unless updated_lines.empty? + src.gsub!(/^.*$/).with_index do |line, lineno| + if updated_lines.include?(lineno) && line.start_with?("\t") # last-committed line with hard tabs + expanded = true + line.sub(/\A\t+/) { |tabs| ' ' * (8 * tabs.length) } + else + line + end + end + end + + if expanded + File.binwrite(f, src) + git.add(f) + end + end +end |
