churn 0.0.9 → 0.0.10

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.
@@ -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