git_fame 2.5.3 → 3.0.0
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.
- 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 +159 -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 +79 -114
- 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: 8df295a66aac19b5e17698fc1cdc0ec965f42a19bd7cbe588c5ae36f32dd56a2
|
4
|
+
data.tar.gz: 11c9a291204496ff78616f0b26303ca25d54b5983ed00ac6b1406f61760cbe2e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 17fcbad587dbfbc71c485641ea2f1894317c634c26133097de9288d84edcc4afa148a8e5fb3fdc8d57417c24d12d5810be40a380046c7a5d540ecf54f1896354
|
7
|
+
data.tar.gz: c548c641a4ea2096fcb6dd3982effad044eef0d0c7ffeb0209ed12eb04e66391c5c28f798bca9b229450e0ad403775319e24197c5a65092698004fa7fd140bf1
|
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
|