turbulence 0.0.9 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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