Last active
January 30, 2019 16:07
-
-
Save ohthatjames/10d21dc75424b9bd67fc71bc19f7863a to your computer and use it in GitHub Desktop.
Stats of rails project over time
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
ruby -e 'require "date"; f=File.open("results.txt", "w"); (2013..2019).to_a.reverse.map{|y| (1..12).to_a.reverse.each{|m| d = Date.new(y, m, 1); next if d < Date.new(2013, 4, 3) || d > Date.new(2019, 2, 1); d = d.strftime("%Y-%m-%d"); commit = `git log --before=#{d} -n 1 --format=format:"%H"`.chomp; f.puts "#{d}: #{commit}"; f.puts `git checkout #{commit} && ruby stats.rb` }}' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require "active_support/core_ext/enumerable" | |
require "active_support/core_ext/string/inflections" | |
class CodeStatisticsCalculator #:nodoc: | |
attr_reader :lines, :code_lines, :classes, :methods | |
PATTERNS = { | |
rb: { | |
line_comment: /^\s*#/, | |
begin_block_comment: /^=begin/, | |
end_block_comment: /^=end/, | |
class: /^\s*class\s+[_A-Z]/, | |
method: /^\s*def\s+[_a-z]/, | |
}, | |
js: { | |
line_comment: %r{^\s*//}, | |
begin_block_comment: %r{^\s*/\*}, | |
end_block_comment: %r{\*/}, | |
method: /function(\s+[_a-zA-Z][\da-zA-Z]*)?\s*\(/, | |
}, | |
coffee: { | |
line_comment: /^\s*#/, | |
begin_block_comment: /^\s*###/, | |
end_block_comment: /^\s*###/, | |
class: /^\s*class\s+[_A-Z]/, | |
method: /[-=]>/, | |
} | |
} | |
PATTERNS[:minitest] = PATTERNS[:rb].merge method: /^\s*(def|test)\s+['"_a-z]/ | |
PATTERNS[:rake] = PATTERNS[:rb] | |
def initialize(lines = 0, code_lines = 0, classes = 0, methods = 0) | |
@lines = lines | |
@code_lines = code_lines | |
@classes = classes | |
@methods = methods | |
end | |
def add(code_statistics_calculator) | |
@lines += code_statistics_calculator.lines | |
@code_lines += code_statistics_calculator.code_lines | |
@classes += code_statistics_calculator.classes | |
@methods += code_statistics_calculator.methods | |
end | |
def add_by_file_path(file_path) | |
File.open(file_path) do |f| | |
add_by_io(f, file_type(file_path)) | |
end | |
end | |
def add_by_io(io, file_type) | |
patterns = PATTERNS[file_type] || {} | |
comment_started = false | |
while line = io.gets | |
@lines += 1 | |
if comment_started | |
if patterns[:end_block_comment] && line =~ patterns[:end_block_comment] | |
comment_started = false | |
end | |
next | |
else | |
if patterns[:begin_block_comment] && line =~ patterns[:begin_block_comment] | |
comment_started = true | |
next | |
end | |
end | |
@classes += 1 if patterns[:class] && line =~ patterns[:class] | |
@methods += 1 if patterns[:method] && line =~ patterns[:method] | |
if line !~ /^\s*$/ && (patterns[:line_comment].nil? || line !~ patterns[:line_comment]) | |
@code_lines += 1 | |
end | |
end | |
end | |
private | |
def file_type(file_path) | |
if file_path.end_with? "_test.rb" | |
:minitest | |
else | |
File.extname(file_path).sub(/\A\./, "").downcase.to_sym | |
end | |
end | |
end | |
class CodeStatistics #:nodoc: | |
TEST_TYPES = ["Controller tests", | |
"Helper tests", | |
"Model tests", | |
"Mailer tests", | |
"Job tests", | |
"Integration tests", | |
"System tests"] | |
HEADERS = { lines: " Lines", code_lines: " LOC", classes: "Classes", methods: "Methods" } | |
def initialize(*pairs) | |
@pairs = pairs | |
@statistics = calculate_statistics | |
@total = calculate_total if pairs.length > 1 | |
end | |
def to_s | |
print_header | |
@pairs.each { |pair| print_line(pair.first, @statistics[pair.first]) } | |
print_splitter | |
if @total | |
print_line("Total", @total) | |
print_splitter | |
end | |
print_code_test_stats | |
end | |
private | |
def calculate_statistics | |
Hash[@pairs.map { |pair| [pair.first, calculate_directory_statistics(pair.last)] }] | |
end | |
def calculate_directory_statistics(directory, pattern = /^(?!\.).*?\.(rb|js|coffee|rake)$/) | |
stats = CodeStatisticsCalculator.new | |
Dir.foreach(directory) do |file_name| | |
path = "#{directory}/#{file_name}" | |
if File.directory?(path) && (/^\./ !~ file_name) | |
stats.add(calculate_directory_statistics(path, pattern)) | |
elsif file_name =~ pattern | |
stats.add_by_file_path(path) | |
end | |
end | |
stats | |
end | |
def calculate_total | |
@statistics.each_with_object(CodeStatisticsCalculator.new) do |pair, total| | |
total.add(pair.last) | |
end | |
end | |
def calculate_code | |
code_loc = 0 | |
@statistics.each { |k, v| code_loc += v.code_lines unless TEST_TYPES.include? k } | |
code_loc | |
end | |
def calculate_tests | |
test_loc = 0 | |
@statistics.each { |k, v| test_loc += v.code_lines if TEST_TYPES.include? k } | |
test_loc | |
end | |
def width_for(label) | |
[@statistics.values.sum { |s| s.send(label) }.to_s.size, HEADERS[label].length].max | |
end | |
def print_header | |
print_splitter | |
print "| Name " | |
HEADERS.each do |k, v| | |
print " | #{v.rjust(width_for(k))}" | |
end | |
puts " | M/C | LOC/M |" | |
print_splitter | |
end | |
def print_splitter | |
print "+----------------------" | |
HEADERS.each_key do |k| | |
print "+#{'-' * (width_for(k) + 2)}" | |
end | |
puts "+-----+-------+" | |
end | |
def print_line(name, statistics) | |
m_over_c = (statistics.methods / statistics.classes) rescue m_over_c = 0 | |
loc_over_m = (statistics.code_lines / statistics.methods) - 2 rescue loc_over_m = 0 | |
print "| #{name.ljust(20)} " | |
HEADERS.each_key do |k| | |
print "| #{statistics.send(k).to_s.rjust(width_for(k))} " | |
end | |
puts "| #{m_over_c.to_s.rjust(3)} | #{loc_over_m.to_s.rjust(5)} |" | |
end | |
def print_code_test_stats | |
code = calculate_code | |
tests = calculate_tests | |
puts " Code LOC: #{code} Test LOC: #{tests} Code to Test Ratio: 1:#{sprintf("%.1f", tests.to_f / code)}" | |
puts "" | |
end | |
end | |
STATS_DIRECTORIES = [ | |
%w(Controllers app/controllers), | |
%w(Helpers app/helpers), | |
%w(Jobs app/jobs), | |
%w(Models app/models), | |
%w(Mailers app/mailers), | |
%w(Channels app/channels), | |
%w(JavaScripts app/assets/javascripts), | |
%w(Libraries lib/), | |
%w(APIs app/apis), | |
%w(Controller\ tests test/controllers), | |
%w(Helper\ tests test/helpers), | |
%w(Model\ tests test/models), | |
%w(Mailer\ tests test/mailers), | |
%w(Job\ tests test/jobs), | |
%w(Integration\ tests test/integration), | |
%w(System\ tests test/system), | |
].collect do |name, dir| | |
[ name, "./#{dir}" ] | |
end.select { |name, dir| File.directory?(dir) } | |
spec_types = begin | |
dirs = Dir['./spec/**/*_spec.rb']. | |
map { |f| f.sub(/^\.\/(spec\/\w+)\/.*/, '\\1') }. | |
uniq. | |
select { |f| File.directory?(f) } | |
Hash[dirs.map { |d| [d.split('/').last, d] }] | |
end | |
spec_types.each do |type, dir| | |
name = type.singularize.capitalize | |
::STATS_DIRECTORIES << ["#{name} specs", dir] | |
::CodeStatistics::TEST_TYPES << "#{name} specs" | |
end | |
CodeStatistics.new(*STATS_DIRECTORIES).to_s |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment