rferraz-churn 0.0.16

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.