rferraz-churn 0.0.16
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/.document +5 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +54 -0
- data/LICENSE +20 -0
- data/README.rdoc +123 -0
- data/Rakefile +96 -0
- data/VERSION +1 -0
- data/bin/churn +32 -0
- data/churn.gemspec +123 -0
- data/lib/churn/bzr_analyzer.rb +31 -0
- data/lib/churn/churn_calculator.rb +252 -0
- data/lib/churn/churn_history.rb +33 -0
- data/lib/churn/git_analyzer.rb +27 -0
- data/lib/churn/hg_analyzer.rb +31 -0
- data/lib/churn/location_mapping.rb +52 -0
- data/lib/churn/source_control.rb +66 -0
- data/lib/churn/svn_analyzer.rb +23 -0
- data/lib/churn.rb +1 -0
- data/lib/tasks/churn_tasks.rb +11 -0
- data/man/churn.1 +150 -0
- data/man/churn.html +542 -0
- data/test/data/churn_calculator.rb +217 -0
- data/test/data/test_helper.rb +14 -0
- data/test/test_helper.rb +14 -0
- data/test/unit/bzr_analyzer_test.rb +65 -0
- data/test/unit/churn_calculator_test.rb +90 -0
- data/test/unit/churn_history_test.rb +24 -0
- data/test/unit/git_analyzer_test.rb +17 -0
- data/test/unit/hg_analyzer_test.rb +66 -0
- data/test/unit/location_mapping_test.rb +35 -0
- metadata +389 -0
@@ -0,0 +1,252 @@
|
|
1
|
+
require 'chronic'
|
2
|
+
require 'sexp_processor'
|
3
|
+
require 'ruby_parser'
|
4
|
+
require 'json'
|
5
|
+
require 'hirb'
|
6
|
+
require 'fileutils'
|
7
|
+
|
8
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
9
|
+
require 'source_control'
|
10
|
+
require 'git_analyzer'
|
11
|
+
require 'svn_analyzer'
|
12
|
+
require 'hg_analyzer'
|
13
|
+
require 'bzr_analyzer'
|
14
|
+
require 'location_mapping'
|
15
|
+
require 'churn_history'
|
16
|
+
|
17
|
+
module Churn
|
18
|
+
|
19
|
+
# The work horse of the the churn library. This class takes user input, determins the SCM the user is using. It then determines changes
|
20
|
+
# made during this revision. Finally it reads all the changes from previous revisions and displays human readable output to the command
|
21
|
+
# line. It can also ouput a yaml format readable by other tools such as metric_fu and Caliper.
|
22
|
+
class ChurnCalculator
|
23
|
+
|
24
|
+
# intialized the churn calculator object
|
25
|
+
def initialize(options={})
|
26
|
+
start_date = options.fetch(:start_date) { '3 months ago' }
|
27
|
+
@minimum_churn_count = options.fetch(:minimum_churn_count) { 5 }
|
28
|
+
@source_control = set_source_control(start_date)
|
29
|
+
@changes = {}
|
30
|
+
@revision_changes = {}
|
31
|
+
@class_changes = {}
|
32
|
+
@method_changes = {}
|
33
|
+
end
|
34
|
+
|
35
|
+
# prepares the data for the given project to be reported.
|
36
|
+
# reads git/svn logs analyzes the output, generates a report and either formats as a nice string or returns hash.
|
37
|
+
# @param [Bolean] format to return the data, true for string or false for hash
|
38
|
+
# @return [Object] returns either a pretty string or a hash representing the chrun of the project
|
39
|
+
def report(print = true)
|
40
|
+
self.emit
|
41
|
+
self.analyze
|
42
|
+
print ? self.to_s : self.to_h
|
43
|
+
end
|
44
|
+
|
45
|
+
# Emits various data from source control to be analyses later... Currently this is broken up like this as a throwback to metric_fu
|
46
|
+
def emit
|
47
|
+
@changes = parse_log_for_changes.reject {|file, change_count| change_count < @minimum_churn_count}
|
48
|
+
@revisions = parse_log_for_revision_changes
|
49
|
+
end
|
50
|
+
|
51
|
+
# Analyze the source control data, filter, sort, and find more information on the editted files
|
52
|
+
def analyze
|
53
|
+
@changes = sort_changes(@changes)
|
54
|
+
@changes = @changes.map {|file_path, times_changed| {:file_path => file_path, :times_changed => times_changed }}
|
55
|
+
|
56
|
+
calculate_revision_changes
|
57
|
+
|
58
|
+
@method_changes = sort_changes(@method_changes)
|
59
|
+
@method_changes = @method_changes.map {|method, times_changed| {'method' => method, 'times_changed' => times_changed }}
|
60
|
+
@class_changes = sort_changes(@class_changes)
|
61
|
+
@class_changes = @class_changes.map {|klass, times_changed| {'klass' => klass, 'times_changed' => times_changed }}
|
62
|
+
end
|
63
|
+
|
64
|
+
# collect all the data into a single hash data structure.
|
65
|
+
def to_h
|
66
|
+
hash = {:churn => {:changes => @changes}}
|
67
|
+
hash[:churn][:class_churn] = @class_changes
|
68
|
+
hash[:churn][:method_churn] = @method_changes
|
69
|
+
#detail the most recent changes made this revision
|
70
|
+
first_revision = @revisions.first
|
71
|
+
first_revision_changes = @revision_changes[first_revision]
|
72
|
+
if first_revision_changes
|
73
|
+
changes = first_revision_changes
|
74
|
+
hash[:churn][:changed_files] = changes[:files]
|
75
|
+
hash[:churn][:changed_classes] = changes[:classes]
|
76
|
+
hash[:churn][:changed_methods] = changes[:methods]
|
77
|
+
end
|
78
|
+
#TODO crappy place to do this but save hash to revision file but while entirely under metric_fu only choice
|
79
|
+
ChurnHistory.store_revision_history(first_revision, hash)
|
80
|
+
hash
|
81
|
+
end
|
82
|
+
|
83
|
+
# Pretty print the data as a string for the user
|
84
|
+
def to_s
|
85
|
+
hash = to_h[:churn]
|
86
|
+
result = seperator
|
87
|
+
result +="* Revision Changes \n"
|
88
|
+
result += seperator
|
89
|
+
result += "Files: \n"
|
90
|
+
result += display_array(hash[:changed_files], :fields=>[:to_str], :headers=>{:to_str=>'file'})
|
91
|
+
result += "\nClasses: \n"
|
92
|
+
result += display_array(hash[:changed_classes])
|
93
|
+
result += "\nMethods: \n"
|
94
|
+
result += display_array(hash[:changed_methods]) + "\n"
|
95
|
+
result += seperator
|
96
|
+
result +="* Project Churn \n"
|
97
|
+
result += seperator
|
98
|
+
result += "Files: \n"
|
99
|
+
result += display_array(hash[:changes])
|
100
|
+
result += "\nClasses: \n"
|
101
|
+
class_churn = collect_items(hash[:class_churn], 'klass')
|
102
|
+
result += display_array(class_churn)
|
103
|
+
result += "\nMethods: \n"
|
104
|
+
method_churn = collect_items(hash[:method_churn], 'method')
|
105
|
+
result += display_array(method_churn)
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def collect_items(collection, match)
|
111
|
+
collection.map {|item| (item.delete(match) || {}).merge(item) }
|
112
|
+
end
|
113
|
+
|
114
|
+
def sort_changes(changes)
|
115
|
+
changes.to_a.sort! {|first,second| second[1] <=> first[1]}
|
116
|
+
end
|
117
|
+
|
118
|
+
def filters
|
119
|
+
/.*\.rb/
|
120
|
+
end
|
121
|
+
|
122
|
+
def display_array(array, options={})
|
123
|
+
array ? Hirb::Helpers::AutoTable.render(array, options.merge(:description=>false)) + "\n" : ''
|
124
|
+
end
|
125
|
+
|
126
|
+
def seperator
|
127
|
+
"*"*70+"\n"
|
128
|
+
end
|
129
|
+
|
130
|
+
def self.git?
|
131
|
+
system("git branch")
|
132
|
+
end
|
133
|
+
|
134
|
+
def self.hg?
|
135
|
+
system("hg branch")
|
136
|
+
end
|
137
|
+
|
138
|
+
def self.bzr?
|
139
|
+
system("bzr nick")
|
140
|
+
end
|
141
|
+
|
142
|
+
def set_source_control(start_date)
|
143
|
+
if self.class.git?
|
144
|
+
GitAnalyzer.new(start_date)
|
145
|
+
elsif self.class.hg?
|
146
|
+
HgAnalyzer.new(start_date)
|
147
|
+
elsif self.class.bzr?
|
148
|
+
BzrAnalyzer.new(start_date)
|
149
|
+
elsif File.exist?(".svn")
|
150
|
+
SvnAnalyzer.new(start_date)
|
151
|
+
else
|
152
|
+
raise "Churning requires a bazaar, git, mercurial, or subversion repo"
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def calculate_revision_changes
|
157
|
+
@revisions.each do |revision|
|
158
|
+
if revision == @revisions.first
|
159
|
+
#can't iterate through all the changes and tally them up
|
160
|
+
#it only has the current files not the files at the time of the revision
|
161
|
+
#parsing requires the files
|
162
|
+
changed_files, changed_classes, changed_methods = calculate_revision_data(revision)
|
163
|
+
else
|
164
|
+
changed_files, changed_classes, changed_methods = ChurnHistory.load_revision_data(revision)
|
165
|
+
end
|
166
|
+
calculate_changes!(changed_methods, @method_changes) if changed_methods
|
167
|
+
calculate_changes!(changed_classes, @class_changes) if changed_classes
|
168
|
+
|
169
|
+
@revision_changes[revision] = { :files => changed_files, :classes => changed_classes, :methods => changed_methods }
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def calculate_revision_data(revision)
|
174
|
+
changed_files = parse_logs_for_updated_files(revision, @revisions)
|
175
|
+
|
176
|
+
changed_classes = []
|
177
|
+
changed_methods = []
|
178
|
+
changed_files.each do |file_changes|
|
179
|
+
if file_changes.first.match(filters)
|
180
|
+
classes, methods = get_changes(file_changes)
|
181
|
+
changed_classes += classes
|
182
|
+
changed_methods += methods
|
183
|
+
end
|
184
|
+
end
|
185
|
+
changed_files = changed_files.map { |file, lines| file }
|
186
|
+
[changed_files, changed_classes, changed_methods]
|
187
|
+
end
|
188
|
+
|
189
|
+
def calculate_changes!(changed_objs, total_changes)
|
190
|
+
if changed_objs
|
191
|
+
changed_objs.each do |change|
|
192
|
+
total_changes.include?(change) ? total_changes[change] = total_changes[change]+1 : total_changes[change] = 1
|
193
|
+
end
|
194
|
+
end
|
195
|
+
total_changes
|
196
|
+
end
|
197
|
+
|
198
|
+
def get_changes(change)
|
199
|
+
file = change.first
|
200
|
+
breakdown = LocationMapping.new
|
201
|
+
breakdown.get_info(file)
|
202
|
+
changes = change.last
|
203
|
+
classes = changes_for_type(changes, breakdown.klasses_collection)
|
204
|
+
methods = changes_for_type(changes, breakdown.methods_collection)
|
205
|
+
classes = classes.map{ |klass| {'file' => file, 'klass' => klass} }
|
206
|
+
methods = methods.map{ |method| {'file' => file, 'klass' => get_klass_for(method), 'method' => method} }
|
207
|
+
[classes, methods]
|
208
|
+
rescue => error
|
209
|
+
[[],[]]
|
210
|
+
end
|
211
|
+
|
212
|
+
def get_klass_for(method)
|
213
|
+
method.gsub(/(#|\.).*/,'')
|
214
|
+
end
|
215
|
+
|
216
|
+
def changes_for_type(changes, item_collection)
|
217
|
+
changed_items = []
|
218
|
+
item_collection.each_pair do |item, item_lines|
|
219
|
+
item_lines = item_lines[0].to_a
|
220
|
+
changes.each do |change_range|
|
221
|
+
item_lines.each do |line|
|
222
|
+
changed_items << item if change_range.include?(line) && !changed_items.include?(item)
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
changed_items
|
227
|
+
end
|
228
|
+
|
229
|
+
def parse_log_for_changes
|
230
|
+
changes = {}
|
231
|
+
|
232
|
+
logs = @source_control.get_logs
|
233
|
+
logs.each do |line|
|
234
|
+
changes[line] ? changes[line] += 1 : changes[line] = 1
|
235
|
+
end
|
236
|
+
changes
|
237
|
+
end
|
238
|
+
|
239
|
+
def parse_log_for_revision_changes
|
240
|
+
return [] unless @source_control.respond_to?(:get_revisions)
|
241
|
+
@source_control.get_revisions
|
242
|
+
end
|
243
|
+
|
244
|
+
def parse_logs_for_updated_files(revision, revisions)
|
245
|
+
#TODO SVN doesn't support this
|
246
|
+
return {} unless @source_control.respond_to?(:get_updated_files_change_info)
|
247
|
+
@source_control.get_updated_files_change_info(revision, revisions)
|
248
|
+
end
|
249
|
+
|
250
|
+
end
|
251
|
+
|
252
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Churn
|
2
|
+
|
3
|
+
# responcible for storing the churn history to json,
|
4
|
+
# and for loading old churn history data from json.
|
5
|
+
class ChurnHistory
|
6
|
+
|
7
|
+
#takes current revision and it's hash_data and stores it
|
8
|
+
def self.store_revision_history(revision, hash_data)
|
9
|
+
FileUtils.mkdir 'tmp' unless File.directory?('tmp')
|
10
|
+
File.open("tmp/#{revision}.json", 'w') {|file| file.write(hash_data.to_json) }
|
11
|
+
end
|
12
|
+
|
13
|
+
#given a previous project revision find and load the churn data from a json file
|
14
|
+
def self.load_revision_data(revision)
|
15
|
+
#load revision data from scratch folder if it exists
|
16
|
+
filename = "tmp/#{revision}.json"
|
17
|
+
if File.exists?(filename)
|
18
|
+
begin
|
19
|
+
json_data = File.read(filename)
|
20
|
+
data = JSON.parse(json_data)
|
21
|
+
changed_files = data['churn']['changed_files']
|
22
|
+
changed_classes = data['churn']['changed_classes']
|
23
|
+
changed_methods = data['churn']['changed_methods']
|
24
|
+
rescue JSON::ParserError
|
25
|
+
#leave all of the objects nil
|
26
|
+
end
|
27
|
+
end
|
28
|
+
[changed_files, changed_classes, changed_methods]
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Churn
|
2
|
+
|
3
|
+
#analizes git SCM to find recently changed files, and what lines have been altered
|
4
|
+
class GitAnalyzer < SourceControl
|
5
|
+
def get_logs
|
6
|
+
`git log #{date_range} --name-only --pretty=format:`.split(/\n/).reject{|line| line == ""}
|
7
|
+
end
|
8
|
+
|
9
|
+
def get_revisions
|
10
|
+
`git log #{date_range} --pretty=format:"%H"`.split(/\n/).reject{|line| line == ""}
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def get_diff(revision, previous_revision)
|
16
|
+
`git diff #{revision} #{previous_revision} --unified=0`.split(/\n/).select{|line| line.match(/^@@/) || line.match(/^---/) || line.match(/^\+\+\+/) }
|
17
|
+
end
|
18
|
+
|
19
|
+
def date_range
|
20
|
+
if @start_date
|
21
|
+
date = Chronic.parse(@start_date)
|
22
|
+
"--after=#{date.strftime('%Y-%m-%d')}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Churn
|
2
|
+
|
3
|
+
#analizes Hg / Mercurial SCM to find recently changed files, and what lines have been altered
|
4
|
+
class HgAnalyzer < SourceControl
|
5
|
+
def get_logs
|
6
|
+
`hg log -v#{date_range}`.split("\n").reject{|line| line !~ /^files:/}.map{|line| line.split(" ")[1..-1]}.flatten
|
7
|
+
end
|
8
|
+
|
9
|
+
def get_revisions
|
10
|
+
`hg log#{date_range}`.split("\n").reject{|line| line !~ /^changeset:/}.map{|line| line[/:(\S+)$/, 1] }
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def get_diff(revision, previous_revision)
|
16
|
+
`hg diff -r #{revision}:#{previous_revision} -U 0`.split(/\n/).select{|line| line.match(/^@@/) || line.match(/^---/) || line.match(/^\+\+\+/) }
|
17
|
+
end
|
18
|
+
|
19
|
+
def date_range
|
20
|
+
if @start_date
|
21
|
+
date = Chronic.parse(@start_date)
|
22
|
+
" -d \"> #{date.strftime('%Y-%m-%d')}\""
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def get_recent_file(line)
|
27
|
+
super(line).split("\t")[0]
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Churn
|
2
|
+
|
3
|
+
# Given a ruby file, map the klass and methods to a range of line numbers
|
4
|
+
# The klass and method to line numbers mappings, are stored in
|
5
|
+
# @klasses_collection and @methods_collection
|
6
|
+
class LocationMapping < SexpProcessor
|
7
|
+
|
8
|
+
attr_reader :klasses_collection, :methods_collection
|
9
|
+
|
10
|
+
def initialize()
|
11
|
+
super
|
12
|
+
@klasses_collection = {}
|
13
|
+
@methods_collection = {}
|
14
|
+
@parser = RubyParser.new
|
15
|
+
self.auto_shift_type = true
|
16
|
+
end
|
17
|
+
|
18
|
+
def get_info(file)
|
19
|
+
ast = @parser.process(File.read(file), file)
|
20
|
+
process ast
|
21
|
+
end
|
22
|
+
|
23
|
+
def process_class(exp)
|
24
|
+
name = exp.shift
|
25
|
+
start_line = exp.line
|
26
|
+
last_line = exp.last.line
|
27
|
+
name = name if name.is_a?(Symbol)
|
28
|
+
name = name.values.value if name.is_a?(Sexp) #deals with cases like class Test::Unit::TestCase
|
29
|
+
@current_class = name
|
30
|
+
@klasses_collection[name.to_s] = [] unless @klasses_collection.include?(name)
|
31
|
+
@klasses_collection[name.to_s] << (start_line..last_line)
|
32
|
+
analyze_list exp
|
33
|
+
s()
|
34
|
+
end
|
35
|
+
|
36
|
+
def analyze_list exp
|
37
|
+
process exp.shift until exp.empty?
|
38
|
+
end
|
39
|
+
|
40
|
+
def process_defn(exp)
|
41
|
+
name = exp.shift
|
42
|
+
start_line = exp.line
|
43
|
+
last_line = exp.last.line
|
44
|
+
full_name = "#{@current_class}##{name}"
|
45
|
+
@methods_collection[full_name] = [] unless @methods_collection.include?(full_name)
|
46
|
+
@methods_collection[full_name] << (start_line..last_line)
|
47
|
+
return s(:defn, name, process(exp.shift), process(exp.shift))
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Churn
|
2
|
+
|
3
|
+
# Base clase for analyzing various SCM systems like git, HG, and SVN
|
4
|
+
class SourceControl
|
5
|
+
def initialize(start_date=nil)
|
6
|
+
@start_date = start_date
|
7
|
+
end
|
8
|
+
|
9
|
+
def get_updated_files_change_info(revision, revisions)
|
10
|
+
updated = {}
|
11
|
+
logs = get_updated_files_from_log(revision, revisions)
|
12
|
+
recent_file = nil
|
13
|
+
logs.each do |line|
|
14
|
+
if line.match(/^---/) || line.match(/^\+\+\+/)
|
15
|
+
# Remove the --- a/ and +++ b/ if present
|
16
|
+
recent_file = get_recent_file(line)
|
17
|
+
updated[recent_file] = [] unless updated.include?(recent_file)
|
18
|
+
elsif line.match(/^@@/)
|
19
|
+
# Now add the added/removed ranges for the line
|
20
|
+
removed_range = get_changed_range(line, '-')
|
21
|
+
added_range = get_changed_range(line, '\+')
|
22
|
+
updated[recent_file] << removed_range
|
23
|
+
updated[recent_file] << added_range
|
24
|
+
else
|
25
|
+
puts line.match(/^---/)
|
26
|
+
raise "diff lines that don't match the two patterns aren't expected: '#{line}'"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
updated
|
30
|
+
end
|
31
|
+
|
32
|
+
def get_updated_files_from_log(revision, revisions)
|
33
|
+
current_index = revisions.index(revision)
|
34
|
+
previous_index = current_index+1
|
35
|
+
previous_revision = revisions[previous_index] unless revisions.length < previous_index
|
36
|
+
if revision && previous_revision
|
37
|
+
get_diff(revision, previous_revision)
|
38
|
+
else
|
39
|
+
[]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def get_changed_range(line, matcher)
|
46
|
+
change_start = line.match(/#{matcher}[0-9]+/)
|
47
|
+
change_end = line.match(/#{matcher}[0-9]+,[0-9]+/)
|
48
|
+
change_start = change_start.to_s.gsub(/#{matcher}/,'')
|
49
|
+
change_end = change_end.to_s.gsub(/.*,/,'')
|
50
|
+
|
51
|
+
change_start_num = change_start.to_i
|
52
|
+
range = if change_end && change_end!=''
|
53
|
+
(change_start_num..(change_start_num+change_end.to_i))
|
54
|
+
else
|
55
|
+
(change_start_num..change_start_num)
|
56
|
+
end
|
57
|
+
range
|
58
|
+
end
|
59
|
+
|
60
|
+
def get_recent_file(line)
|
61
|
+
line = line.gsub(/^--- /,'').gsub(/^\+\+\+ /,'').gsub(/^a\//,'').gsub(/^b\//,'')
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Churn
|
2
|
+
|
3
|
+
#analizes SVN SCM to find recently changed files, and what lines have been altered
|
4
|
+
class SvnAnalyzer < SourceControl
|
5
|
+
def get_logs
|
6
|
+
`svn log #{date_range} --verbose`.split(/\n/).map { |line| clean_up_svn_line(line) }.compact
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
def date_range
|
11
|
+
if @start_date
|
12
|
+
date = Chronic.parse(@start_date)
|
13
|
+
"--revision {#{date.strftime('%Y-%m-%d')}}:{#{Time.now.strftime('%Y-%m-%d')}}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def clean_up_svn_line(line)
|
18
|
+
match = line.match(/\W*[A,M]\W+(\/.*)\b/)
|
19
|
+
match ? match[1] : nil
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
data/lib/churn.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'tasks', 'churn_tasks')
|
@@ -0,0 +1,11 @@
|
|
1
|
+
def report_churn()
|
2
|
+
require File.join(File.dirname(__FILE__), '..', 'churn', 'churn_calculator')
|
3
|
+
Churn::ChurnCalculator.new({:minimum_churn_count => 3}).report
|
4
|
+
end
|
5
|
+
|
6
|
+
desc "Report the current churn for the project"
|
7
|
+
task :churn do
|
8
|
+
report = report_churn()
|
9
|
+
puts report
|
10
|
+
end
|
11
|
+
|
data/man/churn.1
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
.\" generated with Ronn/v0.4.1
|
2
|
+
.\" http://github.com/rtomayko/ronn/
|
3
|
+
.
|
4
|
+
.TH "README" "" "January 2010" "" ""
|
5
|
+
= churn
|
6
|
+
.
|
7
|
+
.P
|
8
|
+
A Project to give the churn file, class, and method for a project for a given checkin
|
9
|
+
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.
|
10
|
+
Churn for files is immediate, but classes and methods requires buildings up a history using churn between revisions. The history is stored in ./tmp
|
11
|
+
.
|
12
|
+
.P
|
13
|
+
Currently has Full Git, Mercurial (hg), and Bazaar (bzr) support, and partial SVN support (supports only file level churn currnetly)
|
14
|
+
.
|
15
|
+
.P
|
16
|
+
Authors:
|
17
|
+
* danmayer
|
18
|
+
* ajwalters
|
19
|
+
* cldwalker
|
20
|
+
* absurdhero
|
21
|
+
.
|
22
|
+
.P
|
23
|
+
== Example Output
|
24
|
+
.
|
25
|
+
.IP "\(bu" 4
|
26
|
+
Revision Changes
|
27
|
+
.
|
28
|
+
.IP "" 0
|
29
|
+
.
|
30
|
+
.P
|
31
|
+
Files:
|
32
|
+
+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+
|
33
|
+
| file |
|
34
|
+
+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+
|
35
|
+
| Rakefile |
|
36
|
+
| lib/churn/churn_calculator.rb |
|
37
|
+
+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+
|
38
|
+
.
|
39
|
+
.P
|
40
|
+
Classes:
|
41
|
+
+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+
|
42
|
+
| file | klass |
|
43
|
+
+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+
|
44
|
+
| lib/churn/churn_calculator.rb | ChurnCalculator |
|
45
|
+
+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+
|
46
|
+
.
|
47
|
+
.P
|
48
|
+
Methods:
|
49
|
+
+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+
|
50
|
+
| file | klass | method |
|
51
|
+
+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+
|
52
|
+
| lib/churn/churn_calculator.rb | ChurnCalculator | ChurnCalculator#filters |
|
53
|
+
| lib/churn/churn_calculator.rb | ChurnCalculator | ChurnCalculator#display_array |
|
54
|
+
| lib/churn/churn_calculator.rb | ChurnCalculator | ChurnCalculator#to_s |
|
55
|
+
+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+
|
56
|
+
.
|
57
|
+
.IP "\(bu" 4
|
58
|
+
Project Churn
|
59
|
+
.
|
60
|
+
.IP "" 0
|
61
|
+
.
|
62
|
+
.P
|
63
|
+
Files:
|
64
|
+
+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+
|
65
|
+
| file_path | times_changed |
|
66
|
+
+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+
|
67
|
+
| lib/churn/churn_calculator.rb | 14 |
|
68
|
+
| README.rdoc | 7 |
|
69
|
+
| lib/tasks/churn_tasks.rb | 6 |
|
70
|
+
| Rakefile | 6 |
|
71
|
+
| lib/churn/git_analyzer.rb | 4 |
|
72
|
+
| VERSION | 4 |
|
73
|
+
| test/test_helper.rb | 4 |
|
74
|
+
| test/unit/churn_calculator_test.rb | 3 |
|
75
|
+
| test/churn_test.rb | 3 |
|
76
|
+
+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+
|
77
|
+
.
|
78
|
+
.P
|
79
|
+
Classes:
|
80
|
+
+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+
|
81
|
+
| file | klass | times_changed |
|
82
|
+
+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+
|
83
|
+
| lib/churn/churn_calculator.rb | ChurnCalculator | 1 |
|
84
|
+
| lib/churn/churn_calculator.rb | ChurnCalculator | 1 |
|
85
|
+
+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+
|
86
|
+
.
|
87
|
+
.P
|
88
|
+
Methods:
|
89
|
+
+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+
|
90
|
+
| file | klass | method | times_changed |
|
91
|
+
+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+
|
92
|
+
| lib/churn/churn_calculator.rb | ChurnCalculator | ChurnCalculator#to_s | 1 |
|
93
|
+
| lib/churn/churn_calculator.rb | ChurnCalculator | ChurnCalculator#display_array | 1 |
|
94
|
+
| lib/churn/churn_calculator.rb | ChurnCalculator | ChurnCalculator#calculate_revision_data | 1 |
|
95
|
+
| lib/churn/churn_calculator.rb | ChurnCalculator | ChurnCalculator#filters | 1 |
|
96
|
+
| lib/churn/churn_calculator.rb | ChurnCalculator | ChurnCalculator#initialize | 1 |
|
97
|
+
| lib/churn/churn_calculator.rb | ChurnCalculator | ChurnCalculator#filters | 1 |
|
98
|
+
| lib/churn/churn_calculator.rb | ChurnCalculator | ChurnCalculator#to_s | 1 |
|
99
|
+
+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-+
|
100
|
+
.
|
101
|
+
.P
|
102
|
+
TODO:
|
103
|
+
* SVN only supports file, add full SVN support
|
104
|
+
* support bazaar, cvs, and darcs
|
105
|
+
* make storage directory configurable instead of using tmp
|
106
|
+
* allow passing in directories to churn, directories to ignore
|
107
|
+
* add a filter that allows for other files besides. *.rb
|
108
|
+
* ignore files pattern, so you can ignore things like vendor/, lib/, or docs/
|
109
|
+
* finish adding better documenation using YARD
|
110
|
+
.
|
111
|
+
.P
|
112
|
+
Executable Usage:
|
113
|
+
* 'gem install churn'
|
114
|
+
* go to project root run 'churn'
|
115
|
+
.
|
116
|
+
.P
|
117
|
+
Rake Usage:
|
118
|
+
* 'gem install churn'
|
119
|
+
* on any project you want to use churn, add "require 'churn'" to your rake file
|
120
|
+
* run 'rake churn' to view the current output, file churn history is immediate, class and method churn builds up a history as it is run on each revision
|
121
|
+
* temporary files with class / method churn history are stored in /tmp, to clear churn history delete them
|
122
|
+
.
|
123
|
+
.P
|
124
|
+
== Note on Patches/Pull Requests
|
125
|
+
.
|
126
|
+
.IP "\(bu" 4
|
127
|
+
Fork the project.
|
128
|
+
.
|
129
|
+
.IP "\(bu" 4
|
130
|
+
Make your feature addition or bug fix.
|
131
|
+
.
|
132
|
+
.IP "\(bu" 4
|
133
|
+
Add tests for it. This is important so I don't break it in a
|
134
|
+
future version unintentionally.
|
135
|
+
.
|
136
|
+
.IP "\(bu" 4
|
137
|
+
Commit, do not mess with rakefile, version, or history.
|
138
|
+
(if you want to have your own version, that is fine but
|
139
|
+
bump version in a commit by itself I can ignore when I pull)
|
140
|
+
.
|
141
|
+
.IP "\(bu" 4
|
142
|
+
Send me a pull request. Bonus points for topic branches.
|
143
|
+
.
|
144
|
+
.IP "" 0
|
145
|
+
.
|
146
|
+
.P
|
147
|
+
== Copyright
|
148
|
+
.
|
149
|
+
.P
|
150
|
+
Copyright (c) 2010 Dan Mayer. See LICENSE for details.
|