git-author-report 1.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 +7 -0
- data/VERSION +1 -0
- data/bin/git-report +21 -0
- data/bin/git_add_alias_report +11 -0
- data/bin/git_remove_alias_report +6 -0
- data/ext/git_report/Makefile +4 -0
- data/ext/git_report/extconf.rb +17 -0
- data/lib/git/author.rb +77 -0
- data/lib/git/parallel.rb +67 -0
- data/lib/git/report.rb +169 -0
- data/lib/git_report.rb +8 -0
- data/lib/rubygems_plugin.rb +7 -0
- data/lib/version.rb +9 -0
- metadata +57 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 53e61550adce28323504c93920a1c75d8e45d6f85e7afb20235a31a4cc0d4016
|
|
4
|
+
data.tar.gz: 41918e48a7553b4134efde8a642e97ff035c253d1f5a4e96bdf04b742e3c63c1
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: d473d8b1f9adbc2b6ac34265c0e5981780fd87e3870099eef5c23323dc59f836023616e23d2a0d40c7ca6a67e3bb06c32dec9e12368cb04c32a12f8bb7df4c8b
|
|
7
|
+
data.tar.gz: c4740e7a739f9521a5afc000052b9957226d5381a11ff7e7b059384c4726f149d5fd4f7780687e637769400739450585c22a5b051101a170267eff92a140bcff
|
data/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.0.0
|
data/bin/git-report
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: utf-8
|
|
3
|
+
|
|
4
|
+
# git-report runs on whatever Ruby is on PATH -- including the stock macOS
|
|
5
|
+
# system Ruby -- so users don't have to install Ruby. It has no gem
|
|
6
|
+
# dependencies: everything it needs comes from the Ruby standard library, so
|
|
7
|
+
# there is nothing to install and no first-run vendoring step.
|
|
8
|
+
|
|
9
|
+
script_dir = File.expand_path(File.dirname(__FILE__))
|
|
10
|
+
|
|
11
|
+
require File.join(script_dir, '../lib/version.rb')
|
|
12
|
+
|
|
13
|
+
if ['-v', '--version'].include?(ARGV[0])
|
|
14
|
+
puts "git-report #{Git::Report::VERSION}"
|
|
15
|
+
exit 0
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
require File.join(script_dir, '../lib/git_report.rb')
|
|
19
|
+
|
|
20
|
+
git_report = Git::Report.new.retrieve_stats
|
|
21
|
+
puts git_report.stats
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
pushd `dirname $0` > /dev/null
|
|
4
|
+
BIN_DIR=`pwd`
|
|
5
|
+
popd > /dev/null
|
|
6
|
+
|
|
7
|
+
git config --global alias.report "!exec \"$BIN_DIR/git-report\""
|
|
8
|
+
echo '#################################'
|
|
9
|
+
echo '# "git report" has been added #'
|
|
10
|
+
echo '# Run: git report #'
|
|
11
|
+
echo '#################################'
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'mkmf'
|
|
4
|
+
|
|
5
|
+
find_executable('bash')
|
|
6
|
+
find_executable('git')
|
|
7
|
+
find_executable('make')
|
|
8
|
+
|
|
9
|
+
# Trick Rubygems into thinking the generated artifact was compiled
|
|
10
|
+
compile = File.join(Dir.pwd, "git_report.#{RbConfig::CONFIG['DLEXT']}")
|
|
11
|
+
File.write(compile, '')
|
|
12
|
+
|
|
13
|
+
# Install "git report"
|
|
14
|
+
puts `../../bin/git_add_alias_report`
|
|
15
|
+
|
|
16
|
+
# Trick Rubygems into thinking the Makefile was executed
|
|
17
|
+
$makefile_created = true
|
data/lib/git/author.rb
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# keys of report are the authors, the values contains the report data:
|
|
2
|
+
# * commits, +LOC, -LOC, LOC,
|
|
3
|
+
require 'shellwords'
|
|
4
|
+
|
|
5
|
+
module Git
|
|
6
|
+
# Author class - represents a git contributor with their statistics
|
|
7
|
+
class Author
|
|
8
|
+
# name: String - the name of the author
|
|
9
|
+
# emails: Set<String> - a list of email addresses
|
|
10
|
+
# commits: Fixnum - number of commits by this author
|
|
11
|
+
# loc_added: Fixnum - number of lines this author added over time
|
|
12
|
+
# loc_deleted: Fixnum - number of lines this author deleted over time
|
|
13
|
+
# loc: Fixnum - number of lines in the current codebase this author added
|
|
14
|
+
# files: Fixnum - number of files
|
|
15
|
+
attr_reader :name, :emails, :commits, :loc_added, :loc_deleted, :loc, :files
|
|
16
|
+
|
|
17
|
+
#
|
|
18
|
+
def initialize(name:, emails: '', commits: 0)
|
|
19
|
+
@name = name
|
|
20
|
+
@emails = Set.new Array(emails)
|
|
21
|
+
@commits = commits.to_i
|
|
22
|
+
@loc = 0
|
|
23
|
+
@loc_added = 0
|
|
24
|
+
@loc_deleted = 0
|
|
25
|
+
@files = 0
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def merge(other)
|
|
29
|
+
return unless mergable?(other)
|
|
30
|
+
@emails.merge other.emails
|
|
31
|
+
@commits += other.commits
|
|
32
|
+
self
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def mergable?(other)
|
|
36
|
+
name == other.name
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def retrieve_loc_stats
|
|
40
|
+
# Run one git log per email in parallel, returning [added, deleted] per
|
|
41
|
+
# email. The instance-variable accumulation happens afterwards on this
|
|
42
|
+
# single thread, so the parallel blocks never race on @loc_added/@loc_deleted.
|
|
43
|
+
Git::Parallel.pmap(emails) do |email|
|
|
44
|
+
# Escape email to prevent command injection
|
|
45
|
+
escaped_email = Shellwords.escape(email)
|
|
46
|
+
numstat = `git log --author=#{escaped_email} \
|
|
47
|
+
--pretty=tformat: --numstat --no-merges`
|
|
48
|
+
|
|
49
|
+
# Skip if no commits found for this email
|
|
50
|
+
next if numstat.empty?
|
|
51
|
+
|
|
52
|
+
loc = numstat.split(/\n/).map do |numstat_line|
|
|
53
|
+
parts = numstat_line.scan(/\A(.*)\t(.*)\t/)[0]
|
|
54
|
+
parts ? parts.map(&:to_i) : nil
|
|
55
|
+
end.compact
|
|
56
|
+
|
|
57
|
+
# Skip if no valid data
|
|
58
|
+
next if loc.empty?
|
|
59
|
+
|
|
60
|
+
loc.transpose.map { |add_del_loc| add_del_loc.inject(&:+) }
|
|
61
|
+
end.each do |added_deleted|
|
|
62
|
+
next unless added_deleted
|
|
63
|
+
added, deleted = added_deleted
|
|
64
|
+
@loc_added += added if added
|
|
65
|
+
@loc_deleted += deleted if deleted
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def loc=(loc)
|
|
70
|
+
@loc = loc.to_i
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def files=(files)
|
|
74
|
+
@files = files.to_i
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
data/lib/git/parallel.rb
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
module Git
|
|
2
|
+
# Minimal parallel helpers built on the Ruby standard library, replacing the
|
|
3
|
+
# former `pmap` gem. The work these drive is I/O-bound -- every block shells
|
|
4
|
+
# out to git -- so threads give real concurrency even under MRI's GVL: a
|
|
5
|
+
# subprocess spawned by backticks runs outside the lock while other threads
|
|
6
|
+
# proceed. Keeping this in-tree is what lets git-report run on stock Ruby
|
|
7
|
+
# with no gems to install.
|
|
8
|
+
module Parallel
|
|
9
|
+
# Upper bound on worker threads, and therefore on how many git subprocesses
|
|
10
|
+
# run at once. This cap is the whole point: a repository with thousands of
|
|
11
|
+
# authors must NOT spawn a thread (and a `git log`) per author -- that would
|
|
12
|
+
# swamp the machine. 64 matches the historical default of the pmap gem this
|
|
13
|
+
# replaced, so throughput on large repositories is unchanged.
|
|
14
|
+
MAX_THREADS = 64
|
|
15
|
+
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
# Like Enumerable#map, but runs the block for each element across a bounded
|
|
19
|
+
# pool of threads and returns the results in the original order.
|
|
20
|
+
def pmap(enum, &block)
|
|
21
|
+
results = []
|
|
22
|
+
mutex = Mutex.new
|
|
23
|
+
process(enum) do |item, index|
|
|
24
|
+
value = block.call(item)
|
|
25
|
+
mutex.synchronize { results[index] = value }
|
|
26
|
+
end
|
|
27
|
+
results
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Like Enumerable#each, but runs the block for each element across a bounded
|
|
31
|
+
# pool of threads. Returns the original enumerable once all work is done.
|
|
32
|
+
def peach(enum, &block)
|
|
33
|
+
process(enum) { |item, _index| block.call(item) }
|
|
34
|
+
enum
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Drains every [item, index] pair through at most MAX_THREADS workers. All
|
|
38
|
+
# jobs are enqueued up front, so each worker simply pops until the queue is
|
|
39
|
+
# empty and then exits -- no poison pills needed. Joining via Thread#value
|
|
40
|
+
# propagates the first exception raised in any worker to the caller.
|
|
41
|
+
def process(enum)
|
|
42
|
+
items = enum.to_a
|
|
43
|
+
queue = Queue.new
|
|
44
|
+
items.each_with_index { |item, index| queue << [item, index] }
|
|
45
|
+
|
|
46
|
+
worker_count = [items.size, MAX_THREADS].min
|
|
47
|
+
workers = Array.new(worker_count) do
|
|
48
|
+
Thread.new do
|
|
49
|
+
# We re-raise from Thread#value below, so silence Ruby's automatic
|
|
50
|
+
# "terminated with exception" dump to stderr -- it would otherwise
|
|
51
|
+
# print the same error twice.
|
|
52
|
+
Thread.current.report_on_exception = false
|
|
53
|
+
loop do
|
|
54
|
+
begin
|
|
55
|
+
item, index = queue.pop(true)
|
|
56
|
+
rescue ThreadError
|
|
57
|
+
break # queue drained
|
|
58
|
+
end
|
|
59
|
+
yield item, index
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
workers.each(&:value)
|
|
64
|
+
end
|
|
65
|
+
private_class_method :process
|
|
66
|
+
end
|
|
67
|
+
end
|
data/lib/git/report.rb
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# Git report module for analyzing repository statistics
|
|
2
|
+
require 'shellwords'
|
|
3
|
+
|
|
4
|
+
module Git
|
|
5
|
+
# Report class - generates statistics about git repository contributors
|
|
6
|
+
# Analyzes commits, lines of code, and file changes per author
|
|
7
|
+
class Report
|
|
8
|
+
attr_reader :authors
|
|
9
|
+
|
|
10
|
+
NAME = /\A[^(]*\((.*)\s*\d{4}-\d\d-\d\d \d\d:\d\d:\d\d [+-]\d{4}\s+\d+\)/
|
|
11
|
+
COMMITS_NAME_EMAIL = /\A\s*(\d*)\t(.*) <(.*)>/
|
|
12
|
+
HEADER = {
|
|
13
|
+
name: 'Name',
|
|
14
|
+
loc: 'LOC',
|
|
15
|
+
commits: 'Commits',
|
|
16
|
+
files: 'files',
|
|
17
|
+
loc_add: '+LOC',
|
|
18
|
+
loc_del: '-LOC'
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
def initialize
|
|
22
|
+
@authors = []
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def retrieve_stats
|
|
26
|
+
verify_git_repository
|
|
27
|
+
parse_shortlog
|
|
28
|
+
parse_blame
|
|
29
|
+
self
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def parse_shortlog
|
|
35
|
+
# First pass: get all authors (including those with only merges).
|
|
36
|
+
# HEAD is required: without an explicit revision and with a non-tty
|
|
37
|
+
# stdin (as when run via the `git report` alias) git shortlog reads the
|
|
38
|
+
# commit list from stdin and would otherwise return nothing.
|
|
39
|
+
`git shortlog -se HEAD`.split(/\n/).map do |shortlog_line|
|
|
40
|
+
add shortlog_line, commits: 0
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Second pass: get actual commit counts (excluding merges)
|
|
44
|
+
`git shortlog -se --no-merges HEAD`.split(/\n/).map do |shortlog_line|
|
|
45
|
+
add shortlog_line
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
Git::Parallel.peach(authors, &:retrieve_loc_stats)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def parse_blame
|
|
52
|
+
ls_files = `git ls-files`.split(/\n/)
|
|
53
|
+
ignore_files = `git status --porcelain`.split(/\n/)
|
|
54
|
+
ignore_files.map! { |file| file[3..-1] }
|
|
55
|
+
ls_files -= ignore_files
|
|
56
|
+
|
|
57
|
+
file_lists = ls_files.each_slice((ls_files.count**0.5).ceil).to_a
|
|
58
|
+
names_loc_count = Hash.new(0)
|
|
59
|
+
names_files = {}
|
|
60
|
+
|
|
61
|
+
Git::Parallel.peach(file_lists) do |files|
|
|
62
|
+
files.each do |file|
|
|
63
|
+
escaped_file = Shellwords.escape(file)
|
|
64
|
+
blame = `git blame -w #{escaped_file} 2> /dev/null`
|
|
65
|
+
next if blame.empty?
|
|
66
|
+
blame.unpack('C*').pack('C*').split(/\n/).map do |line|
|
|
67
|
+
name = line.match(NAME)[1].strip.force_encoding('UTF-8')
|
|
68
|
+
names_loc_count[name] += 1
|
|
69
|
+
names_files[name] ||= Set.new
|
|
70
|
+
names_files[name] << file
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
# find_author can be nil for lines git attributes to someone outside the
|
|
75
|
+
# shortlog history (e.g. "Not Committed Yet" for staged-but-uncommitted
|
|
76
|
+
# changes), so skip those rather than crash.
|
|
77
|
+
names_loc_count.each { |name, loc| find_author(name)&.loc = loc }
|
|
78
|
+
names_files.each { |name, files| find_author(name)&.files = files.size }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def add(line, opts = {})
|
|
82
|
+
shortlog_line = format_shortlog_line(*line.scan(COMMITS_NAME_EMAIL)[0])
|
|
83
|
+
shortlog_line[:commits] = opts[:commits] if opts[:commits]
|
|
84
|
+
candidate = Author.new(**shortlog_line)
|
|
85
|
+
@authors << candidate unless add_author candidate
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def add_author(candidate)
|
|
89
|
+
merged = false
|
|
90
|
+
@authors.each do |author|
|
|
91
|
+
if author.mergable?(candidate)
|
|
92
|
+
author.merge(candidate)
|
|
93
|
+
merged = true
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
merged
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def find_author(name)
|
|
100
|
+
@authors.find { |author| author.name == name }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def stats
|
|
104
|
+
authors.sort! { |this, other| other.name <=> this.name }
|
|
105
|
+
authors.sort! { |this, other| other.files <=> this.files }
|
|
106
|
+
authors.sort! { |this, other| other.loc <=> this.loc }
|
|
107
|
+
|
|
108
|
+
name_length = authors.map { |auth| auth.name.length }.max
|
|
109
|
+
loc_length = authors.map { |auth| auth.loc.to_s.length }.max
|
|
110
|
+
commits_length = authors.map { |auth| auth.commits.to_s.length }.max
|
|
111
|
+
files_length = authors.map { |auth| auth.files.to_s.length }.max
|
|
112
|
+
loc_add_length = authors.map { |auth| auth.loc_added.to_s.length }.max
|
|
113
|
+
loc_del_length = authors.map { |auth| auth.loc_deleted.to_s.length }.max
|
|
114
|
+
|
|
115
|
+
name_length = [name_length, HEADER[:name].length].max
|
|
116
|
+
loc_length = [loc_length, HEADER[:loc].length].max
|
|
117
|
+
commits_length = [commits_length, HEADER[:commits].length].max
|
|
118
|
+
files_length = [files_length, HEADER[:files].length].max
|
|
119
|
+
loc_add_length = [loc_add_length, HEADER[:loc_add].length].max
|
|
120
|
+
loc_del_length = [loc_del_length, HEADER[:loc_del].length].max
|
|
121
|
+
|
|
122
|
+
lines = []
|
|
123
|
+
hr = '+' + ('-' * name_length) + '--'
|
|
124
|
+
hr += '+' + ('-' * loc_length) + '--'
|
|
125
|
+
hr += '+' + ('-' * commits_length) + '--'
|
|
126
|
+
hr += '+' + ('-' * files_length) + '--'
|
|
127
|
+
hr += '+' + ('-' * loc_add_length) + '--'
|
|
128
|
+
hr += '+' + ('-' * loc_del_length) + '--+'
|
|
129
|
+
|
|
130
|
+
head = "| #{HEADER[:name].ljust(name_length)} "
|
|
131
|
+
head += "| #{HEADER[:loc].rjust(loc_length)} "
|
|
132
|
+
head += "| #{HEADER[:commits].rjust(commits_length)} "
|
|
133
|
+
head += "| #{HEADER[:files].rjust(files_length)} "
|
|
134
|
+
head += "| #{HEADER[:loc_add].rjust(loc_add_length)} "
|
|
135
|
+
head += "| #{HEADER[:loc_del].to_s.rjust(loc_del_length)} |"
|
|
136
|
+
|
|
137
|
+
lines << hr
|
|
138
|
+
lines << head
|
|
139
|
+
lines << hr
|
|
140
|
+
authors.each do |author|
|
|
141
|
+
# Skip authors with no contributions
|
|
142
|
+
next if [author.loc, author.commits, author.files, author.loc_added,
|
|
143
|
+
author.loc_deleted].all?(&:zero?)
|
|
144
|
+
|
|
145
|
+
line = "| #{author.name.unicode_normalize.ljust(name_length)} "
|
|
146
|
+
line += "| #{author.loc.to_s.rjust(loc_length)} "
|
|
147
|
+
line += "| #{author.commits.to_s.rjust(commits_length)} "
|
|
148
|
+
line += "| #{author.files.to_s.rjust(files_length)} "
|
|
149
|
+
line += "| #{author.loc_added.to_s.rjust(loc_add_length)} "
|
|
150
|
+
line += "| #{author.loc_deleted.to_s.rjust(loc_del_length)} |"
|
|
151
|
+
lines << line
|
|
152
|
+
end
|
|
153
|
+
lines << hr
|
|
154
|
+
lines
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
private
|
|
158
|
+
|
|
159
|
+
def verify_git_repository
|
|
160
|
+
unless system('git rev-parse --git-dir > /dev/null 2>&1')
|
|
161
|
+
raise "Not a git repository (or any of the parent directories)"
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def format_shortlog_line(commits, name, email)
|
|
166
|
+
{ name: name, emails: email, commits: commits.to_i }
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
data/lib/git_report.rb
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
require 'set'
|
|
2
|
+
|
|
3
|
+
# Git module - contains classes for analyzing git repository statistics
|
|
4
|
+
module Git
|
|
5
|
+
# Auto-load all classes in the git subdirectory (sorted for deterministic
|
|
6
|
+
# load order across platforms)
|
|
7
|
+
Dir[File.join(File.dirname(__FILE__), 'git', '**', '*.rb')].sort.each(&method(:require))
|
|
8
|
+
end
|
data/lib/version.rb
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Git
|
|
4
|
+
# Report carries the gem version, read from the VERSION file at the repo root.
|
|
5
|
+
class Report
|
|
6
|
+
VERSION_FILE = File.expand_path('../VERSION', __dir__)
|
|
7
|
+
VERSION = File.exist?(VERSION_FILE) ? File.read(VERSION_FILE).strip : 'unknown'
|
|
8
|
+
end
|
|
9
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: git-author-report
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Wolfgang Teuber
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: git-author-report is a command line tool, run as `git report`, that prints
|
|
13
|
+
a per-author breakdown of code contribution (surviving LOC, lifetime +/-LOC, commits,
|
|
14
|
+
files) as an ASCII table.
|
|
15
|
+
email: knugie@gmx.net
|
|
16
|
+
executables:
|
|
17
|
+
- git-report
|
|
18
|
+
extensions:
|
|
19
|
+
- ext/git_report/extconf.rb
|
|
20
|
+
extra_rdoc_files: []
|
|
21
|
+
files:
|
|
22
|
+
- VERSION
|
|
23
|
+
- bin/git-report
|
|
24
|
+
- bin/git_add_alias_report
|
|
25
|
+
- bin/git_remove_alias_report
|
|
26
|
+
- ext/git_report/Makefile
|
|
27
|
+
- ext/git_report/extconf.rb
|
|
28
|
+
- lib/git/author.rb
|
|
29
|
+
- lib/git/parallel.rb
|
|
30
|
+
- lib/git/report.rb
|
|
31
|
+
- lib/git_report.rb
|
|
32
|
+
- lib/rubygems_plugin.rb
|
|
33
|
+
- lib/version.rb
|
|
34
|
+
homepage: https://github.com/wteuber/git-author-report
|
|
35
|
+
licenses:
|
|
36
|
+
- MIT
|
|
37
|
+
metadata:
|
|
38
|
+
source_code_uri: https://github.com/wteuber/git-author-report
|
|
39
|
+
rubygems_mfa_required: 'true'
|
|
40
|
+
rdoc_options: []
|
|
41
|
+
require_paths:
|
|
42
|
+
- lib
|
|
43
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '2.6'
|
|
48
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - ">="
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '0'
|
|
53
|
+
requirements: []
|
|
54
|
+
rubygems_version: 4.0.10
|
|
55
|
+
specification_version: 4
|
|
56
|
+
summary: Per-author contribution report for any git repository
|
|
57
|
+
test_files: []
|