class Churn::ChurnCalculator

The work horse of the the churn library. This class takes user input, determines the SCM the user is using. It then determines changes made during this revision. Finally it reads all the changes from previous revisions and displays human readable output on the command line. It can also output a yaml format readable by other tools such as metric_fu and Caliper.

Public Class Methods

new(options={}) click to toggle source

intialize the churn calculator object

# File lib/churn/calculator.rb, line 30
def initialize(options={})
  @churn_options = ChurnOptions.new.set_options(options)

  @minimum_churn_count = @churn_options.minimum_churn_count
  @ignores             = @churn_options.ignores
  @source_control      = SourceControl.set_source_control(@churn_options.start_date)

  @changes          = {}
  @revision_changes = {}
  @class_changes    = {}
  @method_changes   = {}
end
to_s(hash) click to toggle source

Pretty print the data as a string for the user

# File lib/churn/calculator.rb, line 115
def self.to_s(hash)
  result = separator
  result +="* Revision Changes \n"
  result += separator
  result += display_array("Files", hash[:changed_files], :fields=>[:to_str], :headers=>{:to_str=>'file'})
  result += "\n"
  result += display_array("Classes", hash[:changed_classes])
  result += "\n"
  result += display_array("Methods", hash[:changed_methods]) + "\n"
  result += separator
  result +="* Project Churn \n"
  result += separator
  result += "\n"
  result += display_array("Files", hash[:changes])
  result += "\n"
  class_churn = collect_items(hash[:class_churn], 'klass')
  result += display_array("Classes", class_churn)
  result += "\n"
  method_churn = collect_items(hash[:method_churn], 'method')
  result += display_array("Methods", method_churn)
end

Private Class Methods

collect_items(collection, match) click to toggle source
# File lib/churn/calculator.rb, line 139
def self.collect_items(collection, match)
  return [] unless collection
  collection.map {|item| (item.delete(match) || {}).merge(item) }
end
display_array(title, array, options={}) click to toggle source
# File lib/churn/calculator.rb, line 164
def self.display_array(title, array, options={})
  response = ''
  if array && array.length > 0
    response = "#{title}\n"
    response << Hirb::Helpers::AutoTable.render(array, options.merge(:description=>false)) + "\n"
  end
  response
end
separator() click to toggle source
# File lib/churn/calculator.rb, line 173
def self.separator
  "*"*70+"\n"
end

Public Instance Methods

analyze() click to toggle source

Analyze the source control data, filter, sort, and find more information on the edited files

# File lib/churn/calculator.rb, line 77
def analyze
  @changes = sort_changes(@changes)
  @changes = filter_changes(@changes)
  @changes = @changes.map {|file_path, times_changed| {:file_path => file_path, :times_changed => times_changed }}

  calculate_revision_changes

  @method_changes = sort_changes(@method_changes)
  @method_changes = @method_changes.map {|method, times_changed| {'method' => method, 'times_changed' => times_changed }}
  @class_changes  = sort_changes(@class_changes)
  @class_changes  = @class_changes.map {|klass, times_changed| {'klass' => klass, 'times_changed' => times_changed }}
end
emit() click to toggle source

Emits various data from source control to be analyzed later… Currently this is broken up like this as a throwback to metric_fu

# File lib/churn/calculator.rb, line 70
def emit
  @changes   = reject_ignored_files(reject_low_churn_files(parse_log_for_changes))
  @revisions = parse_log_for_revision_changes
end
generate_history() click to toggle source

this method generates the past history of a churn project from first commit to current running the report for oldest commits first so they are built up correctly

# File lib/churn/calculator.rb, line 62
def generate_history
  history_starting_point = Chronic.parse(@churn_options.history)
  @source_control.generate_history(history_starting_point)
  "churn history complete, this has manipulated your source control system so please make sure you are back on HEAD where you expect to be"
end
report(print = true) click to toggle source

prepares the data for the given project to be reported. reads git/svn logs analyzes the output, generates a report and either formats as a nice string or returns hash. @param [Boolean] print to return the data, true for string or false for hash @return [Object] returns either a pretty string or a hash representing the churn of the project

# File lib/churn/calculator.rb, line 49
def report(print = true)
  if @churn_options.history
    generate_history
  else
    emit
    analyze
    print ? self.to_s : self.to_h
  end
end
to_h() click to toggle source

collect all the data into a single hash data structure.

# File lib/churn/calculator.rb, line 91
def to_h
  hash                        = {:churn => {:changes => @changes}}
  hash[:churn][:class_churn]  = @class_changes
  hash[:churn][:method_churn] = @method_changes
  #detail the most recent changes made this revision
  first_revision         = @revisions.first
  first_revision_changes = @revision_changes[first_revision]
  if first_revision_changes
    changes = first_revision_changes
    hash[:churn][:changed_files]   = changes[:files]
    hash[:churn][:changed_classes] = changes[:classes]
    hash[:churn][:changed_methods] = changes[:methods]
  end
  # TODO crappy place to do this but save hash to revision file but
  # while entirely under metric_fu only choice
  ChurnHistory.store_revision_history(first_revision, hash, @churn_options.data_directory)
  hash
end
to_s() click to toggle source
# File lib/churn/calculator.rb, line 110
def to_s
  ChurnCalculator.to_s(to_h[:churn])
end

Private Instance Methods

calculate_changes!(changed_objs, total_changes) click to toggle source
# File lib/churn/calculator.rb, line 210
def calculate_changes!(changed_objs, total_changes)
  if changed_objs
    changed_objs.each do |change|
      total_changes.include?(change) ? total_changes[change] = total_changes[change]+1 : total_changes[change] = 1
    end
  end
  total_changes
end
calculate_revision_changes() click to toggle source
# File lib/churn/calculator.rb, line 177
def calculate_revision_changes
  @revisions.each do |revision|
    if revision == @revisions.first
      #can't iterate through all the changes and tally them up
      #it only has the current files not the files at the time of the revision
      #parsing requires the files
      changed_files, changed_classes, changed_methods = calculate_revision_data(revision)
    else
      changed_files, changed_classes, changed_methods = ChurnHistory.load_revision_data(revision, @churn_options.data_directory)
    end
    calculate_changes!(changed_methods, @method_changes) if changed_methods
    calculate_changes!(changed_classes, @class_changes) if changed_classes

    @revision_changes[revision] = { :files => changed_files, :classes => changed_classes, :methods => changed_methods }
  end
end
calculate_revision_data(revision) click to toggle source
# File lib/churn/calculator.rb, line 194
def calculate_revision_data(revision)
  changed_files   = parse_logs_for_updated_files(revision, @revisions)

  changed_classes = []
  changed_methods = []
  changed_files.each do |file_changes|
    if file_changes.first =~ filters
      classes, methods = get_changes(file_changes)
      changed_classes += classes
      changed_methods += methods
    end
  end
  changed_files   = changed_files.map { |file, lines| file }
  [changed_files, changed_classes, changed_methods]
end
changes_for_type(changes, item_collection) click to toggle source
# File lib/churn/calculator.rb, line 237
def changes_for_type(changes, item_collection)
  changed_items  = []
  item_collection.each_pair do |item, item_lines|
    item_lines = item_lines[0].to_a
    changes.each do |change_range|
      item_lines.each do |line|
        changed_items << item if change_range.include?(line) && !changed_items.include?(item)
      end
    end
  end
  changed_items
end
filter_changes(changes) click to toggle source
# File lib/churn/calculator.rb, line 148
def filter_changes(changes)
  if @churn_options.file_extension && !@churn_options.file_extension.empty?
    changes = changes.select { |file_path, _revision_count| file_path =~ /\.#{@churn_options.file_extension}\z/ }
  end

  if @churn_options.file_prefix && !@churn_options.file_prefix.empty?
    changes = changes.select { |file_path, _revision_count| file_path =~ /\A#{@churn_options.file_prefix}/ }
  end

  changes
end
filters() click to toggle source
# File lib/churn/calculator.rb, line 160
def filters
  /.*\.rb/
end
get_changes(change) click to toggle source
# File lib/churn/calculator.rb, line 219
def get_changes(change)
  file = change.first
  breakdown = LocationMapping.new
  breakdown.get_info(file)
  changes = change.last
  classes = changes_for_type(changes, breakdown.klasses_collection)
  methods = changes_for_type(changes, breakdown.methods_collection)
  classes = classes.map{ |klass| {'file' => file, 'klass' => klass} }
  methods = methods.map{ |method| {'file' => file, 'klass' => get_klass_for(method), 'method' => method} }
  [classes, methods]
rescue
  [[],[]]
end
get_klass_for(method) click to toggle source
# File lib/churn/calculator.rb, line 233
def get_klass_for(method)
  method.gsub(/(#|\.).*/,'')
end
parse_log_for_changes() click to toggle source
# File lib/churn/calculator.rb, line 250
def parse_log_for_changes
  changes = Hash.new(0)

  logs = @source_control.get_logs
  logs.each do |line|
    changes[line] += 1
  end
  changes
end
parse_log_for_revision_changes() click to toggle source
# File lib/churn/calculator.rb, line 260
def parse_log_for_revision_changes
  @source_control.get_revisions
end
parse_logs_for_updated_files(revision, revisions) click to toggle source
# File lib/churn/calculator.rb, line 264
def parse_logs_for_updated_files(revision, revisions)
  files = @source_control.get_updated_files_change_info(revision, revisions)
  reject_ignored_files(files)
end
reject_ignored_files(files) click to toggle source
# File lib/churn/calculator.rb, line 273
def reject_ignored_files(files)
   files.reject do |file, _|
     @ignores.any? do |ignore|
       begin
         /#{ignore}/ =~ file
       rescue RegexpError => e
         puts "churn: ignoring invalid regex: #{e}"
         false
       end
     end
   end
end
reject_low_churn_files(files) click to toggle source
# File lib/churn/calculator.rb, line 269
def reject_low_churn_files(files)
  files.reject{ |_, change_count| change_count < @minimum_churn_count }
end
sort_changes(changes) click to toggle source
# File lib/churn/calculator.rb, line 144
def sort_changes(changes)
  changes.to_a.sort! {|first,second| second[1] <=> first[1]}
end