churn 0.0.9 → 0.0.10
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +14 -3
- data/VERSION +1 -1
- data/lib/churn/churn_calculator.rb +16 -0
- data/lib/churn/hg_analyzer.rb +71 -0
- data/test/unit/churn_calculator_test.rb +8 -0
- data/test/unit/hg_analyzer_test.rb +66 -0
- metadata +5 -2
data/README.rdoc
CHANGED
@@ -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
|
-
*
|
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)
|
114
|
+
Copyright (c) 2010 Dan Mayer. See LICENSE for details.
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
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.
|
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-
|
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
|