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