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