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
@@ -0,0 +1,159 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/enumerable"
|
4
|
+
require "tty-option"
|
5
|
+
require "tty-spinner"
|
6
|
+
|
7
|
+
module GitFame
|
8
|
+
class Command
|
9
|
+
include TTY::Option
|
10
|
+
using Extension
|
11
|
+
|
12
|
+
usage do
|
13
|
+
program "git"
|
14
|
+
command "fame"
|
15
|
+
desc "GitFame is a tool to generate a contributor list from git history"
|
16
|
+
example "Include commits made since 2010", "git fame --after 2010-01-01"
|
17
|
+
example "Include commits made before 2015", "git fame --before 2015-01-01"
|
18
|
+
example "Include commits made since 2010 and before 2015", "git fame --after 2010-01-01 --before 2015-01-01"
|
19
|
+
example "Only changes made to the main branch", "git fame --branch main"
|
20
|
+
example "Only ruby and javascript files", "git fame --extensions .rb .js"
|
21
|
+
example "Exclude spec files and the README", "git fame --exclude */**/*_spec.rb README.md"
|
22
|
+
example "Only spec files and markdown files", "git fame --include */**/*_spec.rb */**/*.md"
|
23
|
+
example "A parent directory of the current directory", "git fame ../other/git/repo"
|
24
|
+
end
|
25
|
+
|
26
|
+
option :log_level do
|
27
|
+
permit ["debug", "info", "warn", "error", "fatal"]
|
28
|
+
long "--log-level [LEVEL]"
|
29
|
+
desc "Log level"
|
30
|
+
end
|
31
|
+
|
32
|
+
option :exclude do
|
33
|
+
desc "Exclude files matching the given glob pattern"
|
34
|
+
long "--exclude [GLOB]"
|
35
|
+
arity zero_or_more
|
36
|
+
short "-E [BLOB]"
|
37
|
+
convert :list
|
38
|
+
end
|
39
|
+
|
40
|
+
option :include do
|
41
|
+
desc "Include files matching the given glob pattern"
|
42
|
+
long "--include [GLOB]"
|
43
|
+
arity zero_or_more
|
44
|
+
short "-I [BLOB]"
|
45
|
+
convert :list
|
46
|
+
end
|
47
|
+
|
48
|
+
option :extensions do
|
49
|
+
desc "File extensions to be included starting with a period"
|
50
|
+
arity zero_or_more
|
51
|
+
long "--extensions [EXT]"
|
52
|
+
short "-ex [EXT]"
|
53
|
+
convert :list
|
54
|
+
|
55
|
+
validate -> input do
|
56
|
+
input.match(/\.\w+/)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
option :before do
|
61
|
+
desc "Only changes made after this date"
|
62
|
+
long "--before [DATE]"
|
63
|
+
short "-B [DATE]"
|
64
|
+
validate -> input do
|
65
|
+
Types::Params::DateTime.valid?(input)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
option :after do
|
70
|
+
desc "Only changes made before this date"
|
71
|
+
long "--after [DATE]"
|
72
|
+
short "-A [DATE]"
|
73
|
+
|
74
|
+
validate -> input do
|
75
|
+
Types::Params::DateTime.valid?(input)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
argument :path do
|
80
|
+
desc "Path or sub path to the git repository"
|
81
|
+
default { Dir.pwd }
|
82
|
+
optional
|
83
|
+
|
84
|
+
validate -> path do
|
85
|
+
File.directory?(path)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
option :branch do
|
90
|
+
desc "Branch to be used as starting point"
|
91
|
+
long "--branch [NAME]"
|
92
|
+
default "HEAD"
|
93
|
+
end
|
94
|
+
|
95
|
+
flag :help do
|
96
|
+
desc "Print usage"
|
97
|
+
long "--help"
|
98
|
+
short "-h"
|
99
|
+
end
|
100
|
+
|
101
|
+
def self.call(argv = ARGV)
|
102
|
+
cmd = new
|
103
|
+
cmd.parse(argv, raise_on_parse_error: true)
|
104
|
+
cmd.run
|
105
|
+
rescue TTY::Option::Error => e
|
106
|
+
abort e.message
|
107
|
+
end
|
108
|
+
|
109
|
+
def run
|
110
|
+
if params[:help]
|
111
|
+
abort help
|
112
|
+
end
|
113
|
+
|
114
|
+
thread = spinner.run do
|
115
|
+
Render.new(result: result, **options(:branch))
|
116
|
+
end
|
117
|
+
|
118
|
+
thread.value.call
|
119
|
+
rescue Dry::Struct::Error => e
|
120
|
+
abort e.message
|
121
|
+
rescue Interrupt
|
122
|
+
exit
|
123
|
+
end
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
def filter
|
128
|
+
Filter.new(**params.to_h.compact_blank.except(:branch))
|
129
|
+
end
|
130
|
+
|
131
|
+
def spinner
|
132
|
+
@spinner ||= TTY::Spinner.new("[:spinner] git-fame is crunching the numbers, hold on ...", interval: 1)
|
133
|
+
end
|
134
|
+
|
135
|
+
def repo
|
136
|
+
Rugged::Repository.discover(params[:path])
|
137
|
+
end
|
138
|
+
|
139
|
+
def collector
|
140
|
+
Collector.new(filter: filter, diff: diff, **options)
|
141
|
+
end
|
142
|
+
|
143
|
+
def diff
|
144
|
+
Diff.new(commit: commit, **options)
|
145
|
+
end
|
146
|
+
|
147
|
+
def options(*args)
|
148
|
+
params.to_h.only(*args, :log_level).compact_blank
|
149
|
+
end
|
150
|
+
|
151
|
+
def commit
|
152
|
+
repo.rev_parse(params[:branch])
|
153
|
+
end
|
154
|
+
|
155
|
+
def result
|
156
|
+
collector.call
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GitFame
|
4
|
+
class Contribution < Base
|
5
|
+
attribute :lines, Types::Integer
|
6
|
+
attribute :commits, Types::Set
|
7
|
+
attribute :files, Types::Set
|
8
|
+
attribute :author, Author
|
9
|
+
|
10
|
+
delegate :name, :email, to: :author
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GitFame
|
4
|
+
class Diff < Base
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
attribute :commit, Types::Any
|
8
|
+
delegate :tree, to: :commit
|
9
|
+
delegate :repo, to: :tree
|
10
|
+
|
11
|
+
# @yield [Hash]
|
12
|
+
#
|
13
|
+
# @return [void]
|
14
|
+
def each(&block)
|
15
|
+
tree.walk(:preorder).each do |root, entry|
|
16
|
+
case entry
|
17
|
+
in { type: :blob, name: file, oid: }
|
18
|
+
Rugged::Blame.new(repo, root + file, newest_commit: commit).each(&block)
|
19
|
+
in { type: type, name: file }
|
20
|
+
say("Ignore type [%s] in for %s", type, root + file)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GitFame
|
4
|
+
class Filter < Base
|
5
|
+
OPT = File::FNM_EXTGLOB | File::FNM_DOTMATCH | File::FNM_CASEFOLD | File::FNM_PATHNAME
|
6
|
+
|
7
|
+
attribute? :before, Types::JSON::DateTime
|
8
|
+
attribute? :after, Types::JSON::DateTime
|
9
|
+
attribute? :extensions, Types::Set
|
10
|
+
attribute? :include, Types::Set
|
11
|
+
attribute? :exclude, Types::Set
|
12
|
+
|
13
|
+
schema schema.strict(false)
|
14
|
+
|
15
|
+
# Invokes block if hunk is valid
|
16
|
+
#
|
17
|
+
# @param hunk [Hash]
|
18
|
+
#
|
19
|
+
# @yieldparam lines [Integer]
|
20
|
+
# @yieldparam orig_path [Pathname]
|
21
|
+
# @yieldparam oid [String]
|
22
|
+
# @yieldparam name [String]
|
23
|
+
# @yieldparam email [String]
|
24
|
+
#
|
25
|
+
# @return [void]
|
26
|
+
def call(hunk, &block)
|
27
|
+
case [hunk, attributes]
|
28
|
+
in [{ orig_path: path, final_signature: { time: created_at } }, { after: }] unless created_at > after
|
29
|
+
say("File %s ignored due to [created > after] (%p > %p)", path, created_at, after)
|
30
|
+
in [{ orig_path: path, final_signature: { time: created_at } }, { before: }] unless created_at < before
|
31
|
+
say("File %s ignored due to [created < before] (%p < %p)", path, created_at, before)
|
32
|
+
in [{ orig_path: path}, { exclude: excluded }] if excluded.any? { File.fnmatch?(_1, path, OPT) }
|
33
|
+
say("File %s excluded by [exclude] (%p)", path, excluded)
|
34
|
+
in [{ orig_path: path }, { include: included }] unless included.any? { File.fnmatch?(_1, path, OPT) }
|
35
|
+
say("File %s excluded by [include] (%p)", path, included)
|
36
|
+
in [{ orig_path: path }, { extensions: }] unless extensions.any? { File.extname(path) == _1 }
|
37
|
+
say("File %s excluded by [extensions] (%p)", path, extensions)
|
38
|
+
in [{final_signature: { name:, email:}, final_commit_id: oid, lines_in_hunk: lines, orig_path: path}, Hash]
|
39
|
+
block[lines, path, oid, name, email]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/dependencies/autoload"
|
4
|
+
require "active_support/number_helper"
|
5
|
+
|
6
|
+
module GitFame
|
7
|
+
class Render
|
8
|
+
module Extension
|
9
|
+
refine Integer do
|
10
|
+
def f
|
11
|
+
ActiveSupport::NumberHelper.number_to_delimited(self, delimiter: " ")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
refine Contribution do
|
16
|
+
def dist(result)
|
17
|
+
l = lines.to_f / result.lines
|
18
|
+
c = commits.count.to_f / result.commits.count
|
19
|
+
f = files.count.to_f / result.files.count
|
20
|
+
|
21
|
+
"%0.1f%% / %0.1f%% / %0.1f%%" % [l * 100, c * 100, f * 100]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "tty-screen"
|
4
|
+
require "tty-table"
|
5
|
+
require "tty-box"
|
6
|
+
require "erb"
|
7
|
+
|
8
|
+
module GitFame
|
9
|
+
class Render < Base
|
10
|
+
FIELDS = [:name, :email, :lines, :commits, :files, :dist].map(&:to_s).freeze
|
11
|
+
|
12
|
+
attribute :branch, Types::String
|
13
|
+
attribute :result, Result
|
14
|
+
delegate_missing_to :result
|
15
|
+
|
16
|
+
using Extension
|
17
|
+
|
18
|
+
# Renders to stdout
|
19
|
+
#
|
20
|
+
# @return [void]
|
21
|
+
def call
|
22
|
+
table = TTY::Table.new(header: FIELDS)
|
23
|
+
width = TTY::Screen.width
|
24
|
+
|
25
|
+
contributions.reverse_each do |c|
|
26
|
+
table << [c.name, c.email, c.lines.f, c.commits.count.f, c.files.count.f, c.dist(self)]
|
27
|
+
end
|
28
|
+
|
29
|
+
print table.render(:unicode, width: width, resize: true, alignment: [:center])
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def contributions
|
35
|
+
result.contributions.sort_by(&:lines)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/git_fame/result.rb
CHANGED
@@ -1,6 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module GitFame
|
2
|
-
class Result <
|
3
|
-
|
4
|
-
|
4
|
+
class Result < Base
|
5
|
+
attribute :contributions, Types.Array(Contribution)
|
6
|
+
|
7
|
+
# @return [Array<Author>]
|
8
|
+
def authors
|
9
|
+
contributions.map(&:author)
|
10
|
+
end
|
11
|
+
|
12
|
+
# @return [Array<String>]
|
13
|
+
def commits
|
14
|
+
contributions.flat_map do |c|
|
15
|
+
c.commits.to_a
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# @return [Array<String>]
|
20
|
+
def files
|
21
|
+
contributions.flat_map do |c|
|
22
|
+
c.files.to_a
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# @return [Integer]
|
27
|
+
def lines
|
28
|
+
contributions.sum(&:lines)
|
29
|
+
end
|
5
30
|
end
|
6
|
-
end
|
31
|
+
end
|
data/lib/git_fame/version.rb
CHANGED
data/lib/git_fame.rb
CHANGED
@@ -1,4 +1,17 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "
|
4
|
-
require "
|
3
|
+
require "active_support/core_ext/module/delegation"
|
4
|
+
require "active_support/isolated_execution_state"
|
5
|
+
require "active_support/core_ext/numeric/time"
|
6
|
+
require "dry/core/memoizable"
|
7
|
+
require "dry/initializer"
|
8
|
+
require "dry/struct"
|
9
|
+
require "dry/types"
|
10
|
+
require "neatjson"
|
11
|
+
require "zeitwerk"
|
12
|
+
require "pathname"
|
13
|
+
require "rugged"
|
14
|
+
|
15
|
+
module GitFame
|
16
|
+
Zeitwerk::Loader.for_gem.tap(&:setup)
|
17
|
+
end
|