git_fame 2.5.3 → 3.0.0
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 +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
|