git_fame 2.5.3 → 3.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/exe/git-fame +9 -0
- data/lib/git_fame/author.rb +5 -75
- data/lib/git_fame/base.rb +9 -523
- data/lib/git_fame/collector.rb +45 -0
- data/lib/git_fame/command.rb +171 -0
- data/lib/git_fame/contribution.rb +12 -0
- data/lib/git_fame/diff.rb +25 -0
- data/lib/git_fame/error.rb +5 -0
- data/lib/git_fame/extension.rb +16 -0
- data/lib/git_fame/filter.rb +43 -0
- data/lib/git_fame/render/extension.rb +26 -0
- data/lib/git_fame/render.rb +38 -0
- data/lib/git_fame/result.rb +29 -4
- data/lib/git_fame/types.rb +11 -0
- data/lib/git_fame/version.rb +3 -1
- data/lib/git_fame.rb +16 -3
- metadata +82 -111
- data/.gitignore +0 -20
- data/.gitmodules +0 -3
- data/.rspec +0 -7
- data/.travis.yml +0 -16
- data/Gemfile +0 -4
- data/LICENSE +0 -22
- data/README.md +0 -140
- data/Rakefile +0 -15
- data/bin/git-fame +0 -58
- data/git_fame.gemspec +0 -42
- data/lib/git_fame/blame_parser.rb +0 -83
- data/lib/git_fame/commit_range.rb +0 -27
- data/lib/git_fame/errors.rb +0 -6
- data/lib/git_fame/file.rb +0 -13
- data/lib/git_fame/helper.rb +0 -11
- data/lib/git_fame/silent_progressbar.rb +0 -20
- data/spec/bin_spec.rb +0 -116
- data/spec/git_fame_spec.rb +0 -488
- data/spec/spec_helper.rb +0 -65
- data/spec/support/startup.rb +0 -21
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 518ce7b11cdf47772c74350a9d9bb1af59074f248170d9f5f62ea5a38b665a81
|
4
|
+
data.tar.gz: cf0742737450cf10694334cd7570250b363ded43b04ba528968daa2f532111fc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 456dcc0f5527156fd4e88212d0573f305d4de1730116ce4482fe8c41625da8d54c3bb02859ffb6f400a9bea73340c2ff9495f934f169ca8c07a64e1b3091af24
|
7
|
+
data.tar.gz: d564055b144b6de9f4d9ae3af6943ddd6a50c9ed17e11c59e970b4e26a9dad706ed5a95f45b3cca5ebc6dd37eb4ac219539b755f300f223cb00a3b844ee5aeb2
|
data/exe/git-fame
ADDED
data/lib/git_fame/author.rb
CHANGED
@@ -1,78 +1,8 @@
|
|
1
|
-
|
2
|
-
class Author
|
3
|
-
include GitFame::Helper
|
4
|
-
attr_accessor :name, :raw_files, :raw_commits,
|
5
|
-
:raw_loc, :files_list, :file_type_counts
|
6
|
-
|
7
|
-
FIELDS = [:loc, :commits, :files]
|
8
|
-
|
9
|
-
#
|
10
|
-
# @args Hash
|
11
|
-
#
|
12
|
-
def initialize(args = {})
|
13
|
-
@raw_loc = 0
|
14
|
-
@raw_commits = 0
|
15
|
-
@raw_files = 0
|
16
|
-
@file_type_counts = Hash.new(0)
|
17
|
-
args.keys.each do |name|
|
18
|
-
instance_variable_set "@" + name.to_s, args[name]
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
def merge(author)
|
23
|
-
tap do
|
24
|
-
FIELDS.each do |field|
|
25
|
-
inc(field, author.raw(field))
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
#
|
31
|
-
# @format loc / commits / files
|
32
|
-
# @return String Distribution (in %) between users
|
33
|
-
#
|
34
|
-
def distribution
|
35
|
-
"%s / %s / %s" % FIELDS.map do |field|
|
36
|
-
("%.1f" % (percent_for_field(field) * 100)).rjust(4, " ")
|
37
|
-
end
|
38
|
-
end
|
39
|
-
alias_method :"distribution (%)", :distribution
|
40
|
-
|
41
|
-
FIELDS.each do |method|
|
42
|
-
define_method(method) do
|
43
|
-
number_with_delimiter(raw(method))
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
def update(params)
|
48
|
-
params.keys.each do |key|
|
49
|
-
send("#{key}=", params[key])
|
50
|
-
end
|
51
|
-
end
|
1
|
+
# frozen_string_literal: true
|
52
2
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
file_type_counts[m.to_s]
|
58
|
-
end
|
59
|
-
|
60
|
-
def raw(method)
|
61
|
-
unless FIELDS.include?(method.to_sym)
|
62
|
-
raise "can't access raw '#{method}' on author"
|
63
|
-
end
|
64
|
-
|
65
|
-
send("raw_#{method}")
|
66
|
-
end
|
67
|
-
|
68
|
-
def inc(method, amount)
|
69
|
-
send("raw_#{method}=", raw(method) + amount)
|
70
|
-
end
|
71
|
-
|
72
|
-
private
|
73
|
-
|
74
|
-
def percent_for_field(field)
|
75
|
-
raw(field) / @parent.send(field).to_f
|
76
|
-
end
|
3
|
+
module GitFame
|
4
|
+
class Author < Base
|
5
|
+
attribute :name, Types::String
|
6
|
+
attribute :email, Types::String
|
77
7
|
end
|
78
8
|
end
|
data/lib/git_fame/base.rb
CHANGED
@@ -1,533 +1,19 @@
|
|
1
|
-
|
2
|
-
require "time"
|
3
|
-
require "open3"
|
4
|
-
require "hirb"
|
5
|
-
require "memoist"
|
6
|
-
require "timeout"
|
7
|
-
|
8
|
-
# String#scrib is build in to Ruby 2.1+
|
9
|
-
if RUBY_VERSION.to_f < 2.1
|
10
|
-
require "scrub_rb"
|
11
|
-
end
|
12
|
-
|
13
|
-
require "git_fame/helper"
|
14
|
-
require "git_fame/author"
|
15
|
-
require "git_fame/silent_progressbar"
|
16
|
-
require "git_fame/blame_parser"
|
17
|
-
require "git_fame/result"
|
18
|
-
require "git_fame/file"
|
19
|
-
require "git_fame/errors"
|
20
|
-
require "git_fame/commit_range"
|
1
|
+
# frozen_string_literal: true
|
21
2
|
|
22
3
|
module GitFame
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
class Base
|
27
|
-
include GitFame::Helper
|
28
|
-
extend Memoist
|
29
|
-
|
30
|
-
#
|
31
|
-
# @args[:repository] String Absolute path to git repository
|
32
|
-
# @args[:sort] String What should #authors be sorted by?
|
33
|
-
# @args[:by_type] Boolean Should counts be grouped by file extension?
|
34
|
-
# @args[:exclude] String Comma-separated list of paths in the repo
|
35
|
-
# which should be excluded
|
36
|
-
# @args[:branch] String Branch to run from
|
37
|
-
# @args[:after] date after
|
38
|
-
# @args[:before] date before
|
39
|
-
#
|
40
|
-
def initialize(args)
|
41
|
-
@default_settings = {
|
42
|
-
branch: "master",
|
43
|
-
sorting: "loc",
|
44
|
-
ignore_types: ["image", "binary"]
|
45
|
-
}
|
46
|
-
@progressbar = args.fetch(:progressbar, false)
|
47
|
-
@file_authors = Hash.new { |h,k| h[k] = {} }
|
48
|
-
# Create array out of comma separated list
|
49
|
-
@exclude = args.fetch(:exclude, "").split(",").
|
50
|
-
map{ |path| path.strip.sub(/\A\//, "") }
|
51
|
-
@extensions = args.fetch(:extensions, "").split(",")
|
52
|
-
# Default sorting option is by loc
|
53
|
-
@include = args.fetch(:include, "").split(",")
|
54
|
-
@sort = args.fetch(:sort, @default_settings.fetch(:sorting))
|
55
|
-
@repository = File.expand_path(args.fetch(:repository))
|
56
|
-
@by_type = args.fetch(:by_type, false)
|
57
|
-
@branch = args.fetch(:branch, nil)
|
58
|
-
@everything = args.fetch(:everything, false)
|
59
|
-
@timeout = args.fetch(:timeout, CMD_TIMEOUT)
|
60
|
-
@git_dir = File.join(@repository, ".git")
|
61
|
-
|
62
|
-
# Figure out what branch the caller is using
|
63
|
-
if present?(@branch = args[:branch])
|
64
|
-
unless branch_exists?(@branch)
|
65
|
-
raise Error, "Branch '#{@branch}' does not exist"
|
66
|
-
end
|
67
|
-
else
|
68
|
-
@branch = default_branch
|
69
|
-
end
|
70
|
-
|
71
|
-
@after = args.fetch(:after, nil)
|
72
|
-
@before = args.fetch(:before, nil)
|
73
|
-
[@after, @before].each do |date|
|
74
|
-
if date and not valid_date?(date)
|
75
|
-
raise Error, "#{date} is not a valid date"
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
# Fields that should be visible in the final table
|
80
|
-
# Used by #csv_puts, #to_csv and #pretty_puts
|
81
|
-
# Format: [ [ :method_on_author, "custom column name" ] ]
|
82
|
-
@visible_fields = [
|
83
|
-
:name,
|
84
|
-
:loc,
|
85
|
-
:commits,
|
86
|
-
:files,
|
87
|
-
[:distribution, "distribution (%)"]
|
88
|
-
]
|
89
|
-
@wopt = args.fetch(:whitespace, false) ? "-w" : ""
|
90
|
-
@authors = {}
|
91
|
-
@verbose = args.fetch(:verbose, false)
|
92
|
-
populate
|
93
|
-
end
|
94
|
-
|
95
|
-
#
|
96
|
-
# Generates pretty output
|
97
|
-
#
|
98
|
-
def pretty_puts
|
99
|
-
extend Hirb::Console
|
100
|
-
Hirb.enable({ pager: false })
|
101
|
-
puts "\nStatistics based on #{commit_range.to_s(true)}"
|
102
|
-
puts "Active files: #{number_with_delimiter(files)}"
|
103
|
-
puts "Active lines: #{number_with_delimiter(loc)}"
|
104
|
-
puts "Total commits: #{number_with_delimiter(commits)}\n"
|
105
|
-
unless @everything
|
106
|
-
puts "\nNote: Files matching MIME type #{ignore_types.join(", ")} has been ignored\n\n"
|
107
|
-
end
|
108
|
-
table(authors, fields: printable_fields)
|
109
|
-
end
|
110
|
-
|
111
|
-
#
|
112
|
-
# Prints CSV
|
113
|
-
#
|
114
|
-
def csv_puts
|
115
|
-
puts to_csv
|
116
|
-
end
|
117
|
-
|
118
|
-
#
|
119
|
-
# Generate csv output
|
120
|
-
#
|
121
|
-
def to_csv
|
122
|
-
CSV.generate do |csv|
|
123
|
-
csv << fields
|
124
|
-
authors.each do |author|
|
125
|
-
csv << fields.map do |f|
|
126
|
-
author.send(f)
|
127
|
-
end
|
128
|
-
end
|
129
|
-
end
|
130
|
-
end
|
131
|
-
|
132
|
-
#
|
133
|
-
# @return Fixnum Total number of files
|
134
|
-
# TODO: Rename this
|
135
|
-
#
|
136
|
-
def files
|
137
|
-
used_files.count
|
138
|
-
end
|
139
|
-
|
140
|
-
#
|
141
|
-
# @return Array list of repo files processed
|
142
|
-
#
|
143
|
-
# TODO: Rename
|
144
|
-
def file_list; used_files; end
|
145
|
-
|
146
|
-
#
|
147
|
-
# @return Fixnum Total number of commits
|
148
|
-
#
|
149
|
-
def commits
|
150
|
-
authors.inject(0) { |result, author| author.raw(:commits) + result }
|
151
|
-
end
|
152
|
-
|
153
|
-
#
|
154
|
-
# @return Fixnum Total number of lines
|
155
|
-
#
|
156
|
-
def loc
|
157
|
-
authors.inject(0) { |result, author| author.raw(:loc) + result }
|
158
|
-
end
|
159
|
-
|
160
|
-
#
|
161
|
-
# @return Array<Author> A list of authors
|
162
|
-
#
|
163
|
-
def authors
|
164
|
-
unique_authors.sort_by do |author|
|
165
|
-
@sort == "name" ? author.send(@sort) : -1 * author.raw(@sort)
|
166
|
-
end
|
167
|
-
end
|
168
|
-
|
169
|
-
protected
|
170
|
-
|
171
|
-
# Populates @authors and with data
|
172
|
-
# Block is called on every call to populate, but
|
173
|
-
# the data is only calculated once
|
174
|
-
def populate
|
175
|
-
# Display progressbar with the number of files as countdown
|
176
|
-
progressbar = init_progressbar(current_files.count)
|
177
|
-
|
178
|
-
# Extract the blame history from all checked in files
|
179
|
-
current_files.each do |file|
|
180
|
-
progressbar.increment
|
181
|
-
|
182
|
-
# Skip this file if non wanted type
|
183
|
-
next unless check_file?(file)
|
184
|
-
|
185
|
-
# -w ignore whitespaces (defined in @wopt)
|
186
|
-
# -M detect moved or copied lines.
|
187
|
-
# -p procelain mode (parsed by BlameParser)
|
188
|
-
execute("git #{git_directory_params} blame #{encoding_opt} -p -M #{default_params} #{commit_range.to_s} #{@wopt} -- '#{file}'") do |result|
|
189
|
-
BlameParser.new(result.to_s).parse.each do |row|
|
190
|
-
next if row[:boundary]
|
191
|
-
|
192
|
-
email = get(row, :author, :mail)
|
193
|
-
name = get(row, :author, :name)
|
194
|
-
|
195
|
-
# Create or find user
|
196
|
-
author = author_by_email(email, name)
|
197
|
-
|
198
|
-
# Get author by name and increase the number of loc by 1
|
199
|
-
author.inc(:loc, get(row, :num_lines))
|
200
|
-
|
201
|
-
# Store the files and authors together
|
202
|
-
associate_file_with_author(author, file)
|
203
|
-
end
|
204
|
-
end
|
205
|
-
end
|
206
|
-
|
207
|
-
# Get repository summery and update each author accordingly
|
208
|
-
execute("git #{git_directory_params} shortlog #{encoding_opt} #{default_params} -se #{commit_range.to_s}") do |result|
|
209
|
-
result.to_s.split("\n").map do |line|
|
210
|
-
_, commits, name, email = line.match(/(\d+)\s+(.+)\s+<(.+?)>/).to_a
|
211
|
-
author = author_by_email(email)
|
212
|
-
|
213
|
-
author.name = name
|
214
|
-
|
215
|
-
author.update({
|
216
|
-
raw_commits: commits.to_i,
|
217
|
-
raw_files: files_from_author(author).count,
|
218
|
-
files_list: files_from_author(author)
|
219
|
-
})
|
220
|
-
end
|
221
|
-
end
|
222
|
-
|
223
|
-
progressbar.finish
|
224
|
-
end
|
225
|
-
|
226
|
-
# Ignore mime types found in {ignore_types}
|
227
|
-
def check_file?(file)
|
228
|
-
return true if @everything
|
229
|
-
type = mime_type_for_file(file)
|
230
|
-
! ignore_types.any? { |ignored| type.include?(ignored) }
|
231
|
-
end
|
232
|
-
|
233
|
-
# Return mime type for file (form: x/y)
|
234
|
-
def mime_type_for_file(file)
|
235
|
-
execute("git #{git_directory_params} show #{commit_range.range.last}:'#{file}' | LC_ALL=C file --mime-type -").to_s.
|
236
|
-
match(/.+: (.+?)$/).to_a[1]
|
237
|
-
end
|
238
|
-
|
239
|
-
def get(hash, *keys)
|
240
|
-
keys.inject(hash) { |h, key| h.fetch(key) }
|
241
|
-
end
|
242
|
-
|
243
|
-
def ignore_types
|
244
|
-
@default_settings.fetch(:ignore_types)
|
245
|
-
end
|
246
|
-
|
247
|
-
def unique_authors
|
248
|
-
# Merges duplicate users (users with the same name)
|
249
|
-
# Object#dup prevents the original to be changed
|
250
|
-
@authors.values.dup.each_with_object({}) do |author, result|
|
251
|
-
if ex_author = result[author.name]
|
252
|
-
result[author.name] = ex_author.dup.merge(author)
|
253
|
-
else
|
254
|
-
result[author.name] = author
|
255
|
-
end
|
256
|
-
end.values
|
257
|
-
end
|
258
|
-
|
259
|
-
# Uses the more printable names in @visible_fields
|
260
|
-
def printable_fields
|
261
|
-
raw_fields.map do |field|
|
262
|
-
field.is_a?(Array) ? field.last : field
|
263
|
-
end
|
264
|
-
end
|
265
|
-
|
266
|
-
def associate_file_with_author(author, file)
|
267
|
-
if @by_type
|
268
|
-
author.file_type_counts[file.extname] += 1
|
269
|
-
end
|
270
|
-
@file_authors[author][file] ||= 1
|
271
|
-
end
|
272
|
-
|
273
|
-
def used_files
|
274
|
-
@file_authors.values.map(&:keys).flatten.uniq
|
275
|
-
end
|
276
|
-
|
277
|
-
def file_extensions
|
278
|
-
used_files.map(&:extname)
|
279
|
-
end
|
280
|
-
|
281
|
-
# Check to see if a string is empty (nil or "")
|
282
|
-
def blank?(value)
|
283
|
-
value.nil? or value.empty?
|
284
|
-
end
|
285
|
-
|
286
|
-
def files_from_author(author)
|
287
|
-
@file_authors[author].keys
|
288
|
-
end
|
4
|
+
class Base < Dry::Struct
|
5
|
+
schema schema.strict(true)
|
289
6
|
|
290
|
-
|
291
|
-
not blank?(value)
|
292
|
-
end
|
293
|
-
|
294
|
-
def valid_date?(date)
|
295
|
-
!! date.match(/\d{4}-\d{2}-\d{2}/)
|
296
|
-
end
|
297
|
-
|
298
|
-
# Includes fields from file extensions
|
299
|
-
def raw_fields
|
300
|
-
return @visible_fields unless @by_type
|
301
|
-
(@visible_fields + file_extensions).uniq
|
302
|
-
end
|
303
|
-
|
304
|
-
# Method fields used by #to_csv and #pretty_puts
|
305
|
-
def fields
|
306
|
-
raw_fields.map do |field|
|
307
|
-
field.is_a?(Array) ? field.first : field
|
308
|
-
end
|
309
|
-
end
|
310
|
-
|
311
|
-
# Command to be executed at @repository
|
312
|
-
# @silent = true wont raise an error on exit code =! 0
|
313
|
-
def execute(command, silent = false, &block)
|
314
|
-
result = run_with_timeout(command)
|
315
|
-
if result.success? or silent
|
316
|
-
warn command if @verbose
|
317
|
-
return result unless block
|
318
|
-
return block.call(result)
|
319
|
-
end
|
320
|
-
raise Error, cmd_error_message(command, result.data)
|
321
|
-
rescue Errno::ENOENT
|
322
|
-
raise Error, cmd_error_message(command, $!.message)
|
323
|
-
end
|
324
|
-
|
325
|
-
def run_with_timeout(command)
|
326
|
-
if @timeout != -1
|
327
|
-
Timeout.timeout(CMD_TIMEOUT) { run_no_timeout(command) }
|
328
|
-
else
|
329
|
-
run_no_timeout(command)
|
330
|
-
end
|
331
|
-
end
|
332
|
-
|
333
|
-
def run_no_timeout(command)
|
334
|
-
out, err, status = Open3.capture3(command)
|
335
|
-
ok = status.success?
|
336
|
-
output = ok ? out : err
|
337
|
-
Result.new(output.scrub.strip, ok)
|
338
|
-
end
|
339
|
-
|
340
|
-
def cmd_error_message(command, message)
|
341
|
-
"Could not run '#{command}' => #{message}"
|
342
|
-
end
|
343
|
-
|
344
|
-
# Does @branch exist in the current git repo?
|
345
|
-
def branch_exists?(branch)
|
346
|
-
return true if branch == "HEAD"
|
347
|
-
execute("git #{git_directory_params} show-ref '#{branch}'", true) do |result|
|
348
|
-
result.success?
|
349
|
-
end
|
350
|
-
end
|
351
|
-
|
352
|
-
# In those cases the users havent defined a branch
|
353
|
-
# We try to define it for him/her by
|
354
|
-
# 1. check if { @default_settings.fetch(:branch) } exists
|
355
|
-
# 1. look at .git/HEAD (basically)
|
356
|
-
def default_branch
|
357
|
-
if branch_exists?(@default_settings.fetch(:branch))
|
358
|
-
return @default_settings.fetch(:branch)
|
359
|
-
end
|
360
|
-
|
361
|
-
execute("git #{git_directory_params} rev-parse HEAD | head -1") do |result|
|
362
|
-
return result.data.split(" ")[0] if result.success?
|
363
|
-
end
|
364
|
-
raise Error, "No branch found. Define one using --branch=<branch>"
|
365
|
-
end
|
366
|
-
|
367
|
-
def author_by_email(email, name = nil)
|
368
|
-
@authors[(email || "").strip] ||= Author.new({ parent: self, name: name })
|
369
|
-
end
|
370
|
-
|
371
|
-
# Lists the paths to contained git submodules
|
372
|
-
def current_submodules
|
373
|
-
execute("git config --file .gitmodules --get-regexp path | awk '{ print $2 }'") do |result|
|
374
|
-
result.to_s.split(/\n/)
|
375
|
-
end
|
376
|
-
end
|
377
|
-
|
378
|
-
# List all files in current git directory, excluding
|
379
|
-
# extensions in @extensions defined by the user
|
380
|
-
def current_files
|
381
|
-
if commit_range.is_range?
|
382
|
-
execute("git #{git_directory_params} -c diff.renames=0 -c diff.renameLimit=1000 diff -M -C -c --name-only --ignore-submodules=all --diff-filter=AM #{encoding_opt} #{default_params} #{commit_range.to_s}") do |result|
|
383
|
-
filter_files(result.to_s.split(/\n/))
|
384
|
-
end
|
385
|
-
else
|
386
|
-
submodules = current_submodules
|
387
|
-
execute("git #{git_directory_params} ls-tree -r #{commit_range.to_s} --name-only") do |result|
|
388
|
-
filter_files(result.to_s.split(/\n/).select { |f| !submodules.index(f) })
|
389
|
-
end
|
390
|
-
end
|
391
|
-
end
|
392
|
-
|
393
|
-
def default_params
|
394
|
-
"--date=local"
|
395
|
-
end
|
396
|
-
|
397
|
-
def git_directory_params
|
398
|
-
"--git-dir='#{@git_dir}' --work-tree='#{@repository}'"
|
399
|
-
end
|
7
|
+
attribute? :log_level, Types::Coercible::Symbol.default(:info).enum(:debug, :info, :warn, :error, :fatal, :unknown)
|
400
8
|
|
401
|
-
|
402
|
-
"--encoding=UTF-8"
|
403
|
-
end
|
404
|
-
|
405
|
-
def filter_files(raw_files)
|
406
|
-
files = remove_excluded_files(raw_files)
|
407
|
-
files = keep_included_files(files)
|
408
|
-
files = files.map { |file| GitFame::FileUnit.new(file) }
|
409
|
-
return files if @extensions.empty?
|
410
|
-
files.select { |file| @extensions.include?(file.extname) }
|
411
|
-
end
|
412
|
-
|
413
|
-
def commit_range
|
414
|
-
CommitRange.new(current_range, @branch)
|
415
|
-
end
|
416
|
-
|
417
|
-
def current_range
|
418
|
-
return @branch if blank?(@after) and blank?(@before)
|
419
|
-
|
420
|
-
if present?(@after) and present?(@before)
|
421
|
-
if end_date < start_date
|
422
|
-
raise Error, "after=#{@after} can't be greater then before=#{@before}"
|
423
|
-
end
|
424
|
-
|
425
|
-
if end_date > end_commit_date and start_date > end_commit_date
|
426
|
-
raise Error, "after=#{@after} and before=#{@before} is set too high, higest is #{end_commit_date}"
|
427
|
-
end
|
428
|
-
|
429
|
-
if end_date < start_commit_date and start_date < start_commit_date
|
430
|
-
raise Error, "after=#{@after} and before=#{@before} is set too low, lowest is #{start_commit_date}"
|
431
|
-
end
|
432
|
-
elsif present?(@after)
|
433
|
-
if start_date > end_commit_date
|
434
|
-
raise Error, "after=#{@after} is set too high, highest is #{end_commit_date}"
|
435
|
-
end
|
436
|
-
elsif present?(@before)
|
437
|
-
if end_date < start_commit_date
|
438
|
-
raise Error, "before=#{@before} is set too low, lowest is #{start_commit_date}"
|
439
|
-
end
|
440
|
-
end
|
441
|
-
|
442
|
-
if present?(@before)
|
443
|
-
if end_date > end_commit_date
|
444
|
-
commit2 = @branch
|
445
|
-
else
|
446
|
-
# Try finding a commit that day
|
447
|
-
commit2 = execute("git #{git_directory_params} rev-list --before='#{@before} 23:59:59' --after='#{@before} 00:00:01' #{default_params} '#{@branch}' | head -1").to_s
|
448
|
-
|
449
|
-
# Otherwise, look for the closest commit
|
450
|
-
if blank?(commit2)
|
451
|
-
commit2 = execute("git #{git_directory_params} rev-list --before='#{@before}' #{default_params} '#{@branch}' | head -1").to_s
|
452
|
-
end
|
453
|
-
end
|
454
|
-
end
|
455
|
-
|
456
|
-
if present?(@after)
|
457
|
-
if start_date < start_commit_date
|
458
|
-
return present?(commit2) ? commit2 : @branch
|
459
|
-
end
|
460
|
-
|
461
|
-
commit1 = execute("git #{git_directory_params} rev-list --before='#{end_of_yesterday(@after)}' #{default_params} '#{@branch}' | head -1").to_s
|
462
|
-
|
463
|
-
# No commit found this early
|
464
|
-
# If NO end date is choosen, just use current branch
|
465
|
-
# Otherwise use specified (@before) as end date
|
466
|
-
if blank?(commit1)
|
467
|
-
return @branch unless @before
|
468
|
-
return commit2
|
469
|
-
end
|
470
|
-
end
|
471
|
-
|
472
|
-
if @after and @before
|
473
|
-
# Nothing found in date span
|
474
|
-
if commit1 == commit2
|
475
|
-
raise Error, "There are no commits between #{@before} and #{@after}"
|
476
|
-
end
|
477
|
-
return [commit1, commit2]
|
478
|
-
end
|
9
|
+
private
|
479
10
|
|
480
|
-
|
481
|
-
|
11
|
+
def say(template, *args)
|
12
|
+
logger.debug(template % args)
|
482
13
|
end
|
483
14
|
|
484
|
-
def
|
485
|
-
|
15
|
+
def logger
|
16
|
+
@logger ||= Logger.new($stdout, level: log_level, progname: self.class.name)
|
486
17
|
end
|
487
|
-
|
488
|
-
def start_commit_date
|
489
|
-
Time.parse(execute("git #{git_directory_params} log #{encoding_opt} --pretty=format:'%cd' #{default_params} #{@branch} | tail -1").to_s)
|
490
|
-
end
|
491
|
-
|
492
|
-
def end_commit_date
|
493
|
-
Time.parse(execute("git #{git_directory_params} log #{encoding_opt} --pretty=format:'%cd' #{default_params} #{@branch} | head -1").to_s)
|
494
|
-
end
|
495
|
-
|
496
|
-
def end_date
|
497
|
-
Time.parse("#{@before} 23:59:59")
|
498
|
-
end
|
499
|
-
|
500
|
-
def start_date
|
501
|
-
Time.parse("#{@after} 00:00:01")
|
502
|
-
end
|
503
|
-
|
504
|
-
# Removes files excluded by the user
|
505
|
-
# Defined using --exclude
|
506
|
-
def remove_excluded_files(files)
|
507
|
-
return files if @exclude.empty?
|
508
|
-
files.reject do |file|
|
509
|
-
@exclude.any? { |exclude| File.fnmatch(exclude, file) }
|
510
|
-
end
|
511
|
-
end
|
512
|
-
|
513
|
-
def keep_included_files(files)
|
514
|
-
return files if @include.empty?
|
515
|
-
files.select do |file|
|
516
|
-
@include.any? { |include| File.fnmatch(include, file) }
|
517
|
-
end
|
518
|
-
end
|
519
|
-
|
520
|
-
def init_progressbar(files_count)
|
521
|
-
SilentProgressbar.new("Git Fame", files_count, (@progressbar and not @verbose))
|
522
|
-
end
|
523
|
-
|
524
|
-
# TODO: Are all these needed?
|
525
|
-
memoize :populate, :run_with_timeout
|
526
|
-
memoize :current_range, :current_files
|
527
|
-
memoize :printable_fields, :files_from_author
|
528
|
-
memoize :raw_fields, :fields, :file_list
|
529
|
-
memoize :end_commit_date, :loc, :commits
|
530
|
-
memoize :start_commit_date, :files, :authors
|
531
|
-
memoize :file_extensions, :used_files
|
532
18
|
end
|
533
19
|
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GitFame
|
4
|
+
class Collector
|
5
|
+
extend Dry::Initializer
|
6
|
+
|
7
|
+
option :filter, type: Filter
|
8
|
+
option :diff, type: Types::Any
|
9
|
+
|
10
|
+
# @return [Collector]
|
11
|
+
def call
|
12
|
+
Result.new(contributions: contributions)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def contributions
|
18
|
+
commits = Hash.new { |h, k| h[k] = Set.new }
|
19
|
+
files = Hash.new { |h, k| h[k] = Set.new }
|
20
|
+
lines = Hash.new(0)
|
21
|
+
names = {}
|
22
|
+
|
23
|
+
diff.each do |change|
|
24
|
+
filter.call(change) do |loc, file, oid, name, email|
|
25
|
+
commits[email].add(oid)
|
26
|
+
files[email].add(file)
|
27
|
+
names[email] = name
|
28
|
+
lines[email] += loc
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
lines.each_key.map do |email|
|
33
|
+
Contribution.new({
|
34
|
+
lines: lines[email],
|
35
|
+
commits: commits[email],
|
36
|
+
files: files[email],
|
37
|
+
author: {
|
38
|
+
name: names[email],
|
39
|
+
email: email
|
40
|
+
}
|
41
|
+
})
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|