todidnt 0.2.0 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +1 -3
- data/lib/todidnt/cache.rb +48 -0
- data/lib/todidnt/git_command.rb +8 -6
- data/lib/todidnt/git_history.rb +248 -0
- data/lib/todidnt/html_generator.rb +1 -1
- data/lib/todidnt/todo_line.rb +14 -28
- data/lib/todidnt.rb +25 -205
- data/templates/css/vendor/d3-tip.css +56 -0
- data/templates/history.erb +2 -1
- data/templates/js/todidnt-history.js +10 -1
- data/templates/js/vendor/d3-tip.js +293 -0
- data/templates/js/vendor/d3.min.js +5 -0
- data/templates/layout.erb +14 -6
- data/todidnt.gemspec +1 -1
- metadata +15 -12
- data/test/test_todo_line.rb +0 -108
- /data/templates/css/{chosen.min.css → vendor/chosen.min.css} +0 -0
- /data/templates/js/{JSXTransformer.js → vendor/JSXTransformer.js} +0 -0
- /data/templates/js/{chosen.jquery.min.js → vendor/chosen.jquery.min.js} +0 -0
- /data/templates/js/{jquery-2.1.0.min.js → vendor/jquery-2.1.0.min.js} +0 -0
- /data/templates/js/{react.js → vendor/react.js} +0 -0
- /data/templates/js/{react.min.js → vendor/react.min.js} +0 -0
- /data/templates/js/{underscore.min.js → vendor/underscore.min.js} +0 -0
data/README.md
CHANGED
@@ -20,14 +20,12 @@ Then, run `todidnt` in any Git repository directory:
|
|
20
20
|
TODO[1]
|
21
21
|
----
|
22
22
|
|
23
|
-
- Optimizing blame step
|
24
23
|
- Filtering by author, label
|
25
|
-
- Fancy formatted reports
|
26
24
|
- TODO stats
|
27
25
|
|
28
26
|
Credit
|
29
27
|
----
|
30
28
|
|
31
|
-
Paul Battley (@threedaymonk) for initial idea
|
29
|
+
Paul Battley (@threedaymonk) for initial idea and witty name.
|
32
30
|
|
33
31
|
1: Oh, the irony.
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'tilt'
|
2
|
+
require 'erb'
|
3
|
+
require 'fileutils'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
module Todidnt
|
7
|
+
class Cache
|
8
|
+
CACHE_PATH = '.todidnt/cache'
|
9
|
+
|
10
|
+
attr_reader :time, :data
|
11
|
+
|
12
|
+
def initialize(data)
|
13
|
+
@time = Time.now.to_i
|
14
|
+
@data = data
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.save(key, data)
|
18
|
+
# Create the destinateion folder unless it already exists.
|
19
|
+
FileUtils.mkdir_p(CACHE_PATH)
|
20
|
+
|
21
|
+
File.open("#{CACHE_PATH}/#{key}", 'w') do |file|
|
22
|
+
file.write(Marshal.dump(Cache.new(data)))
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.load(key)
|
27
|
+
return nil unless exists?(key)
|
28
|
+
|
29
|
+
begin
|
30
|
+
raw_cache = File.open("#{CACHE_PATH}/#{key}").read
|
31
|
+
Marshal.load(raw_cache)
|
32
|
+
rescue Exception
|
33
|
+
puts "Cache file was malformed; skipping..."
|
34
|
+
nil
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.exists?(key)
|
39
|
+
File.exists?("#{CACHE_PATH}/#{key}")
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.clear!
|
43
|
+
Dir.glob("#{CACHE_PATH}/*").each do |file|
|
44
|
+
File.delete(file)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/todidnt/git_command.rb
CHANGED
@@ -1,16 +1,18 @@
|
|
1
|
+
require 'subprocess'
|
2
|
+
|
1
3
|
module Todidnt
|
2
4
|
class GitCommand
|
3
5
|
def initialize(command, options)
|
4
6
|
@command = command
|
5
7
|
@options = options
|
6
|
-
end
|
7
8
|
|
8
|
-
|
9
|
-
run!.strip.split(/\n/)
|
9
|
+
@process = Subprocess::Process.new(command_with_options, :stdout => Subprocess::PIPE)
|
10
10
|
end
|
11
11
|
|
12
|
-
def
|
13
|
-
|
12
|
+
def execute!(&blk)
|
13
|
+
@process.stdout.each_line do |line|
|
14
|
+
yield line.chomp
|
15
|
+
end
|
14
16
|
end
|
15
17
|
|
16
18
|
def command_with_options
|
@@ -20,7 +22,7 @@ module Todidnt
|
|
20
22
|
full_command << " #{option.join(' ')}"
|
21
23
|
end
|
22
24
|
|
23
|
-
full_command
|
25
|
+
"git #{full_command}"
|
24
26
|
end
|
25
27
|
end
|
26
28
|
end
|
@@ -0,0 +1,248 @@
|
|
1
|
+
module Todidnt
|
2
|
+
class GitHistory
|
3
|
+
|
4
|
+
attr_accessor :blames
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@history = []
|
8
|
+
@blames = {}
|
9
|
+
@unmatched_deletions = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def timeline!
|
13
|
+
# TODO: It would probably be better/simpler to just Marshal the
|
14
|
+
# GitHistory object itself.
|
15
|
+
if Cache.exists?(:history)
|
16
|
+
puts "Found cached history..."
|
17
|
+
|
18
|
+
cache = Cache.load(:history)
|
19
|
+
|
20
|
+
@history = cache.data[:history]
|
21
|
+
@blames = cache.data[:blames]
|
22
|
+
@unmatched_deletions = cache.data[:unmatched_deletions]
|
23
|
+
|
24
|
+
last_commit = cache.data[:last_commit]
|
25
|
+
end
|
26
|
+
|
27
|
+
new_commit = analyze(last_commit)
|
28
|
+
if new_commit != last_commit
|
29
|
+
# If there's any new history, update the cache.
|
30
|
+
to_cache = {
|
31
|
+
last_commit: new_commit,
|
32
|
+
history: @history,
|
33
|
+
blames: @blames,
|
34
|
+
unmatched_deletions: @unmatched_deletions
|
35
|
+
}
|
36
|
+
|
37
|
+
Cache.save(:history, to_cache)
|
38
|
+
end
|
39
|
+
|
40
|
+
if @unmatched_deletions.length > 0
|
41
|
+
puts "Warning: there are some unmatched TODO deletions."
|
42
|
+
end
|
43
|
+
|
44
|
+
bucket
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def analyze(last_commit=nil)
|
50
|
+
if last_commit
|
51
|
+
puts "Going through history starting at #{last_commit}..."
|
52
|
+
commit_range = ["#{last_commit}...HEAD"]
|
53
|
+
else
|
54
|
+
puts "Going through history..."
|
55
|
+
end
|
56
|
+
|
57
|
+
command = GitCommand.new(:log, [['-G', 'TODO'], commit_range, ['--format="COMMIT %an %ae %at %h"'], ['-p'], ['-U0'], ['--no-merges'], ['--reverse']].compact)
|
58
|
+
|
59
|
+
patch_additions = []
|
60
|
+
patch_deletions = []
|
61
|
+
metadata = nil
|
62
|
+
filename = nil
|
63
|
+
commit = nil
|
64
|
+
seen_commits = Set.new
|
65
|
+
count = 0
|
66
|
+
|
67
|
+
command.execute! do |line|
|
68
|
+
if (diff = /diff --git a\/(.*) b\/(.*)/.match(line))
|
69
|
+
filename = diff[1]
|
70
|
+
elsif (diff = /^\+(.*TODO.*)/.match(line))
|
71
|
+
patch_additions << diff[1] unless filename =~ TodoLine::IGNORE
|
72
|
+
elsif (diff = /^\-(.*TODO.*)/.match(line))
|
73
|
+
patch_deletions << diff[1] unless filename =~ TodoLine::IGNORE
|
74
|
+
elsif (summary = /^COMMIT (.*) (.*) (.*) (.*)/.match(line))
|
75
|
+
count += 1
|
76
|
+
$stdout.write "\r#{count} commits analyzed..."
|
77
|
+
|
78
|
+
unless commit.nil? || seen_commits.include?(commit)
|
79
|
+
flush(metadata, patch_additions, patch_deletions)
|
80
|
+
seen_commits << commit
|
81
|
+
end
|
82
|
+
|
83
|
+
patch_additions = []
|
84
|
+
patch_deletions = []
|
85
|
+
|
86
|
+
commit = summary[4]
|
87
|
+
metadata = {
|
88
|
+
name: summary[1],
|
89
|
+
time: summary[3].to_i,
|
90
|
+
}
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
if commit
|
95
|
+
puts
|
96
|
+
flush(metadata, patch_additions, patch_deletions)
|
97
|
+
end
|
98
|
+
|
99
|
+
return commit || last_commit # return the last commit hash we were on
|
100
|
+
end
|
101
|
+
|
102
|
+
def flush(metadata, patch_additions, patch_deletions)
|
103
|
+
name = metadata[:name]
|
104
|
+
time = metadata[:time]
|
105
|
+
|
106
|
+
# Put the additions in the blame hash so when someone removes we
|
107
|
+
# can tell who the original author was. Mrrrh, this isn't going to
|
108
|
+
# work if people add the same string (pretty common e.g. # TODO).
|
109
|
+
# We can figure this out later thoug.
|
110
|
+
patch_additions.each do |line|
|
111
|
+
@blames[line] ||= []
|
112
|
+
@blames[line] << {name: name, time: time}
|
113
|
+
end
|
114
|
+
|
115
|
+
deletions_by_author = {}
|
116
|
+
patch_deletions.each do |line|
|
117
|
+
author = @blames[line] && @blames[line].pop
|
118
|
+
|
119
|
+
if author
|
120
|
+
deletions_by_author[author[:name]] ||= 0
|
121
|
+
deletions_by_author[author[:name]] += 1
|
122
|
+
else
|
123
|
+
@unmatched_deletions << line
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
@history << {
|
128
|
+
:timestamp => time,
|
129
|
+
:author => name,
|
130
|
+
:additions => patch_additions.count,
|
131
|
+
:deletions => deletions_by_author[name] || 0
|
132
|
+
}
|
133
|
+
|
134
|
+
deletions_by_author.delete(name)
|
135
|
+
deletions_by_author.each do |author, deletion_count|
|
136
|
+
@history << {
|
137
|
+
:timestamp => time,
|
138
|
+
:author => author,
|
139
|
+
:additions => 0,
|
140
|
+
:deletions => deletion_count
|
141
|
+
}
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def bucket
|
146
|
+
@history.sort_by! {|slice| slice[:timestamp]}
|
147
|
+
min_commit_date = Time.at(@history.first[:timestamp])
|
148
|
+
max_commit_date = Time.at(@history.last[:timestamp])
|
149
|
+
|
150
|
+
timespan = max_commit_date - min_commit_date
|
151
|
+
|
152
|
+
# Figure out what the interval should be based on the total timespan.
|
153
|
+
if timespan > 86400 * 365 * 10 # 10+ years
|
154
|
+
interval = 86400 * 365 # years
|
155
|
+
elsif timespan > 86400 * 365 * 5 # 5-10 years
|
156
|
+
interval = 86400 * (365 / 2) # 6 months
|
157
|
+
elsif timespan > 86400 * 365 # 2-5 years
|
158
|
+
interval = 86400 * (365 / 4) # 3 months
|
159
|
+
elsif timespan > 86400 * 30 * 6 # 6 months-3 year
|
160
|
+
interval = 86400 * 30 # months
|
161
|
+
elsif timespan > 86400 * 1 # 1 month - 6 months
|
162
|
+
interval = 86400 * 7
|
163
|
+
else # 0 - 2 months
|
164
|
+
interval = 86400 # days
|
165
|
+
end
|
166
|
+
|
167
|
+
original_interval_start = Time.new(min_commit_date.year, min_commit_date.month, min_commit_date.day).to_i
|
168
|
+
interval_start = original_interval_start
|
169
|
+
interval_end = interval_start + interval
|
170
|
+
|
171
|
+
puts "Finalizing timeline..."
|
172
|
+
buckets = []
|
173
|
+
current_bucket_authors = {}
|
174
|
+
bucket_total = 0
|
175
|
+
|
176
|
+
# Add the first bucket of 0
|
177
|
+
buckets << {
|
178
|
+
:timestamp => Time.at(interval_start).strftime('%D'),
|
179
|
+
:authors => {},
|
180
|
+
:total => 0
|
181
|
+
}
|
182
|
+
|
183
|
+
i = 0
|
184
|
+
# Going through the entire history of +/-'s of TODOs.
|
185
|
+
while i < @history.length
|
186
|
+
should_increment = false
|
187
|
+
slice = @history[i]
|
188
|
+
author = slice[:author]
|
189
|
+
|
190
|
+
# Does the current slice exist inside the bucket we're currently
|
191
|
+
# in? If so, add it to the author's total and go to the next slice.
|
192
|
+
if slice[:timestamp] >= interval_start && slice[:timestamp] < interval_end
|
193
|
+
current_bucket_authors[author] ||= 0
|
194
|
+
current_bucket_authors[author] += slice[:additions] - slice[:deletions]
|
195
|
+
bucket_total += slice[:additions] - slice[:deletions]
|
196
|
+
should_increment = true
|
197
|
+
end
|
198
|
+
|
199
|
+
# If we're on the last slice, or the next slice would have been
|
200
|
+
# in a new bucket, finish the current bucket.
|
201
|
+
if i == (@history.length - 1) || @history[i + 1][:timestamp] >= interval_end
|
202
|
+
buckets << {
|
203
|
+
:timestamp => ([Time.at(interval_end), max_commit_date].min).strftime('%D'),
|
204
|
+
:authors => current_bucket_authors,
|
205
|
+
:total => bucket_total
|
206
|
+
}
|
207
|
+
interval_start += interval
|
208
|
+
interval_end += interval
|
209
|
+
|
210
|
+
current_bucket_authors = current_bucket_authors.clone
|
211
|
+
end
|
212
|
+
|
213
|
+
i += 1 if should_increment
|
214
|
+
end
|
215
|
+
|
216
|
+
authors = Set.new
|
217
|
+
contains_other = false
|
218
|
+
buckets.each do |bucket|
|
219
|
+
significant_authors = {}
|
220
|
+
other_count = 0
|
221
|
+
bucket[:authors].each do |author, count|
|
222
|
+
# Only include the author if they account for more than > 3% of
|
223
|
+
# the TODOs in this bucket.
|
224
|
+
if count > bucket[:total] * 0.03
|
225
|
+
significant_authors[author] = count
|
226
|
+
authors << author
|
227
|
+
else
|
228
|
+
other_count += count
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
if other_count > 0
|
233
|
+
significant_authors['Other'] = other_count
|
234
|
+
contains_other = true
|
235
|
+
end
|
236
|
+
|
237
|
+
bucket[:authors] = significant_authors
|
238
|
+
end
|
239
|
+
|
240
|
+
if contains_other
|
241
|
+
authors << 'Other'
|
242
|
+
end
|
243
|
+
|
244
|
+
[buckets, authors]
|
245
|
+
end
|
246
|
+
|
247
|
+
end
|
248
|
+
end
|
@@ -13,7 +13,7 @@ module Todidnt
|
|
13
13
|
|
14
14
|
def self.generate_common
|
15
15
|
# Create the destination folder unless it already exists.
|
16
|
-
|
16
|
+
FileUtils.mkdir_p(DESTINATION_PATH)
|
17
17
|
|
18
18
|
# Copy over directories (e.g. js, css) to the destination.
|
19
19
|
common_dirs = []
|
data/lib/todidnt/todo_line.rb
CHANGED
@@ -2,52 +2,38 @@ module Todidnt
|
|
2
2
|
class TodoLine
|
3
3
|
IGNORE = %r{assets/js|third_?party|node_modules|jquery|Binary|vendor}
|
4
4
|
|
5
|
-
attr_reader :filename, :line_number, :
|
5
|
+
attr_reader :filename, :line_number, :raw_content
|
6
|
+
attr_accessor :author, :timestamp
|
6
7
|
|
7
8
|
def self.all(expressions)
|
8
9
|
options = [['-n']]
|
9
10
|
expressions.each { |e| options << ['-e', e] }
|
10
11
|
|
11
|
-
|
12
|
-
|
12
|
+
lines = []
|
13
|
+
|
14
|
+
command = GitCommand.new(:grep, options)
|
15
|
+
command.execute! do |line|
|
13
16
|
filename, line_number, content = line.split(/:/, 3)
|
14
17
|
unless filename =~ IGNORE
|
15
|
-
lines
|
18
|
+
lines << self.new(filename, line_number.to_i, content)
|
16
19
|
end
|
17
|
-
end
|
20
|
+
end
|
21
|
+
|
22
|
+
lines
|
18
23
|
end
|
19
24
|
|
20
|
-
def initialize(filename, line_number,
|
25
|
+
def initialize(filename, line_number, raw_content)
|
21
26
|
@filename = filename
|
22
27
|
@line_number = line_number
|
23
|
-
@
|
24
|
-
end
|
25
|
-
|
26
|
-
# TODO: This logic should probably be moved out somewhere else
|
27
|
-
def populate_blame
|
28
|
-
options = [
|
29
|
-
['--line-porcelain'],
|
30
|
-
['-L', "#{@line_number},#{@line_number}"],
|
31
|
-
['-w'],
|
32
|
-
[@filename]
|
33
|
-
]
|
34
|
-
|
35
|
-
blame = GitCommand.new(:blame, options)
|
36
|
-
blame.output_lines.each do |line|
|
37
|
-
if (author = /author (.*)/.match(line))
|
38
|
-
@author = author[1]
|
39
|
-
elsif (author_time = /author-time (.*)/.match(line))
|
40
|
-
@timestamp = author_time[1].to_i
|
41
|
-
end
|
42
|
-
end
|
28
|
+
@raw_content = raw_content
|
43
29
|
end
|
44
30
|
|
45
31
|
def pretty_time
|
46
32
|
Time.at(@timestamp).strftime('%F')
|
47
33
|
end
|
48
34
|
|
49
|
-
def
|
50
|
-
|
35
|
+
def content
|
36
|
+
raw_content.strip[0..100]
|
51
37
|
end
|
52
38
|
|
53
39
|
def to_hash
|
data/lib/todidnt.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
|
+
require 'todidnt/cache'
|
1
2
|
require 'todidnt/git_repo'
|
2
3
|
require 'todidnt/git_command'
|
3
4
|
require 'todidnt/todo_line'
|
5
|
+
require 'todidnt/git_history'
|
4
6
|
require 'todidnt/html_generator'
|
5
7
|
|
6
8
|
require 'chronic'
|
@@ -8,229 +10,47 @@ require 'launchy'
|
|
8
10
|
|
9
11
|
module Todidnt
|
10
12
|
class CLI
|
11
|
-
VALID_COMMANDS = %w{
|
13
|
+
VALID_COMMANDS = %w{generate clear}
|
12
14
|
|
13
15
|
def self.run(command, options)
|
16
|
+
command ||= 'generate'
|
17
|
+
|
14
18
|
if command && VALID_COMMANDS.include?(command)
|
15
19
|
self.send(command, options)
|
16
20
|
elsif command
|
17
21
|
$stderr.puts("Sorry, `#{command}` is not a valid command.")
|
18
22
|
exit
|
19
|
-
else
|
20
|
-
$stderr.puts("You must specify a command! Try `todidnt all`.")
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
def self.all(options)
|
25
|
-
all_lines = self.all_lines(options).sort_by(&:timestamp)
|
26
|
-
|
27
|
-
puts "\nOpening results..."
|
28
|
-
|
29
|
-
file_path = HTMLGenerator.generate(:all, :all_lines => all_lines)
|
30
|
-
Launchy.open("file://#{file_path}")
|
31
|
-
end
|
32
|
-
|
33
|
-
def self.overdue(options)
|
34
|
-
date = Chronic.parse(options[:date] || 'now', :context => :past)
|
35
|
-
if date.nil?
|
36
|
-
$stderr.puts("Invalid date passed: #{options[:date]}")
|
37
|
-
exit
|
38
|
-
else
|
39
|
-
puts "Finding overdue TODOs (created before #{date.strftime('%F')})..."
|
40
|
-
end
|
41
|
-
|
42
|
-
all_lines = self.all_lines(options)
|
43
|
-
|
44
|
-
puts "\nResults:"
|
45
|
-
all_lines.sort_by do |line|
|
46
|
-
line.timestamp
|
47
|
-
end.select do |line|
|
48
|
-
line.timestamp < date.to_i
|
49
|
-
end.each do |line|
|
50
|
-
puts line.pretty
|
51
23
|
end
|
52
24
|
end
|
53
25
|
|
54
|
-
def self.
|
26
|
+
def self.generate(options)
|
55
27
|
GitRepo.new(options[:path]).run do |path|
|
56
|
-
|
57
|
-
|
58
|
-
puts "Found #{lines.count} TODOs. Blaming..."
|
59
|
-
|
60
|
-
lines.each_with_index do |todo, i|
|
61
|
-
todo.populate_blame
|
62
|
-
$stdout.write "\rBlamed: #{i}/#{lines.count}"
|
63
|
-
end
|
64
|
-
|
65
|
-
lines
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
def self.history(options)
|
70
|
-
GitRepo.new(options[:path]).run do |path|
|
71
|
-
log = GitCommand.new(:log, [['-G', 'TODO'], ['--format="COMMIT %an %ae %at"'], ['-p'], ['-U0']])
|
72
|
-
|
73
|
-
history = []
|
74
|
-
|
75
|
-
blame_hash = {}
|
76
|
-
|
77
|
-
puts "Going through log..."
|
78
|
-
patch_additions = []
|
79
|
-
patch_deletions = []
|
80
|
-
filename = nil
|
81
|
-
total = log.output_lines.count
|
82
|
-
|
83
|
-
log.output_lines.reverse.each do |line|
|
84
|
-
if (summary = /^COMMIT (.*) (.*) (.*)/.match(line))
|
85
|
-
name = summary[1]
|
86
|
-
email = summary[2]
|
87
|
-
time = summary[3]
|
88
|
-
|
89
|
-
unless filename =~ TodoLine::IGNORE
|
90
|
-
# Put the additions in the blame hash so when someone removes we
|
91
|
-
# can tell who the original author was. Mrrrh, this isn't going to
|
92
|
-
# work if people add the same string (pretty common e.g. # TODO).
|
93
|
-
# We can figure this out later though.
|
94
|
-
patch_additions.each do |line|
|
95
|
-
blame_hash[line] ||= []
|
96
|
-
blame_hash[line] << name
|
97
|
-
end
|
98
|
-
|
99
|
-
deletions_by_author = {}
|
100
|
-
patch_deletions.each do |line|
|
101
|
-
author = blame_hash[line] && blame_hash[line].pop
|
102
|
-
|
103
|
-
if author
|
104
|
-
deletions_by_author[author] ||= 0
|
105
|
-
deletions_by_author[author] += 1
|
106
|
-
else
|
107
|
-
puts "BAD BAD can't find original author: #{line}"
|
108
|
-
end
|
109
|
-
end
|
28
|
+
history = GitHistory.new
|
29
|
+
buckets, authors = history.timeline!
|
110
30
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
:timestamp => time.to_i,
|
122
|
-
:author => author,
|
123
|
-
:additions => 0,
|
124
|
-
:deletions => deletion_count
|
125
|
-
}
|
126
|
-
end
|
127
|
-
end
|
128
|
-
|
129
|
-
patch_additions = []
|
130
|
-
patch_deletions = []
|
131
|
-
elsif (diff = /diff --git a\/(.*) b\/(.*)/.match(line))
|
132
|
-
filename = diff[1]
|
133
|
-
elsif (diff = /^\+(.*TODO.*)/.match(line))
|
134
|
-
patch_additions << diff[1]
|
135
|
-
elsif (diff = /^\-(.*TODO.*)/.match(line))
|
136
|
-
patch_deletions << diff[1]
|
137
|
-
end
|
138
|
-
end
|
139
|
-
|
140
|
-
history.sort_by! {|slice| slice[:timestamp]}
|
141
|
-
min_commit_date = Time.at(history.first[:timestamp])
|
142
|
-
max_commit_date = Time.at(history.last[:timestamp])
|
143
|
-
|
144
|
-
timespan = max_commit_date - min_commit_date
|
145
|
-
|
146
|
-
# Figure out what the interval should be based on the total timespan.
|
147
|
-
if timespan > 86400 * 365 * 10 # 10+ years
|
148
|
-
interval = 86400 * 365 # years
|
149
|
-
elsif timespan > 86400 * 365 * 5 # 5-10 years
|
150
|
-
interval = 86400 * (365 / 2) # 6 months
|
151
|
-
elsif timespan > 86400 * 365 # 2-5 years
|
152
|
-
interval = 86400 * (365 / 4) # 3 months
|
153
|
-
elsif timespan > 86400 * 30 * 6 # 6 months-3 year
|
154
|
-
interval = 86400 * 30 # months
|
155
|
-
elsif timespan > 86400 * 1 # 1 month - 6 months
|
156
|
-
interval = 86400 * 7
|
157
|
-
else # 0 - 2 months
|
158
|
-
interval = 86400 # days
|
159
|
-
end
|
160
|
-
|
161
|
-
original_interval_start = Time.new(min_commit_date.year, min_commit_date.month, min_commit_date.day).to_i
|
162
|
-
interval_start = original_interval_start
|
163
|
-
interval_end = interval_start + interval
|
164
|
-
|
165
|
-
puts "Finalizing timeline..."
|
166
|
-
buckets = []
|
167
|
-
current_bucket_authors = {}
|
168
|
-
bucket_total = 0
|
169
|
-
|
170
|
-
i = 0
|
171
|
-
# Going through the entire history of +/-'s of TODOs.
|
172
|
-
while i < history.length
|
173
|
-
should_increment = false
|
174
|
-
slice = history[i]
|
175
|
-
author = slice[:author]
|
176
|
-
|
177
|
-
# Does the current slice exist inside the bucket we're currently
|
178
|
-
# in? If so, add it to the author's total and go to the next slice.
|
179
|
-
if slice[:timestamp] >= interval_start && slice[:timestamp] < interval_end
|
180
|
-
current_bucket_authors[author] ||= 0
|
181
|
-
current_bucket_authors[author] += slice[:additions] - slice[:deletions]
|
182
|
-
bucket_total += slice[:additions] - slice[:deletions]
|
183
|
-
should_increment = true
|
184
|
-
end
|
185
|
-
|
186
|
-
# If we're on the last slice, or the next slice would have been
|
187
|
-
# in a new bucket, finish the current bucket.
|
188
|
-
if i == (history.length - 1) || history[i + 1][:timestamp] >= interval_end
|
189
|
-
buckets << {
|
190
|
-
:timestamp => Time.at(interval_start).strftime('%D'),
|
191
|
-
:authors => current_bucket_authors,
|
192
|
-
:total => bucket_total
|
193
|
-
}
|
194
|
-
interval_start += interval
|
195
|
-
interval_end += interval
|
196
|
-
|
197
|
-
current_bucket_authors = current_bucket_authors.clone
|
198
|
-
end
|
199
|
-
|
200
|
-
i += 1 if should_increment
|
201
|
-
end
|
202
|
-
|
203
|
-
authors = Set.new
|
204
|
-
contains_other = false
|
205
|
-
buckets.each do |bucket|
|
206
|
-
significant_authors = {}
|
207
|
-
other_count = 0
|
208
|
-
bucket[:authors].each do |author, count|
|
209
|
-
# Only include the author if they account for more than > 3% of
|
210
|
-
# the TODOs in this bucket.
|
211
|
-
if count > bucket[:total] * 0.03
|
212
|
-
significant_authors[author] = count
|
213
|
-
authors << author
|
214
|
-
else
|
215
|
-
other_count += count
|
216
|
-
end
|
217
|
-
end
|
218
|
-
|
219
|
-
if other_count > 0
|
220
|
-
significant_authors['Other'] = other_count
|
221
|
-
contains_other = true
|
31
|
+
lines = TodoLine.all(["TODO"])
|
32
|
+
lines.each do |todo|
|
33
|
+
blames = history.blames[todo.raw_content]
|
34
|
+
|
35
|
+
if blames && (metadata = blames.pop)
|
36
|
+
todo.author = metadata[:name]
|
37
|
+
todo.timestamp = metadata[:time]
|
38
|
+
else
|
39
|
+
todo.author = "(Not yet committed)"
|
40
|
+
todo.timestamp = Time.now.to_i
|
222
41
|
end
|
223
|
-
|
224
|
-
bucket[:authors] = significant_authors
|
225
|
-
end
|
226
|
-
|
227
|
-
if contains_other
|
228
|
-
authors << 'Other'
|
229
42
|
end
|
230
43
|
|
44
|
+
file_path = HTMLGenerator.generate(:all, :all_lines => lines.sort_by(&:timestamp).reverse)
|
231
45
|
file_path = HTMLGenerator.generate(:history, :data => {:history => buckets.map {|h| h[:authors].merge('Date' => h[:timestamp]) }, :authors => authors.to_a})
|
232
46
|
Launchy.open("file://#{file_path}")
|
233
47
|
end
|
234
48
|
end
|
49
|
+
|
50
|
+
def self.clear(options)
|
51
|
+
puts "Deleting cache..."
|
52
|
+
Cache.clear!
|
53
|
+
puts "Done!"
|
54
|
+
end
|
235
55
|
end
|
236
56
|
end
|