Created
April 9, 2011 13:28
-
-
Save michaelfeathers/911400 to your computer and use it in GitHub Desktop.
Print the complexities of all methods over time for a class in a git repo.
This file contains 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
#!/usr/bin/env ruby | |
# Print the complexities of all methods over time | |
# for a class in a git repo. | |
# | |
# Requires: bash | |
# >= git 1.7.1 | |
# >= ruby 1.9.2 | |
# >= flog 2.5.0 | |
require 'stringio' | |
require 'flog' | |
require 'time' | |
class CodeEvent | |
def initialize(hash = {}) | |
hash.each_pair do |attr,value| | |
define_attribute(attr) | |
self.send(writer_for(attr), value) | |
end | |
end | |
def file_name | |
return nil unless instance_variable_get(:@location) | |
location.split(/\//).select {|seg| seg.end_with?('.rb') }.first | |
end | |
def class_name | |
return nil unless instance_variable_get(:@location) | |
segs = location.split(/\//) | |
class_index = segs.find_index {|seg| seg.end_with?('.rb')} | |
return nil unless class_index | |
segs[class_index + 1] | |
end | |
def method_name | |
return nil unless instance_variable_get(:@location) | |
location.split(/\//).last | |
end | |
private | |
def define_attribute(attr) | |
singleton_class.send(:public) | |
singleton_class.send(:attr_accessor, attr) | |
end | |
def singleton_class | |
class << self; self; end | |
end | |
def reader_for(sym) | |
sym.to_s.end_with?('=') ? sym.to_s.chop.to_sym : sym | |
end | |
def writer_for(sym) | |
(sym.to_s + "=").to_sym | |
end | |
def method_missing(sym, *args, &block) | |
if sym.to_s.end_with?('=') | |
define_attribute(reader_for(sym)) | |
self.send(sym,*args) | |
else | |
super | |
end | |
end | |
end | |
TEMP_FILE = "/tmp/class_trend_junk.rb" | |
class ComplexityGrabber < StringIO | |
def initialize(class_name) | |
super() | |
@class_name = class_name | |
end | |
def class_complexities | |
complexities = [] | |
string.each_line do |line| | |
match_data = line.match /(\d+\.\d+):\s#{@class_name}*#(\w+)/ | |
next unless match_data | |
complexity, method_name = match_data.captures | |
complexities << CodeEvent.new(:method_name => method_name, :complexity => complexity.to_f) | |
end | |
complexities | |
end | |
end | |
class ClassTrend | |
def self.trails_for(filename, classname) | |
new(filename, classname) | |
.generate_datestamped_complexities.sort { |left,right| left.date <=> right.date } | |
.select { |o| o.method_name != "none" } | |
end | |
def initialize(filename, classname) | |
@filename = filename | |
@classname = classname | |
end | |
def generate_datestamped_complexities | |
complexities = [] | |
sha1s_of_commits.each do |sha1| | |
method_events = complexity_events_of(sha1) | |
method_events.each do |event| | |
event.date = commit_date_of(sha1) | |
complexities << event | |
end | |
end | |
complexities | |
end | |
def commit_date_of(sha1) | |
Time.parse(`git show -s --format=%cd #{sha1}`) | |
end | |
def complexity_events_of(sha1) | |
`git show #{sha1}:#{@filename} > #{TEMP_FILE}` | |
flogger = Flog.new({ :all => true }) | |
grabber = ComplexityGrabber.new(@classname) | |
flogger.flog(TEMP_FILE) | |
flogger.report(grabber) | |
$stderr.print '.' | |
grabber.class_complexities | |
end | |
def sha1s_of_commits | |
@sha1s ||= `git log #{@filename}| grep ^commit`.split(/\n/).map(&:split).map {|fields| fields[sha1_column = 1] } | |
end | |
end | |
if ARGV.size != 2 | |
puts "help: classtrend <rubyfilename> <classname>" | |
puts "" | |
puts " Run in a git repository folder" | |
puts "" | |
puts " Ex. classtrend app/models/flubber.rb Flubber" | |
puts "" | |
exit | |
end | |
class TrailRenderer | |
def initialize(trails) | |
@method_groups = trails.group_by(&:method_name) | |
@date_groups = trails.group_by(&:date) | |
@method_names = @method_groups.map {|x| x[0] } | |
end | |
def as_csv | |
header_line + body | |
end | |
private | |
def header_line | |
"," + (@method_names.map {|name| name + "," }).inject(:+) + "\n" | |
end | |
def body | |
@date_groups.map { |date,array| "#{date.strftime('%D')},#{method_complexities(array)}\n" }.inject(:+) | |
end | |
def method_complexities(array) | |
@method_names.map {|name| complexity_for_method(name,array) + ","}.inject(:+) | |
end | |
def complexity_for_method(method_name, array) | |
matches = array.select { |e| e.method_name == method_name } | |
matches.length > 0 ? matches.first.complexity.to_s: "0.0" | |
end | |
end | |
trails = ClassTrend.trails_for(ARGV[0], ARGV[1]) | |
print TrailRenderer.new(trails).as_csv |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment