todidnt 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.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
|