Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Perforce integration #11

Merged
merged 13 commits into from
Oct 6, 2011
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ pkg/*
.bundle
.rvmrc
turbulence/*
tmp/*
10 changes: 9 additions & 1 deletion README.md
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,15 @@ In your project directory, run:

$ bule

For now it just dumps out a hash of churn + flog metrics
and it will generate (and open) turbulence/turbulence.html

Supported SCM systems
---------------------
Currently, bule defaults to using git. If you are using Perforce, call it like so:

$ bule --scm p4

You need to have an environment variable P4CLIENT set to the name of your client workspace.

WARNING
-------
Expand Down
19 changes: 13 additions & 6 deletions lib/turbulence/calculators/churn.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ class Churn

class << self
attr_accessor :scm, :compute_mean, :commit_range

def for_these_files(files)
changes_by_ruby_file.each do |filename, count|
yield filename, count if files.include?(filename)
Expand All @@ -14,14 +14,21 @@ def for_these_files(files)

def changes_by_ruby_file
ruby_files_changed_in_scm.group_by(&:first).map do |filename, stats|
churn = stats[0..-2].map(&:last).inject(0){|n, i| n + i}
if compute_mean && stats.size > 1
churn /= (stats.size-1)
end
[filename, churn]
churn_for_file(filename,stats)
end
end

def churn_for_file(filename,stats)
churn = stats[0..-2].map(&:last).inject(0){|running_total, changes| running_total + changes}
churn = calculate_mean_of_churn(churn, stats.size - 1) if compute_mean
[filename, churn]
end

def calculate_mean_of_churn(churn, sample_size)
return churn if sample_size < 1
churn /= sample_size
end

def ruby_files_changed_in_scm
counted_line_changes_by_file_by_commit.select do |filename, _|
filename.end_with?(RUBY_FILE_EXTENSION) && File.exist?(filename)
Expand Down
26 changes: 17 additions & 9 deletions lib/turbulence/scatter_plot_generator.rb
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -35,23 +35,31 @@ def mangle
end

def to_js
clean_metrics_from_missing_data
directory_series = {}
grouped_by_directory.each_pair do |directory, metrics_hash|
directory_series[directory] = file_metrics_for_directory(metrics_hash) end

"var directorySeries = #{directory_series.to_json};"
end

def clean_metrics_from_missing_data
metrics_hash.reject! do |filename, metrics|
metrics[x_metric].nil? || metrics[y_metric].nil?
end

grouped_by_directory = metrics_hash.group_by do |filename, _|
end

def grouped_by_directory
metrics_hash.group_by do |filename, _|
directories = File.dirname(filename).split("/")
directories[0..1].join("/")
end
end

directory_series = {}
grouped_by_directory.each_pair do |directory, metrics_hash|
directory_series[directory] = metrics_hash.map do |filename, metrics|
{:filename => filename, :x => metrics[x_metric], :y => metrics[y_metric]}
end
def file_metrics_for_directory(metrics_hash)
metrics_hash.map do |filename, metrics|
{:filename => filename, :x => metrics[x_metric], :y => metrics[y_metric]}
end

"var directorySeries = #{directory_series.to_json};"
end
end
end
34 changes: 17 additions & 17 deletions lib/turbulence/scm/git.rb
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
class Turbulence
module Scm
class Git
class << self
def log_command(commit_range)
`git log --all -M -C --numstat --format="%n" #{commit_range}`
end
def is_repo?(directory)
FileUtils.cd(directory) {
return !(`git status 2>&1` =~ /Not a git repository/)
}
end
end
end
end
end
class Turbulence
module Scm
class Git
class << self
def log_command(commit_range = "")
`git log --all -M -C --numstat --format="%n" #{commit_range}`
end

def is_repo?(directory)
FileUtils.cd(directory) {
return !(`git status 2>&1` =~ /Not a git repository/)
}
end
end
end
end
end
105 changes: 90 additions & 15 deletions lib/turbulence/scm/perforce.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,90 @@
class Turbulence
module Scm
class Perforce
class << self
def log_command(commit_range)
end

def is_repo?(directory)
p4client = ENV['P4CLIENT']
return !(p4client.nil? or p4client.empty?)
end
end
end
end
end
require 'fileutils'
require 'pathname'

class Turbulence
module Scm
class Perforce
class << self
def log_command(commit_range = "")
full_log = ""
changes.each do |cn|
files_per_change(cn).each do |file|
full_log << transform_for_output(file)
end
end
return full_log
end

def is_repo?(directory)
p4client = ENV['P4CLIENT']
return !((p4client.nil? or p4client.empty?) and not self.has_p4?)
end

def has_p4?
ENV['PATH'].split(File::PATH_SEPARATOR).any? do |directory|
File.executable?(File.join(directory, 'p4'))
end
end

def changes(commit_range = "")
p4_list_changes.each_line.map do |change|
change.match(/Change (\d+)/)[1]
end
end

def depot_to_local(depot_file)
abs_path = extract_clientfile_from_fstat_of(depot_file)
Pathname.new(abs_path).relative_path_from(Pathname.new(FileUtils.pwd)).to_s
end

def extract_clientfile_from_fstat_of(depot_file)
p4_fstat(depot_file).each_line.select {
|line| line =~ /clientFile/
}[0].split(" ")[2].tr("\\","/")
end

def files_per_change(change)
describe_output = p4_describe_change(change).split("\n")
map = []
describe_output.each_index do |index|
if describe_output[index].start_with?("====")
fn = filename_from_describe(describe_output, index)
churn = sum_of_changes(describe_output[index .. index + 4].join("\n"))
map << [churn,fn]
end
end
return map
end

def filename_from_describe(output,index)
depot_to_local(output[index].match(/==== (\/\/.*)#\d+/)[1])
end

def transform_for_output(arr)
"#{arr[0]}\t0\t#{arr[1]}\n"
end

def sum_of_changes(p4_describe_output)
churn = 0
p4_describe_output.each_line do |line|
next unless line =~ /(add|deleted|changed) .* (\d+) lines/
churn += line.match(/(\d+) lines/)[1].to_i
end
return churn
end

def p4_list_changes(commit_range = "")
`p4 changes -s submitted ...#{commit_range}`
end

def p4_fstat(depot_file)
`p4 fstat #{depot_file}`
end

def p4_describe_change(change)
`p4 describe -ds #{change}`
end
end
end
end
end
25 changes: 25 additions & 0 deletions spec/turbulence/calculators/churn_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,16 @@
calculator.compute_mean = false
calculator.changes_by_ruby_file.should =~ [ ['lib/eddies.rb', 25], ['lib/turbulence.rb', 21]]
end

it "interprets a single entry as zero churn" do
calculator.stub(:ruby_files_changed_in_scm) {
[
['lib/eddies.rb', 4],
]
}
calculator.compute_mean = false
calculator.changes_by_ruby_file.should =~ [ ['lib/eddies.rb', 0] ]
end

it "groups and takes the mean of churns, excluding the last" do
calculator.compute_mean = true
Expand All @@ -94,6 +104,21 @@
end
end

describe "::calculate_mean_of_churn" do
it "handles zero sample size" do
calculator.calculate_mean_of_churn(8,0).should == 8
end

it "returns original churn for sample size = 1" do
calculator.calculate_mean_of_churn(8,1).should == 8
end

it "returns churn divided by sample size" do
calculator.calculate_mean_of_churn(25,3).should == 8
end

end

context "Full stack tests" do
context "when one ruby file is given" do
context "with two log entries for file" do
Expand Down
2 changes: 1 addition & 1 deletion spec/turbulence/command_line_interface_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
end
it "bundles the files" do
cli.generate_bundle
Dir.glob('turbulence/*').should eq(["turbulence/cc.js", "turbulence/highcharts.js", "turbulence/jquery.min.js", "turbulence/turbulence.html"])
Dir.glob('turbulence/*').sort.should eq(["turbulence/cc.js", "turbulence/highcharts.js", "turbulence/jquery.min.js", "turbulence/turbulence.html"])
end
end
describe "command line options" do
Expand Down
68 changes: 60 additions & 8 deletions spec/turbulence/scatter_splot_generator_spec.rb
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,80 @@
it "generates JavaScript" do
generator = Turbulence::ScatterPlotGenerator.new(
"foo.rb" => {
Turbulence::Calculators::Churn => 1,
Turbulence::Calculators::Complexity => 2
}
Turbulence::Calculators::Churn => 1,
Turbulence::Calculators::Complexity => 2
}
)
generator.to_js.should =~ /var directorySeries/
generator.to_js.should =~ /\"filename\"\:\"foo.rb\"/
generator.to_js.should =~ /\"x\":1/
generator.to_js.should =~ /\"y\":2/
generator.to_js.should =~ /\"filename\"\:\"foo.rb\"/
generator.to_js.should =~ /\"x\":1/
generator.to_js.should =~ /\"y\":2/
end
end

context "with a missing Metric" do
it "generates JavaScript" do
generator = Turbulence::ScatterPlotGenerator.new(
"foo.rb" => {
Turbulence::Calculators::Churn => 1
}
Turbulence::Calculators::Churn => 1
}
)
generator.to_js.should == 'var directorySeries = {};'
end
end

describe "#clean_metrics_from_missing_data" do
let(:spg) {Turbulence::ScatterPlotGenerator.new({})}

it "removes entries with missing churn" do
spg.stub(:metrics_hash).and_return("foo.rb" => {
Turbulence::Calculators::Complexity => 88.3})
spg.clean_metrics_from_missing_data.should == {}
end

it "removes entries with missing complexity" do
spg.stub(:metrics_hash).and_return("foo.rb" => {
Turbulence::Calculators::Churn => 1})
spg.clean_metrics_from_missing_data.should == {}
end

it "keeps entries with churn and complexity present" do
spg.stub(:metrics_hash).and_return("foo.rb" => {
Turbulence::Calculators::Churn => 1,
Turbulence::Calculators::Complexity => 88.3})
spg.clean_metrics_from_missing_data.should_not == {}
end
end

describe "#grouped_by_directory" do
let(:spg) {Turbulence::ScatterPlotGenerator.new("lib/foo/foo.rb" => {
Turbulence::Calculators::Churn => 1},
"lib/bar.rb" => {
Turbulence::Calculators::Churn => 2} )}

it "uses \".\" to denote flat hierarchy" do
spg.stub(:metrics_hash).and_return("foo.rb" => {
Turbulence::Calculators::Churn => 1
})
spg.grouped_by_directory.should == {"." => [["foo.rb", Turbulence::Calculators::Churn => 1]]}
end

it "takes full path into account" do
spg.grouped_by_directory.should == {"lib/foo" => [["lib/foo/foo.rb", Turbulence::Calculators::Churn => 1]],
"lib" => [["lib/bar.rb", Turbulence::Calculators::Churn => 2]]}
end
end

describe "#file_metrics_for_directory" do
let(:spg) {Turbulence::ScatterPlotGenerator.new({})}
it "assigns :filename, :x, :y" do
spg.file_metrics_for_directory("lib/foo/foo.rb" => {
Turbulence::Calculators::Churn => 1,
Turbulence::Calculators::Complexity => 88.2}).should == [{:filename => "lib/foo/foo.rb",
:x => 1, :y => 88.2}]
end
end

describe Turbulence::FileNameMangler do
subject { Turbulence::FileNameMangler.new }
it "anonymizes a string" do
Expand Down
Loading