#!/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[ abbrev base64 benchmark bundler cmath csv debug delegate did_you_mean drb english erb fileutils find forwardable getoptlong ipaddr irb logger mutex_m net-http net-protocol observer open3 open-uri optparse ostruct pp prettyprint prime pstore rdoc readline reline resolv resolv-replace rexml rinda rss rubygems scanf securerandom set shellwords singleton tempfile thwait time timeout tmpdir un tsort uri weakref yaml ] DEFAULT_GEM_EXTS = %w[ bigdecimal cgi date digest etc fcntl fiddle io-console io-nonblock io-wait json nkf openssl pathname psych racc readline-ext stringio strscan syslog win32ole 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