turbulence 0.0.9 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -1,5 +1,9 @@
1
+ *.swp
2
+ .DS_Store
1
3
  pkg/*
2
4
  *.gem
5
+ *.swp
3
6
  .bundle
4
7
  .rvmrc
5
8
  turbulence/*
9
+ tmp/*
data/Gemfile CHANGED
@@ -2,11 +2,3 @@ source "http://rubygems.org"
2
2
 
3
3
  # Specify your gem's dependencies in turbulence.gemspec
4
4
  gemspec
5
-
6
- gem 'flog', "= 2.5.0"
7
- gem 'json', "= 1.4.6"
8
- gem 'launchy', '~> 0.4.0'
9
-
10
- group :development, :test do
11
- gem 'rspec'
12
- end
data/Gemfile.lock CHANGED
@@ -1,42 +1,45 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- turbulence (0.0.5)
4
+ turbulence (0.0.9)
5
+ ParseTree (~> 3.0.7)
5
6
  flog (= 2.5.0)
6
- json (= 1.4.6)
7
- launchy (~> 0.4.0)
7
+ json (>= 1.4.6)
8
+ launchy (~> 2.0.0)
8
9
 
9
10
  GEM
10
11
  remote: http://rubygems.org/
11
12
  specs:
12
- configuration (1.2.0)
13
- diff-lcs (1.1.2)
13
+ ParseTree (3.0.8)
14
+ RubyInline (>= 3.7.0)
15
+ sexp_processor (>= 3.0.0)
16
+ RubyInline (3.11.0)
17
+ ZenTest (~> 4.3)
18
+ ZenTest (4.6.2)
19
+ addressable (2.2.6)
20
+ diff-lcs (1.1.3)
14
21
  flog (2.5.0)
15
22
  ruby_parser (~> 2.0)
16
23
  sexp_processor (~> 3.0)
17
- json (1.4.6)
18
- launchy (0.4.0)
19
- configuration (>= 0.0.5)
20
- rake (>= 0.8.1)
21
- rake (0.8.7)
22
- rspec (2.5.0)
23
- rspec-core (~> 2.5.0)
24
- rspec-expectations (~> 2.5.0)
25
- rspec-mocks (~> 2.5.0)
26
- rspec-core (2.5.1)
27
- rspec-expectations (2.5.0)
24
+ json (1.6.1)
25
+ launchy (2.0.5)
26
+ addressable (~> 2.2.6)
27
+ rspec (2.6.0)
28
+ rspec-core (~> 2.6.0)
29
+ rspec-expectations (~> 2.6.0)
30
+ rspec-mocks (~> 2.6.0)
31
+ rspec-core (2.6.4)
32
+ rspec-expectations (2.6.0)
28
33
  diff-lcs (~> 1.1.2)
29
- rspec-mocks (2.5.0)
30
- ruby_parser (2.0.6)
34
+ rspec-mocks (2.6.0)
35
+ ruby_parser (2.3.1)
31
36
  sexp_processor (~> 3.0)
32
- sexp_processor (3.0.5)
37
+ sexp_processor (3.0.7)
33
38
 
34
39
  PLATFORMS
35
40
  ruby
41
+ x86-mingw32
36
42
 
37
43
  DEPENDENCIES
38
- flog (= 2.5.0)
39
- json (= 1.4.6)
40
- launchy (~> 0.4.0)
41
- rspec
44
+ rspec (~> 2.6.0)
42
45
  turbulence!
data/README.md CHANGED
@@ -1,17 +1,28 @@
1
1
  Hopefully-meaningful Metrics
2
2
  ============================
3
3
 
4
- A quick hack based on Michael Feathers' recent work in project churn and complexity:
5
- http://www.stickyminds.com/sitewide.asp?Function=edetail&ObjectType=COL&ObjectId=16679&tth=DYN&tt=siteemail&iDyn=2
4
+ Based on Michael Feathers' [recent work](http://www.stickyminds.com/sitewide.asp?Function=edetail&ObjectType=COL&ObjectId=16679&tth=DYN&tt=siteemail&iDyn=2) in project churn and complexity.
6
5
 
6
+ Installation
7
+ ------------
8
+
9
+ $ gem install turbulence
7
10
 
8
11
  Usage
9
12
  -----
13
+ In your project directory, run:
14
+
15
+ $ bule
16
+
17
+ and it will generate (and open) turbulence/turbulence.html
10
18
 
11
- $ bule
19
+ Supported SCM systems
20
+ ---------------------
21
+ Currently, bule defaults to using git. If you are using Perforce, call it like so:
12
22
 
13
- For now it just dumps out a hash of churn + flog metrics
23
+ $ bule --scm p4
14
24
 
25
+ You need to have an environment variable P4CLIENT set to the name of your client workspace.
15
26
 
16
27
  WARNING
17
28
  -------
data/Rakefile CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'bundler'
2
+ require File.join(File.dirname(__FILE__), 'win_rakefile_location_fix')
2
3
  Bundler::GemHelper.install_tasks
3
4
  require 'rake'
4
5
  require 'rspec/core/rake_task'
data/bin/bule CHANGED
@@ -2,12 +2,13 @@
2
2
  require 'turbulence'
3
3
  require 'turbulence/checks_environment'
4
4
 
5
- unless Turbulence::ChecksEnvironment.git_repo?(Dir.pwd)
6
- STDERR.puts "Turbulence could not calculate metrics, as we could not find a git repository in the current directory."
7
- STDERR.puts "Please run bule from inside a git repository."
5
+ cli = Turbulence::CommandLineInterface.new(ARGV)
6
+
7
+ unless Turbulence::ChecksEnvironment.scm_repo?(Dir.pwd)
8
+ STDERR.puts "Turbulence could not calculate metrics, as we could not find a repository in the current directory."
9
+ STDERR.puts "Please run bule from inside a repository."
8
10
  exit
9
11
  end
10
12
 
11
- cli = Turbulence::CommandLineInterface.new(Dir.pwd)
12
13
  cli.generate_bundle
13
14
  cli.open_bundle
@@ -1,38 +1,53 @@
1
1
  class Turbulence
2
2
  module Calculators
3
3
  class Churn
4
+ RUBY_FILE_EXTENSION = ".rb"
5
+
4
6
  class << self
7
+ attr_accessor :scm, :compute_mean, :commit_range
8
+
5
9
  def for_these_files(files)
6
- changes_by_ruby_file.select do |count, filename|
10
+ changes_by_ruby_file.each do |filename, count|
7
11
  yield filename, count if files.include?(filename)
8
12
  end
9
13
  end
10
14
 
11
15
  def changes_by_ruby_file
12
- ruby_files_changed_in_git.group_by(&:first).map do |filename, stats|
13
- [stats.map(&:last).tap{|list| list.pop}.inject(0){|n, i| n + i}, filename]
16
+ ruby_files_changed_in_scm.group_by(&:first).map do |filename, stats|
17
+ churn_for_file(filename,stats)
14
18
  end
15
19
  end
16
20
 
17
- def counted_line_changes_by_file_by_commit
18
- git_log_file_lines.map do |line|
19
- adds, deletes, filename = line.split(/\t/)
20
- [filename, adds.to_i + deletes.to_i]
21
+ def churn_for_file(filename,stats)
22
+ churn = stats[0..-2].map(&:last).inject(0){|running_total, changes| running_total + changes}
23
+ churn = calculate_mean_of_churn(churn, stats.size - 1) if compute_mean
24
+ [filename, churn]
25
+ end
26
+
27
+ def calculate_mean_of_churn(churn, sample_size)
28
+ return churn if sample_size < 1
29
+ churn /= sample_size
30
+ end
31
+
32
+ def ruby_files_changed_in_scm
33
+ counted_line_changes_by_file_by_commit.select do |filename, _|
34
+ filename.end_with?(RUBY_FILE_EXTENSION) && File.exist?(filename)
21
35
  end
22
36
  end
23
37
 
24
- def ruby_files_changed_in_git
25
- counted_line_changes_by_file_by_commit.select do |filename, count|
26
- filename =~ /\.rb$/ && File.exist?(filename)
38
+ def counted_line_changes_by_file_by_commit
39
+ scm_log_file_lines.map do |line|
40
+ adds, deletes, filename = line.split(/\t/)
41
+ [filename, adds.to_i + deletes.to_i]
27
42
  end
28
43
  end
29
44
 
30
- def git_log_file_lines
31
- git_log_command.each_line.reject{|line| line =~ /^\n$/}.map(&:chomp)
45
+ def scm_log_file_lines
46
+ scm_log_command.each_line.reject{|line| line == "\n"}.map(&:chomp)
32
47
  end
33
48
 
34
- def git_log_command
35
- `git log --all -M -C --numstat --format="%n"`
49
+ def scm_log_command
50
+ scm.log_command(commit_range)
36
51
  end
37
52
  end
38
53
  end
@@ -5,7 +5,7 @@ class Turbulence
5
5
  class Complexity
6
6
  class << self
7
7
  def flogger
8
- @flogger ||= Flog.new
8
+ @flogger ||= Flog.new(:continue => true)
9
9
  end
10
10
  def for_these_files(files)
11
11
  files.each do |filename|
@@ -18,14 +18,13 @@ class Turbulence
18
18
  reporter = Reporter.new
19
19
  flogger.report(reporter)
20
20
  reporter.score
21
- rescue SyntaxError, Racc::ParseError
22
- STDERR.puts "\nError flogging: #{filename}\n"
23
21
  end
24
22
  end
25
23
 
26
24
  class Reporter < ::StringIO
25
+ SCORE_LINE_DETECTOR = /^\s+([^:]+).*flog total$/
27
26
  def score
28
- Float(string.scan(/^\s+([^:]+).*total$/).flatten.first)
27
+ Float(string.scan(SCORE_LINE_DETECTOR).flatten.first)
29
28
  end
30
29
  end
31
30
  end
@@ -1,11 +1,8 @@
1
- require 'open3'
2
1
  class Turbulence
3
2
  class ChecksEnvironment
4
3
  class << self
5
- def git_repo?(directory)
6
- Open3.popen3("git status") do |_, _, err, _|
7
- return !(err.read =~ /Not a git repository/)
8
- end
4
+ def scm_repo?(directory)
5
+ Turbulence::Calculators::Churn.scm.is_repo?(directory)
9
6
  end
10
7
  end
11
8
  end
@@ -1,23 +1,51 @@
1
1
  require 'fileutils'
2
2
  require 'launchy'
3
+ require 'optparse'
4
+ require 'turbulence/scm/git'
5
+ require 'turbulence/scm/perforce'
3
6
 
4
7
  class Turbulence
5
8
  class CommandLineInterface
6
- TURBULENCE_PATH = File.join(File.expand_path(File.dirname(__FILE__)), "..", "..")
7
-
9
+ TURBULENCE_TEMPLATE_PATH = File.join(File.expand_path(File.dirname(__FILE__)), "..", "..", "template")
10
+ TEMPLATE_FILES = ['turbulence.html', 'highcharts.js', 'jquery.min.js'].map { |filename|
11
+ File.join(TURBULENCE_TEMPLATE_PATH, filename)
12
+ }
13
+
14
+ attr_reader :exclusion_pattern
8
15
  attr_reader :directory
9
- def initialize(directory)
10
- @directory = directory
11
- end
16
+ def initialize(argv)
17
+ Turbulence::Calculators::Churn.scm = Scm::Git
18
+ OptionParser.new do |opts|
19
+ opts.banner = "Usage: bule [options] [dir]"
20
+
21
+ opts.on('--scm p4|git', String, 'scm to use (default: git)') do |s|
22
+ case s
23
+ when "git", "", nil
24
+ when "p4"
25
+ Turbulence::Calculators::Churn.scm = Scm::Perforce
26
+ end
27
+ end
28
+ opts.on('--churn-range since..until', String, 'commit range to compute file churn') do |s|
29
+ Turbulence::Calculators::Churn.commit_range = s
30
+ end
31
+ opts.on('--churn-mean', 'calculate mean churn instead of cummulative') do
32
+ Turbulence::Calculators::Churn.compute_mean = true
33
+ end
34
+ opts.on('--exclude pattern', String, 'exclude files matching pattern') do |pattern|
35
+ @exclusion_pattern = pattern
36
+ end
12
37
 
13
- def path_to_template(filename)
14
- File.join(TURBULENCE_PATH, "template", filename)
38
+ opts.on_tail("-h", "--help", "Show this message") do
39
+ puts opts
40
+ exit
41
+ end
42
+ end.parse!(argv)
43
+
44
+ @directory = argv.first || Dir.pwd
15
45
  end
16
46
 
17
47
  def copy_templates_into(directory)
18
- ['turbulence.html', 'highcharts.js', 'jquery.min.js'].each do |filename|
19
- FileUtils.cp path_to_template(filename), directory
20
- end
48
+ FileUtils.cp TEMPLATE_FILES, directory
21
49
  end
22
50
  private :copy_templates_into
23
51
 
@@ -26,7 +54,8 @@ class Turbulence
26
54
  Dir.chdir("turbulence") do
27
55
  copy_templates_into(Dir.pwd)
28
56
  File.open("cc.js", "w") do |f|
29
- f.write Turbulence::ScatterPlotGenerator.from(Turbulence.new(directory).metrics).to_js
57
+ turb = Turbulence.new(directory,STDOUT, @exclusion_pattern)
58
+ f.write Turbulence::ScatterPlotGenerator.from(turb.metrics).to_js
30
59
  end
31
60
  end
32
61
  end
@@ -1,31 +1,65 @@
1
1
  require 'json'
2
+
2
3
  class Turbulence
4
+ class FileNameMangler
5
+ def initialize
6
+ @current_id = 0
7
+ @segment_map = { "" => "", "app" => "app", "controllers" => "controllers", "helpers" => "helpers", "lib" => "lib" }
8
+ end
9
+
10
+ def transform(segment)
11
+ @segment_map[segment] ||= (@current_id += 1)
12
+ end
13
+
14
+ def mangle_name(filename)
15
+ filename.split('/').map {|seg|transform(seg)}.join('/') + ".rb"
16
+ end
17
+ end
18
+
3
19
  class ScatterPlotGenerator
4
20
  def self.from(metrics_hash)
5
21
  new(metrics_hash)
6
22
  end
7
- attr_reader :metrics_hash
8
- def initialize(metrics_hash)
23
+ attr_reader :metrics_hash, :x_metric, :y_metric
24
+ def initialize(metrics_hash, x_metric = Turbulence::Calculators::Churn, y_metric = Turbulence::Calculators::Complexity)
25
+ @x_metric = x_metric
26
+ @y_metric = y_metric
9
27
  @metrics_hash = metrics_hash
10
28
  end
11
29
 
30
+ def mangle
31
+ mangler = FileNameMangler.new
32
+ mangled = {}
33
+ metrics_hash.each_pair { |filename, metrics| mangled[mangler.mangle_name(filename)] = metrics}
34
+ @metrics_hash = mangled
35
+ end
36
+
12
37
  def to_js
13
- grouped_by_directory = metrics_hash.group_by do |filename, _|
38
+ clean_metrics_from_missing_data
39
+ directory_series = {}
40
+ grouped_by_directory.each_pair do |directory, metrics_hash|
41
+ directory_series[directory] = file_metrics_for_directory(metrics_hash) end
42
+
43
+ "var directorySeries = #{directory_series.to_json};"
44
+ end
45
+
46
+ def clean_metrics_from_missing_data
47
+ metrics_hash.reject! do |filename, metrics|
48
+ metrics[x_metric].nil? || metrics[y_metric].nil?
49
+ end
50
+ end
51
+
52
+ def grouped_by_directory
53
+ metrics_hash.group_by do |filename, _|
14
54
  directories = File.dirname(filename).split("/")
15
55
  directories[0..1].join("/")
16
56
  end
57
+ end
17
58
 
18
- directory_series = {}
19
- grouped_by_directory.each_pair do |directory, metrics_hash|
20
- data_in_json_format = metrics_hash.map do |filename, metrics|
21
- {:filename => filename, :x => metrics[Turbulence::Calculators::Churn], :y => metrics[Turbulence::Calculators::Complexity]}
22
- end.reject do |metrics|
23
- metrics[:x].nil? || metrics[:y].nil?
24
- end
25
- directory_series[directory] = data_in_json_format
59
+ def file_metrics_for_directory(metrics_hash)
60
+ metrics_hash.map do |filename, metrics|
61
+ {:filename => filename, :x => metrics[x_metric], :y => metrics[y_metric]}
26
62
  end
27
-
28
- "var directorySeries = #{directory_series.to_json};"
29
63
  end
30
64
  end
31
65
  end
@@ -0,0 +1,17 @@
1
+ class Turbulence
2
+ module Scm
3
+ class Git
4
+ class << self
5
+ def log_command(commit_range = "")
6
+ `git log --all -M -C --numstat --format="%n" #{commit_range}`
7
+ end
8
+
9
+ def is_repo?(directory)
10
+ FileUtils.cd(directory) {
11
+ return !(`git status 2>&1` =~ /Not a git repository/)
12
+ }
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,90 @@
1
+ require 'fileutils'
2
+ require 'pathname'
3
+
4
+ class Turbulence
5
+ module Scm
6
+ class Perforce
7
+ class << self
8
+ def log_command(commit_range = "")
9
+ full_log = ""
10
+ changes.each do |cn|
11
+ files_per_change(cn).each do |file|
12
+ full_log << transform_for_output(file)
13
+ end
14
+ end
15
+ return full_log
16
+ end
17
+
18
+ def is_repo?(directory)
19
+ p4client = ENV['P4CLIENT']
20
+ return !((p4client.nil? or p4client.empty?) and not self.has_p4?)
21
+ end
22
+
23
+ def has_p4?
24
+ ENV['PATH'].split(File::PATH_SEPARATOR).any? do |directory|
25
+ File.executable?(File.join(directory, 'p4'))
26
+ end
27
+ end
28
+
29
+ def changes(commit_range = "")
30
+ p4_list_changes.each_line.map do |change|
31
+ change.match(/Change (\d+)/)[1]
32
+ end
33
+ end
34
+
35
+ def depot_to_local(depot_file)
36
+ abs_path = extract_clientfile_from_fstat_of(depot_file)
37
+ Pathname.new(abs_path).relative_path_from(Pathname.new(FileUtils.pwd)).to_s
38
+ end
39
+
40
+ def extract_clientfile_from_fstat_of(depot_file)
41
+ p4_fstat(depot_file).each_line.select {
42
+ |line| line =~ /clientFile/
43
+ }[0].split(" ")[2].tr("\\","/")
44
+ end
45
+
46
+ def files_per_change(change)
47
+ describe_output = p4_describe_change(change).split("\n")
48
+ map = []
49
+ describe_output.each_index do |index|
50
+ if describe_output[index].start_with?("====")
51
+ fn = filename_from_describe(describe_output, index)
52
+ churn = sum_of_changes(describe_output[index .. index + 4].join("\n"))
53
+ map << [churn,fn]
54
+ end
55
+ end
56
+ return map
57
+ end
58
+
59
+ def filename_from_describe(output,index)
60
+ depot_to_local(output[index].match(/==== (\/\/.*)#\d+/)[1])
61
+ end
62
+
63
+ def transform_for_output(arr)
64
+ "#{arr[0]}\t0\t#{arr[1]}\n"
65
+ end
66
+
67
+ def sum_of_changes(p4_describe_output)
68
+ churn = 0
69
+ p4_describe_output.each_line do |line|
70
+ next unless line =~ /(add|deleted|changed) .* (\d+) lines/
71
+ churn += line.match(/(\d+) lines/)[1].to_i
72
+ end
73
+ return churn
74
+ end
75
+
76
+ def p4_list_changes(commit_range = "")
77
+ `p4 changes -s submitted ...#{commit_range}`
78
+ end
79
+
80
+ def p4_fstat(depot_file)
81
+ `p4 fstat #{depot_file}`
82
+ end
83
+
84
+ def p4_describe_change(change)
85
+ `p4 describe -ds #{change}`
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -1,3 +1,3 @@
1
1
  class Turbulence
2
- VERSION = "0.0.9"
2
+ VERSION = "1.0.0"
3
3
  end
data/lib/turbulence.rb CHANGED
@@ -1,43 +1,43 @@
1
1
  require 'turbulence/scatter_plot_generator'
2
2
  require 'turbulence/command_line_interface'
3
+ require 'turbulence/checks_environment'
3
4
  require 'turbulence/calculators/churn'
4
5
  require 'turbulence/calculators/complexity'
5
6
 
6
7
  class Turbulence
7
- attr_reader :dir
8
+ CODE_DIRECTORIES = ["app/models", "app/controllers", "app/helpers", "lib"]
9
+ CALCULATORS = [Turbulence::Calculators::Complexity, Turbulence::Calculators::Churn]
10
+
11
+ attr_reader :exclusion_pattern
8
12
  attr_reader :metrics
9
- def initialize(dir)
10
- @dir = dir
13
+ def initialize(directory, output = nil, exclusion_pattern = nil)
14
+ @output = output
11
15
  @metrics = {}
12
- Dir.chdir(dir) do
13
- churn
14
- complexity
16
+ @exclusion_pattern = exclusion_pattern
17
+ Dir.chdir(directory) do
18
+ CALCULATORS.each(&method(:calculate_metrics_with))
15
19
  end
16
20
  end
17
21
 
18
22
  def files_of_interest
19
- files = ["app/models", "app/controllers", "app/helpers", "lib"].map{|base_dir| "#{base_dir}/**/*\.rb"}
20
- @ruby_files ||= Dir[*files]
21
- end
22
-
23
- def complexity
24
- calculate_metrics Turbulence::Calculators::Complexity
25
- end
26
-
27
- def churn
28
- calculate_metrics Turbulence::Calculators::Churn
23
+ file_list = CODE_DIRECTORIES.map{|base_dir| "#{base_dir}/**/*\.rb"}
24
+ @ruby_files ||= exclude_files(Dir[*file_list])
29
25
  end
30
26
 
31
- def calculate_metrics(calculator)
32
- puts "calculating metric: #{calculator}"
27
+ def calculate_metrics_with(calculator)
28
+ report "calculating metric: #{calculator}\n"
33
29
  calculator.for_these_files(files_of_interest) do |filename, score|
30
+ report "."
34
31
  set_file_metric(filename, calculator, score)
35
32
  end
36
- puts "\n"
33
+ report "\n"
34
+ end
35
+
36
+ def report(this)
37
+ @output.print this unless @output.nil?
37
38
  end
38
39
 
39
40
  def set_file_metric(filename, metric, value)
40
- print "."
41
41
  metrics_for(filename)[metric] = value
42
42
  end
43
43
 
@@ -45,4 +45,11 @@ class Turbulence
45
45
  @metrics[filename] ||= {}
46
46
  end
47
47
 
48
+ private
49
+ def exclude_files(files)
50
+ if not @exclusion_pattern.nil?
51
+ files = files.reject { |f| f =~ Regexp.new(@exclusion_pattern) }
52
+ end
53
+ files
54
+ end
48
55
  end