churn 0.0.9 → 0.0.10

Sign up to get free protection for your applications and to get access to all the features.
@@ -4,6 +4,13 @@ A Project to give the churn file, class, and method for a project for a given ch
4
4
  Over time the tool adds up the history of chruns to give the number of times a file, class, or method is changing during the life of a project.
5
5
  Churn for files is immediate, but classes and methods requires buildings up a history using churn between revisions. The history is stored in ./tmp
6
6
 
7
+ Currently has Full Git and Mercurial (hg) support, and partial SVN support (supports only file level churn currnetly)
8
+
9
+ Authors:
10
+ * danmayer
11
+ * ajwalters
12
+ * cldwalker
13
+
7
14
  == Example Output
8
15
  **********************************************************************
9
16
  * Revision Changes
@@ -71,11 +78,15 @@ Churn for files is immediate, but classes and methods requires buildings up a hi
71
78
  | lib/churn/churn_calculator.rb | ChurnCalculator | ChurnCalculator#to_s | 1 |
72
79
  +-------------------------------+-----------------+-----------------------------------------+---------------+
73
80
 
81
+
74
82
  TODO:
75
- * SVN only supports file
83
+ * SVN only supports file, add full SVN support
84
+ * support bazaar, cvs, and darcs
76
85
  * make storage directory configurable instead of using tmp
77
86
  * allow passing in directories to churn, directories to ignore
78
- * todo add a filter that allows for other files besides. *.rb
87
+ * add a filter that allows for other files besides. *.rb
88
+ * ignore files pattern, so you can ignore things like vendor/, lib/, or docs/
89
+ * finish adding better documenation using YARD
79
90
 
80
91
  Executable Usage:
81
92
  * 'gem install churn'
@@ -100,4 +111,4 @@ Rake Usage:
100
111
 
101
112
  == Copyright
102
113
 
103
- Copyright (c) 2009 Dan Mayer. See LICENSE for details.
114
+ Copyright (c) 2010 Dan Mayer. See LICENSE for details.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.9
1
+ 0.0.10
@@ -9,6 +9,7 @@ $LOAD_PATH.unshift(File.dirname(__FILE__))
9
9
  require 'source_control'
10
10
  require 'git_analyzer'
11
11
  require 'svn_analyzer'
12
+ require 'hg_analyzer'
12
13
  require 'location_mapping'
13
14
  require 'churn_history'
14
15
 
@@ -16,6 +17,7 @@ module Churn
16
17
 
17
18
  class ChurnCalculator
18
19
 
20
+ # intialized the churn calculator object
19
21
  def initialize(options={})
20
22
  start_date = options.fetch(:start_date) { '3 months ago' }
21
23
  @minimum_churn_count = options.fetch(:minimum_churn_count) { 5 }
@@ -26,17 +28,23 @@ module Churn
26
28
  @method_changes = {}
27
29
  end
28
30
 
31
+ # prepares the data for the given project to be reported.
32
+ # reads git/svn logs analyzes the output, generates a report and either formats as a nice string or returns hash.
33
+ # @param [Bolean] format to return the data, true for string or false for hash
34
+ # @return [Object] returns either a pretty string or a hash representing the chrun of the project
29
35
  def report(print = true)
30
36
  self.emit
31
37
  self.analyze
32
38
  print ? self.to_s : self.to_h
33
39
  end
34
40
 
41
+ # Emits various data from source control to be analyses later... Currently this is broken up like this as a throwback to metric_fu
35
42
  def emit
36
43
  @changes = parse_log_for_changes.reject {|file, change_count| change_count < @minimum_churn_count}
37
44
  @revisions = parse_log_for_revision_changes
38
45
  end
39
46
 
47
+ # Analyze the source control data, filter, sort, and find more information on the editted files
40
48
  def analyze
41
49
  @changes = @changes.to_a.sort {|x,y| y[1] <=> x[1]}
42
50
  @changes = @changes.map {|file_path, times_changed| {:file_path => file_path, :times_changed => times_changed }}
@@ -49,6 +57,7 @@ module Churn
49
57
  @class_changes = @class_changes.map {|klass, times_changed| {'klass' => klass, 'times_changed' => times_changed }}
50
58
  end
51
59
 
60
+ # collect all the data into a single hash data structure.
52
61
  def to_h
53
62
  hash = {:churn => {:changes => @changes}}
54
63
  hash[:churn][:class_churn] = @class_changes
@@ -65,6 +74,7 @@ module Churn
65
74
  hash
66
75
  end
67
76
 
77
+ # Pretty print the data as a string for the user
68
78
  def to_s
69
79
  hash = to_h
70
80
  result = seperator
@@ -107,9 +117,15 @@ module Churn
107
117
  system("git branch")
108
118
  end
109
119
 
120
+ def self.hg?
121
+ system("hg branch")
122
+ end
123
+
110
124
  def set_source_control(start_date)
111
125
  if self.class.git?
112
126
  GitAnalyzer.new(start_date)
127
+ elsif self.class.hg?
128
+ HgAnalyzer.new(start_date)
113
129
  elsif File.exist?(".svn")
114
130
  SvnAnalyzer.new(start_date)
115
131
  else
@@ -0,0 +1,71 @@
1
+ module Churn
2
+ class HgAnalyzer < SourceControl
3
+ def get_logs
4
+ `hg log -v#{date_range}`.split("\n").reject{|line| line !~ /^files:/}.map{|l| l.split(" ")[1..-1]}.flatten
5
+ end
6
+
7
+ def get_revisions
8
+ `hg log#{date_range}`.split("\n").reject{|line| line !~ /^changeset:/}.map{|l| l[/:(\S+)$/, 1] }
9
+ end
10
+
11
+ def get_updated_files_from_log(revision, revisions)
12
+ current_index = revisions.index(revision)
13
+ previous_index = current_index+1
14
+ previous_revision = revisions[previous_index] unless revisions.length < previous_index
15
+ if revision && previous_revision
16
+ `hg diff -r #{revision}:#{previous_revision} -U 0`.split(/\n/).select{|line| line.match(/^@@/) || line.match(/^---/) || line.match(/^\+\+\+/) }
17
+ else
18
+ []
19
+ end
20
+ end
21
+
22
+ def get_updated_files_change_info(revision, revisions)
23
+ updated = {}
24
+ logs = get_updated_files_from_log(revision, revisions)
25
+ recent_file = nil
26
+ logs.each do |line|
27
+ if line.match(/^---/) || line.match(/^\+\+\+/)
28
+ # Remove the --- a/ and +++ b/ if present
29
+ recent_file = get_recent_file(line)
30
+ updated[recent_file] = [] unless updated.include?(recent_file)
31
+ elsif line.match(/^@@/)
32
+ # Now add the added/removed ranges for the line
33
+ removed_range = get_changed_range(line, '-')
34
+ added_range = get_changed_range(line, '\+')
35
+ updated[recent_file] << removed_range
36
+ updated[recent_file] << added_range
37
+ else
38
+ puts line.match(/^---/)
39
+ raise "hg diff lines that don't match the two patterns aren't expected: '#{line}'"
40
+ end
41
+ end
42
+ updated
43
+ end
44
+
45
+ private
46
+ def date_range
47
+ if @start_date
48
+ date = Chronic.parse(@start_date)
49
+ " -d \"> #{date.strftime('%Y-%m-%d')}\""
50
+ end
51
+ end
52
+
53
+ def get_recent_file(line)
54
+ line = line.gsub(/^--- /,'').gsub(/^\+\+\+ /,'').gsub(/^a\//,'').gsub(/^b\//,'').split("\t")[0]
55
+ end
56
+
57
+ def get_changed_range(line, matcher)
58
+ change_start = line.match(/#{matcher}[0-9]+/)
59
+ change_end = line.match(/#{matcher}[0-9]+,[0-9]+/)
60
+ change_start = change_start.to_s.gsub(/#{matcher}/,'')
61
+ change_end = change_end.to_s.gsub(/.*,/,'')
62
+
63
+ range = if change_end && change_end!=''
64
+ (change_start.to_i..(change_start.to_i+change_end.to_i))
65
+ else
66
+ (change_start.to_i..change_start.to_i)
67
+ end
68
+ range
69
+ end
70
+ end
71
+ end
@@ -78,5 +78,13 @@ class ChurnCalculatorTest < Test::Unit::TestCase
78
78
  assert_equal [{"klass"=>{"klass"=>"LocationMapping", "file"=>"lib/churn/location_mapping.rb"}, "times_changed"=>1}], report[:churn][:class_churn]
79
79
  end
80
80
  end
81
+
82
+ should "initialize a churn calculator for hg repositories" do
83
+ Churn::ChurnCalculator.stubs(:git?).returns(false)
84
+ Churn::ChurnCalculator.stubs(:system).with('hg branch').returns(true)
85
+ churn = Churn::ChurnCalculator.new({:minimum_churn_count => 3})
86
+ assert churn.instance_variable_get(:@source_control).is_a?(Churn::HgAnalyzer)
87
+ end
88
+
81
89
 
82
90
  end
@@ -0,0 +1,66 @@
1
+ require File.expand_path('../test_helper', File.dirname(__FILE__))
2
+
3
+ class HgAnalyzerTest < Test::Unit::TestCase
4
+
5
+ context "HgAnalyzer#get_logs" do
6
+ should "return a list of changed files" do
7
+ hg_analyzer = Churn::HgAnalyzer.new
8
+ hg_analyzer.expects(:`).with('hg log -v').returns("changeset: 1:4760c1d7cd40\ntag: tip\nuser: Adam Walters <awalters@obtiva.com>\ndate: Sat Jan 16 14:21:28 2010 -0600\nfiles: file1.rb file2.rb file3.rb\ndescription:\nSecond commit with 3 files now.\nLong commit\n\n\nchangeset: 0:3cb77114f02a\nuser: Adam Walters <awalters@obtiva.com>\ndate: Sat Jan 16 14:19:32 2010 -0600\nfiles: file1.rb\ndescription:\nFirst commit\n\n\n")
9
+ assert_equal ["file1.rb", "file2.rb", "file3.rb", "file1.rb"], hg_analyzer.get_logs
10
+ end
11
+
12
+ should "scope the changed files to an optional date range" do
13
+ hg_analyzer = Churn::HgAnalyzer.new("1/16/2010")
14
+ hg_analyzer.expects(:`).with('hg log -v -d "> 2010-01-16"').returns("changeset: 1:4760c1d7cd40\ntag: tip\nuser: Adam Walters <awalters@obtiva.com>\ndate: Sat Jan 16 14:21:28 2010 -0600\nfiles: file1.rb file2.rb file3.rb\ndescription:\nSecond commit with 3 files now.\nLong commit\n\n\nchangeset: 0:3cb77114f02a\nuser: Adam Walters <awalters@obtiva.com>\ndate: Sat Jan 16 14:19:32 2010 -0600\nfiles: file1.rb\ndescription:\nFirst commit\n\n\n")
15
+ assert_equal ["file1.rb", "file2.rb", "file3.rb", "file1.rb"], hg_analyzer.get_logs
16
+ end
17
+ end
18
+
19
+ context "HgAnalyzer#get_revisions" do
20
+ should "return a list of changeset ids" do
21
+ hg_analyzer = Churn::HgAnalyzer.new
22
+ hg_analyzer.expects(:`).with('hg log').returns("changeset: 1:4760c1d7cd40\ntag: tip\nuser: Adam Walters <awalters@obtiva.com>\ndate: Sat Jan 16 14:21:28 2010 -0600\nsummary: Second commit with 3 files now.\n\nchangeset: 0:3cb77114f02a\nuser: Adam Walters <awalters@obtiva.com>\ndate: Sat Jan 16 14:19:32 2010 -0600\nsummary: First commit\n\n")
23
+ assert_equal ["4760c1d7cd40", "3cb77114f02a"], hg_analyzer.get_revisions
24
+ end
25
+
26
+ should "scope the changesets to an optional date range" do
27
+ hg_analyzer = Churn::HgAnalyzer.new("1/16/2010")
28
+ hg_analyzer.expects(:`).with('hg log -d "> 2010-01-16"').returns("changeset: 1:4760c1d7cd40\ntag: tip\nuser: Adam Walters <awalters@obtiva.com>\ndate: Sat Jan 16 14:21:28 2010 -0600\nsummary: Second commit with 3 files now.\n\nchangeset: 0:3cb77114f02a\nuser: Adam Walters <awalters@obtiva.com>\ndate: Sat Jan 16 14:19:32 2010 -0600\nsummary: First commit\n\n")
29
+ assert_equal ["4760c1d7cd40", "3cb77114f02a"], hg_analyzer.get_revisions
30
+ end
31
+ end
32
+
33
+ context "HgAnalyzer#get_updated_files_from_log(revision, revisions)" do
34
+ should "return a list of modified files and the change hunks (chunks)" do
35
+ hg_analyzer = Churn::HgAnalyzer.new
36
+ hg_analyzer.expects(:`).with('hg diff -r 4760c1d7cd40:3cb77114f02a -U 0').returns("diff -r 4760c1d7cd40 -r 3cb77114f02a file1.rb\n--- a/file1.rb\tSat Jan 16 14:21:28 2010 -0600\n+++ b/file1.rb\tSat Jan 16 14:19:32 2010 -0600\n@@ -1,3 +0,0 @@\n-First\n-Adding sample data\n-Third line\ndiff -r 4760c1d7cd40 -r 3cb77114f02a file2.rb\n--- a/file2.rb\tSat Jan 16 14:21:28 2010 -0600\n+++ /dev/null\tThu Jan 01 00:00:00 1970 +0000\n@@ -1,7 +0,0 @@\n-This is the second file.\n-\n-Little more data\n-\n-def cool_method\n- \"hello\"\n-end\ndiff -r 4760c1d7cd40 -r 3cb77114f02a file3.rb\n--- a/file3.rb\tSat Jan 16 14:21:28 2010 -0600\n+++ /dev/null\tThu Jan 01 00:00:00 1970 +0000\n@@ -1,5 +0,0 @@\n-Third file here.\n-\n-def another_method\n- \"foo\"\n-end\n")
37
+ assert_equal ["--- a/file1.rb\tSat Jan 16 14:21:28 2010 -0600", "+++ b/file1.rb\tSat Jan 16 14:19:32 2010 -0600", "@@ -1,3 +0,0 @@", "--- a/file2.rb\tSat Jan 16 14:21:28 2010 -0600", "+++ /dev/null\tThu Jan 01 00:00:00 1970 +0000", "@@ -1,7 +0,0 @@", "--- a/file3.rb\tSat Jan 16 14:21:28 2010 -0600", "+++ /dev/null\tThu Jan 01 00:00:00 1970 +0000", "@@ -1,5 +0,0 @@"], hg_analyzer.get_updated_files_from_log("4760c1d7cd40", ["4760c1d7cd40", "3cb77114f02a"])
38
+ end
39
+
40
+ should "return an empty array if it's the final revision" do
41
+ hg_analyzer = Churn::HgAnalyzer.new
42
+ assert_equal [], hg_analyzer.get_updated_files_from_log("3cb77114f02a", ["4760c1d7cd40", "3cb77114f02a"])
43
+ end
44
+ end
45
+
46
+ context "HgAnalyzer#get_updated_files_change_info(revision, revisions)" do
47
+ setup do
48
+ @hg_analyzer = Churn::HgAnalyzer.new
49
+ end
50
+
51
+ should "return all modified files with their line differences" do
52
+ @hg_analyzer.expects(:get_updated_files_from_log).with("4760c1d7cd40", ["4760c1d7cd40", "3cb77114f02a"]).returns(["--- a/file1.rb\tSat Jan 16 14:21:28 2010 -0600", "+++ b/file1.rb\tSat Jan 16 14:19:32 2010 -0600", "@@ -1,3 +0,0 @@", "--- a/file2.rb\tSat Jan 16 14:21:28 2010 -0600", "+++ /dev/null\tThu Jan 01 00:00:00 1970 +0000", "@@ -1,7 +0,0 @@", "--- a/file3.rb\tSat Jan 16 14:21:28 2010 -0600", "+++ /dev/null\tThu Jan 01 00:00:00 1970 +0000", "@@ -1,5 +0,0 @@"])
53
+ assert_equal({"/dev/null" => [1..8, 0..0, 1..6, 0..0], "file3.rb" => [], "file1.rb" => [], "file2.rb" => [], "file1.rb" => [1..4, 0..0]}, @hg_analyzer.get_updated_files_change_info("4760c1d7cd40", ["4760c1d7cd40", "3cb77114f02a"]))
54
+ end
55
+
56
+ should "raise an error if it encounters a line it cannot parse" do
57
+ @hg_analyzer.expects(:get_updated_files_from_log).with("4760c1d7cd40", ["4760c1d7cd40", "3cb77114f02a"]).returns(["foo"])
58
+ assert_raise RuntimeError do
59
+ @hg_analyzer.stubs(:puts) # supress output from raised error
60
+ @hg_analyzer.get_updated_files_change_info("4760c1d7cd40", ["4760c1d7cd40", "3cb77114f02a"])
61
+ end
62
+ end
63
+ end
64
+
65
+ end
66
+
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: churn
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.9
4
+ version: 0.0.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dan Mayer
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2010-01-19 00:00:00 -05:00
12
+ date: 2010-01-20 00:00:00 -05:00
13
13
  default_executable: churn
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -123,6 +123,7 @@ files:
123
123
  - lib/churn/churn_calculator.rb
124
124
  - lib/churn/churn_history.rb
125
125
  - lib/churn/git_analyzer.rb
126
+ - lib/churn/hg_analyzer.rb
126
127
  - lib/churn/location_mapping.rb
127
128
  - lib/churn/source_control.rb
128
129
  - lib/churn/svn_analyzer.rb
@@ -133,6 +134,7 @@ files:
133
134
  - test/unit/churn_calculator_test.rb
134
135
  - test/unit/churn_history_test.rb
135
136
  - test/unit/git_analyzer_test.rb
137
+ - test/unit/hg_analyzer_test.rb
136
138
  - test/unit/location_mapping_test.rb
137
139
  has_rdoc: true
138
140
  homepage: http://github.com/danmayer/churn
@@ -169,4 +171,5 @@ test_files:
169
171
  - test/unit/churn_calculator_test.rb
170
172
  - test/unit/churn_history_test.rb
171
173
  - test/unit/git_analyzer_test.rb
174
+ - test/unit/hg_analyzer_test.rb
172
175
  - test/unit/location_mapping_test.rb