#!/usr/bin/env ruby # encoding: UTF-8 require 'optparse' class File def self.binary?(name) fstat = stat(name) return false unless fstat.file? open(name) do |file| blk = file.read(fstat.blksize) return blk && ( blk.size == 0 || blk.count("^ -~", "^\r\n") / blk.size > 0.3 || blk.count("\x00") > 0 ) end end end options = {} OptionParser.new do |opts| opts.banner = "Usage: git-rank-contributors [path] [options]" opts.on("--file-types [TYPES]", "Comma separated list of file types") do |v| options[:file_types] = v.split(",") end opts.on("--count-blank", "Count blank lines") do |v| options[:count_blank] = v end opts.on("--count-comments", "Count comments") do |v| options[:count_comments] = v end opts.on("--debug", "Show debugging messages") do |v| options[:debug] = v end opts.on("--ignore [REGEXP]", "RegExp of files to ignore") do |v| options[:ignore] = Regexp.new(v) end end.parse! dir = ARGV[0] || "." COMMENTS = { '.js' => { :oneline => %r{^(//.*|.\*.*\*/)$}, :start => %r{^/\*}, :end => %r{\*/} }, '.css' => { :oneline => %r{^/\*.*\*/$}, :start => %r{^/\*}, :end => %r{\*/} } } cols = `tput cols` cols = (cols =~ /^\d+$/) ? cols.to_i : 75 files = `git ls-files #{dir}`.split("\n").map(&:strip) files.reject!{|f| File.directory?(f) } # submodules files.reject!{|f| !options[:file_types].include?(File.extname(f)[1..-1]) } if options[:file_types] files.reject!{|f| f =~ options[:ignore] } if options[:ignore] files.reject!{|f| File.binary?(f) } puts files.join("\n") if options[:debug] contributors = {} total_lines = 0 puts "Processing..." files.each do |file| file_disp = file.length < cols-3 ? file : file[0..(cols-3)]+"..." print "#{file_disp.ljust(cols)}\r" $stdout.flush ext = File.extname(file) file_lines = 0 is_comment = false File.read(file).each do |l| l.strip! next if l.empty? && !options[:count_blank] if !options[:count_comments] && regexp = COMMENTS[ext] if is_comment is_comment = false if l =~ regexp[:end] next else next if l =~ regexp[:oneline] if l =~ regexp[:start] && l !~ regexp[:end] is_comment = true next end end end file_lines += 1 end total_lines += file_lines commits = {} is_comment = false next_is_comment = false enum = `git blame -pw #{file}`.each_line loop do begin l = enum.next.strip rescue StopIteration break end if l =~ /^([a-f0-9]{40}) \d+ \d+( \d+)?$/ last_commit = $1 l = enum.next.strip if l =~ /^author (.+)$/ commits[last_commit] = { :author => $1, :lines => 0 } while l = enum.next if l.strip =~ /^filename/ l = enum.next.strip break end end end if !options[:count_comments] && regexp = COMMENTS[ext] if is_comment next_is_comment = false if l =~ regexp[:end] else if l =~ regexp[:oneline] is_comment = true next_is_comment = false elsif l =~ regexp[:start] && l !~ regexp[:end] is_comment = next_is_comment = true end end end if (options[:count_blank] || !l.empty?) && (options[:count_comments] || !is_comment) puts l if options[:debug] commits[last_commit][:lines] += 1 end is_comment = next_is_comment end end commits.values.each do |c| contributors[c[:author]] ||= 0 contributors[c[:author]] += c[:lines] end end puts " " * cols # reverse sort by lines contributors.sort{|a,b| b[1] <=> a[1] }.each do |name, lines| puts "#{name[0..30].ljust(32)}: #{lines}" end puts "\n" puts "#{"Total Lines".ljust(32)}: #{total_lines}" puts "#{"Total Processed".ljust(32)}: #{contributors.values.inject{|sum,x| sum + x}}"