nova_git_stats 2.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/git_stats +6 -0
- data/config/locales/bg.yml +63 -0
- data/config/locales/bg_default.yml +70 -0
- data/config/locales/de.yml +63 -0
- data/config/locales/de_default.yml +224 -0
- data/config/locales/en.yml +63 -0
- data/config/locales/es.yml +63 -0
- data/config/locales/es_deafult.yml +70 -0
- data/config/locales/pl.yml +63 -0
- data/config/locales/pl_default.yml +224 -0
- data/config/locales/tr.yml +63 -0
- data/config/locales/tr_default.yml +70 -0
- data/config/locales/zh_cn.yml +65 -0
- data/config/locales/zh_cn_default.yml +70 -0
- data/config/locales/zh_tw.yml +63 -0
- data/config/locales/zh_tw_default.yml +76 -0
- data/lib/git_stats/base.rb +28 -0
- data/lib/git_stats/cli.rb +35 -0
- data/lib/git_stats/command_parser.rb +30 -0
- data/lib/git_stats/command_runner.rb +13 -0
- data/lib/git_stats/core_extensions/enumerable.rb +8 -0
- data/lib/git_stats/core_extensions/hash.rb +24 -0
- data/lib/git_stats/core_extensions/symbol.rb +5 -0
- data/lib/git_stats/generator.rb +38 -0
- data/lib/git_stats/git_data/activity.rb +76 -0
- data/lib/git_stats/git_data/author.rb +77 -0
- data/lib/git_stats/git_data/blob.rb +41 -0
- data/lib/git_stats/git_data/comment_stat.rb +38 -0
- data/lib/git_stats/git_data/commit.rb +76 -0
- data/lib/git_stats/git_data/repo.rb +172 -0
- data/lib/git_stats/git_data/short_stat.rb +29 -0
- data/lib/git_stats/git_data/tree.rb +21 -0
- data/lib/git_stats/hash_initializable.rb +9 -0
- data/lib/git_stats/i18n.rb +2 -0
- data/lib/git_stats/inspector.rb +52 -0
- data/lib/git_stats/stats_view/charts/activity_charts.rb +69 -0
- data/lib/git_stats/stats_view/charts/authors_charts.rb +49 -0
- data/lib/git_stats/stats_view/charts/chart.rb +126 -0
- data/lib/git_stats/stats_view/charts/charts.rb +36 -0
- data/lib/git_stats/stats_view/charts/repo_charts.rb +61 -0
- data/lib/git_stats/stats_view/template.rb +21 -0
- data/lib/git_stats/stats_view/view.rb +89 -0
- data/lib/git_stats/stats_view/view_data.rb +28 -0
- data/lib/git_stats/version.rb +7 -0
- data/lib/git_stats.rb +14 -0
- data/templates/activity/_activity.haml +102 -0
- data/templates/activity/by_date.haml +1 -0
- data/templates/activity/day_of_week.haml +1 -0
- data/templates/activity/hour_of_day.haml +1 -0
- data/templates/activity/hour_of_week.haml +1 -0
- data/templates/activity/month_of_year.haml +1 -0
- data/templates/activity/year.haml +1 -0
- data/templates/activity/year_month.haml +1 -0
- data/templates/assets/bootstrap/css/bootstrap-3.3.7.min.css +6 -0
- data/templates/assets/bootstrap/css/bootstrap-theme-3.3.7.min.css +6 -0
- data/templates/assets/bootstrap/css/bootstrap-theme.min.css.map +1 -0
- data/templates/assets/bootstrap/css/bootstrap.min.css.map +1 -0
- data/templates/assets/bootstrap/js/bootstrap-3.3.7.min.js +7 -0
- data/templates/assets/bootstrap-4.6.0-dist/css/bootstrap-grid.css +3872 -0
- data/templates/assets/bootstrap-4.6.0-dist/css/bootstrap-grid.css.map +1 -0
- data/templates/assets/bootstrap-4.6.0-dist/css/bootstrap-grid.min.css +7 -0
- data/templates/assets/bootstrap-4.6.0-dist/css/bootstrap-grid.min.css.map +1 -0
- data/templates/assets/bootstrap-4.6.0-dist/css/bootstrap-reboot.css +325 -0
- data/templates/assets/bootstrap-4.6.0-dist/css/bootstrap-reboot.css.map +1 -0
- data/templates/assets/bootstrap-4.6.0-dist/css/bootstrap-reboot.min.css +8 -0
- data/templates/assets/bootstrap-4.6.0-dist/css/bootstrap-reboot.min.css.map +1 -0
- data/templates/assets/bootstrap-4.6.0-dist/css/bootstrap.css +10298 -0
- data/templates/assets/bootstrap-4.6.0-dist/css/bootstrap.css.map +1 -0
- data/templates/assets/bootstrap-4.6.0-dist/css/bootstrap.min.css +7 -0
- data/templates/assets/bootstrap-4.6.0-dist/css/bootstrap.min.css.map +1 -0
- data/templates/assets/bootstrap-4.6.0-dist/js/bootstrap.bundle.js +7045 -0
- data/templates/assets/bootstrap-4.6.0-dist/js/bootstrap.bundle.js.map +1 -0
- data/templates/assets/bootstrap-4.6.0-dist/js/bootstrap.bundle.min.js +7 -0
- data/templates/assets/bootstrap-4.6.0-dist/js/bootstrap.bundle.min.js.map +1 -0
- data/templates/assets/bootstrap-4.6.0-dist/js/bootstrap.js +4432 -0
- data/templates/assets/bootstrap-4.6.0-dist/js/bootstrap.js.map +1 -0
- data/templates/assets/bootstrap-4.6.0-dist/js/bootstrap.min.js +7 -0
- data/templates/assets/bootstrap-4.6.0-dist/js/bootstrap.min.js.map +1 -0
- data/templates/assets/export-data.js +17 -0
- data/templates/assets/exporting.js +27 -0
- data/templates/assets/highstock.js +525 -0
- data/templates/assets/jquery-3.6.0.min.js +2 -0
- data/templates/assets/menu.css +28 -0
- data/templates/author_details/_author_details.haml +31 -0
- data/templates/author_details/changed_lines_by_date.haml +1 -0
- data/templates/author_details/commits_by_date.haml +1 -0
- data/templates/author_details/deletions_by_date.haml +1 -0
- data/templates/author_details/insertions_by_date.haml +1 -0
- data/templates/authors/_authors.haml +72 -0
- data/templates/authors/best_authors.haml +1 -0
- data/templates/authors/changed_lines_by_author_by_date.haml +1 -0
- data/templates/authors/commits_sum_by_author_by_date.haml +1 -0
- data/templates/authors/deletions_by_author_by_date.haml +1 -0
- data/templates/authors/insertions_by_author_by_date.haml +1 -0
- data/templates/comments/_comments.haml +11 -0
- data/templates/comments/by_date.haml +1 -0
- data/templates/files/_files.haml +16 -0
- data/templates/files/by_date.haml +1 -0
- data/templates/files/by_extension.haml +1 -0
- data/templates/general.haml +31 -0
- data/templates/layout.haml +32 -0
- data/templates/lines/_lines.haml +16 -0
- data/templates/lines/by_date.haml +1 -0
- data/templates/lines/by_extension.haml +1 -0
- data/templates/static/index.html +5 -0
- metadata +262 -0
@@ -0,0 +1,30 @@
|
|
1
|
+
module GitStats
|
2
|
+
class CommandParser
|
3
|
+
def parse(command, result)
|
4
|
+
cmd, params = command.scan(/git (.*) (.*)/).first.map(&:split).flatten
|
5
|
+
# TODO: params is not needed?
|
6
|
+
send("parse_#{cmd.underscore}", result, params)
|
7
|
+
end
|
8
|
+
|
9
|
+
def parse_shortlog(result, _params)
|
10
|
+
result.lines.map do |line|
|
11
|
+
commits, name, email = line.scan(/(.*)\t(.*)<(.*)>/).first.map(&:strip)
|
12
|
+
{commits: commits.to_i, name: name, email: email}
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def parse_ls_tree(result, _params)
|
17
|
+
result.lines.map do |line|
|
18
|
+
mode, type, sha, filename = line.scan(/(.*) (.*) (.*)\t(.*)/).first.map(&:strip)
|
19
|
+
{mode: mode, type: type, sha: sha, filename: filename}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def parse_rev_list(result, _params)
|
24
|
+
result.lines.map do |line|
|
25
|
+
sha, stamp, date, author_email = line.split('|').map(&:strip)
|
26
|
+
{sha: sha, stamp: stamp, date: date, author_email: author_email}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class Hash
|
2
|
+
def to_key_indexed_array(min_size: 0, default: nil)
|
3
|
+
raise ArgumentError, 'all the keys must be numbers to convert to key indexed array' unless all? { |k, _v| k.is_a? Numeric }
|
4
|
+
|
5
|
+
each_with_object(Array.new(min_size, default)) { |(k, v), acc| acc[k] = v }.map { |e| e || default }
|
6
|
+
end
|
7
|
+
|
8
|
+
def fill_empty_days!(aggregated: true)
|
9
|
+
return self if empty?
|
10
|
+
|
11
|
+
self_with_date_keys = transform_keys(&:to_date)
|
12
|
+
days_with_data = self_with_date_keys.keys.sort.uniq
|
13
|
+
prev = 0
|
14
|
+
|
15
|
+
days_with_data.first.upto(days_with_data.last) do |day|
|
16
|
+
if days_with_data.include?(day)
|
17
|
+
prev = self_with_date_keys[day]
|
18
|
+
else
|
19
|
+
self[day] = aggregated ? prev : 0
|
20
|
+
end
|
21
|
+
end
|
22
|
+
self
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require_relative 'inspector'
|
2
|
+
|
3
|
+
module GitStats
|
4
|
+
class Generator
|
5
|
+
include GitStats::Inspector
|
6
|
+
|
7
|
+
delegate :add_command_observer, to: :@repo
|
8
|
+
delegate :render_all, to: :@view
|
9
|
+
|
10
|
+
attr_reader :path, :out_path
|
11
|
+
|
12
|
+
def initialize(options)
|
13
|
+
@path = validate_repo_path(options[:path])
|
14
|
+
@out_path = File.expand_path(options[:out_path])
|
15
|
+
|
16
|
+
@repo = GitData::Repo.new(options.merge(path: path))
|
17
|
+
view_data = StatsView::ViewData.new(@repo)
|
18
|
+
@view = StatsView::View.new(view_data, out_path)
|
19
|
+
|
20
|
+
yield self if block_given?
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def validate_repo_path(repo_path)
|
26
|
+
raise ArgumentError, '`path` is not specified' unless repo_path
|
27
|
+
|
28
|
+
path = File.expand_path(repo_path)
|
29
|
+
raise ArgumentError, "'#{path}' is not a git repository" unless valid_repo_path?(path)
|
30
|
+
|
31
|
+
path
|
32
|
+
end
|
33
|
+
|
34
|
+
def valid_repo_path?(repo_path)
|
35
|
+
Dir.exist?("#{repo_path}/.git") || File.exist?("#{repo_path}/.git") || File.exist?("#{repo_path}/HEAD")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module GitStats
|
2
|
+
module GitData
|
3
|
+
class Activity
|
4
|
+
def initialize(commits)
|
5
|
+
add_commits(commits)
|
6
|
+
end
|
7
|
+
|
8
|
+
def by_date
|
9
|
+
@by_date ||= default_hash
|
10
|
+
end
|
11
|
+
|
12
|
+
def by_hour
|
13
|
+
@by_hour ||= default_hash
|
14
|
+
end
|
15
|
+
|
16
|
+
def by_hour_array
|
17
|
+
by_hour.to_key_indexed_array(min_size: 24, default: 0)
|
18
|
+
end
|
19
|
+
|
20
|
+
def by_wday
|
21
|
+
@by_wday ||= default_hash
|
22
|
+
end
|
23
|
+
|
24
|
+
def by_wday_array
|
25
|
+
by_wday.to_key_indexed_array(min_size: 7, default: 0)
|
26
|
+
end
|
27
|
+
|
28
|
+
def by_wday_hour
|
29
|
+
@by_wday_hour ||= default_double_hash
|
30
|
+
end
|
31
|
+
|
32
|
+
def by_month
|
33
|
+
@by_month ||= default_hash
|
34
|
+
end
|
35
|
+
|
36
|
+
def by_month_array
|
37
|
+
by_month.to_key_indexed_array(min_size: 13, default: 0)[1..-1]
|
38
|
+
end
|
39
|
+
|
40
|
+
def by_year
|
41
|
+
@by_year ||= default_hash
|
42
|
+
end
|
43
|
+
|
44
|
+
def by_year_month
|
45
|
+
@by_year_month ||= default_double_hash
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def add_commits(commits)
|
51
|
+
commits = commits.values if commits.is_a? Hash
|
52
|
+
commits.each do |commit|
|
53
|
+
add_commit_at(commit.date)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def add_commit_at(date)
|
58
|
+
by_date[date] += 1
|
59
|
+
by_hour[date.hour] += 1
|
60
|
+
by_wday[date.wday] += 1
|
61
|
+
by_wday_hour[date.wday][date.hour] += 1
|
62
|
+
by_month[date.month] += 1
|
63
|
+
by_year[date.year] += 1
|
64
|
+
by_year_month[date.year][date.month] += 1
|
65
|
+
end
|
66
|
+
|
67
|
+
def default_hash
|
68
|
+
Hash.new(0)
|
69
|
+
end
|
70
|
+
|
71
|
+
def default_double_hash
|
72
|
+
Hash.new { |h, k| h[k] = Hash.new(0) }
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require_relative '../hash_initializable'
|
2
|
+
require_relative '../inspector'
|
3
|
+
|
4
|
+
module GitStats
|
5
|
+
module GitData
|
6
|
+
class Author
|
7
|
+
include GitStats::HashInitializable
|
8
|
+
include GitStats::Inspector
|
9
|
+
|
10
|
+
attr_reader :repo, :name, :email
|
11
|
+
|
12
|
+
def commits
|
13
|
+
@commits ||= repo.commits.select { |commit| commit.author == self }
|
14
|
+
end
|
15
|
+
|
16
|
+
def commits_sum
|
17
|
+
commits.size
|
18
|
+
end
|
19
|
+
|
20
|
+
def changed_lines
|
21
|
+
insertions + deletions
|
22
|
+
end
|
23
|
+
|
24
|
+
def insertions
|
25
|
+
short_stats.sum(&:insertions)
|
26
|
+
end
|
27
|
+
|
28
|
+
def deletions
|
29
|
+
short_stats.sum(&:deletions)
|
30
|
+
end
|
31
|
+
|
32
|
+
def commits_sum_by_date
|
33
|
+
sum = 0
|
34
|
+
commits.map do |commit|
|
35
|
+
sum += 1
|
36
|
+
[commit.date, sum]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
[:insertions, :deletions, :changed_lines].each do |method|
|
41
|
+
define_method "total_#{method}_by_date" do
|
42
|
+
sum = 0
|
43
|
+
commits.map do |commit|
|
44
|
+
sum += commit.short_stat.send(method)
|
45
|
+
[commit.date, sum]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
define_method "#{method}_by_date" do
|
50
|
+
commits.group_by { |c| c.date.to_date }.map { |arr| [arr[0], arr[1].sum { |c| c.short_stat.send(method) }] }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def short_stats
|
55
|
+
commits.map(&:short_stat)
|
56
|
+
end
|
57
|
+
|
58
|
+
def activity
|
59
|
+
@activity ||= Activity.new(commits)
|
60
|
+
end
|
61
|
+
|
62
|
+
def dirname
|
63
|
+
name.underscore.split.join '_'
|
64
|
+
end
|
65
|
+
|
66
|
+
def ==(other)
|
67
|
+
[repo, name, email] == [other.repo, other.name, other.email]
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def ivars_to_be_displayed
|
73
|
+
[:@name, :@email]
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require_relative '../hash_initializable'
|
2
|
+
require_relative '../inspector'
|
3
|
+
|
4
|
+
module GitStats
|
5
|
+
module GitData
|
6
|
+
class Blob
|
7
|
+
include GitStats::HashInitializable
|
8
|
+
include GitStats::Inspector
|
9
|
+
|
10
|
+
attr_reader :repo, :sha, :filename
|
11
|
+
|
12
|
+
def lines_count
|
13
|
+
@lines_count ||= binary? ? 0 : repo.run("git cat-file blob #{sha} | wc -l").to_i
|
14
|
+
end
|
15
|
+
|
16
|
+
def content
|
17
|
+
@content ||= repo.run("git cat-file blob #{sha}")
|
18
|
+
end
|
19
|
+
|
20
|
+
def extension
|
21
|
+
@extension ||= File.extname(filename)
|
22
|
+
end
|
23
|
+
|
24
|
+
def binary?
|
25
|
+
repo.run("git cat-file blob #{sha} | grep -m 1 '^'").dup
|
26
|
+
.force_encoding('ISO-8859-1').encode('utf-8', replace: nil)
|
27
|
+
.include?('Binary file')
|
28
|
+
end
|
29
|
+
|
30
|
+
def ==(other)
|
31
|
+
[repo, sha, filename] == [other.repo, other.sha, other.filename]
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def ivars_to_be_displayed
|
37
|
+
[:@sha, :@filename]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module GitStats
|
2
|
+
module GitData
|
3
|
+
class CommentStat
|
4
|
+
attr_reader :commit, :insertions, :deletions
|
5
|
+
|
6
|
+
def initialize(commit)
|
7
|
+
@commit = commit
|
8
|
+
calculate_stat
|
9
|
+
end
|
10
|
+
|
11
|
+
def changed_lines
|
12
|
+
insertions + deletions
|
13
|
+
end
|
14
|
+
|
15
|
+
def escape_characters_in_string(string)
|
16
|
+
pattern = %r{['".*/\\-]}
|
17
|
+
string.gsub(pattern) { |match| "\\#{match}" }
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def calculate_stat
|
23
|
+
escaped_string = escape_characters_in_string(commit.repo.comment_string)
|
24
|
+
command = "git show #{commit.sha} | " \
|
25
|
+
"awk 'BEGIN {adds=0; dels=0} " \
|
26
|
+
"{if ($0 ~ /^\\+#{escaped_string}/) adds++; if ($0 ~ /^\-#{escaped_string}/) dels++} " \
|
27
|
+
"END {print adds \" insertions \" dels \" deletes\"}'"
|
28
|
+
stat_line = commit.repo.run(command).lines.to_a[0]
|
29
|
+
if stat_line.blank?
|
30
|
+
@insertions = @deletions = 0
|
31
|
+
else
|
32
|
+
@insertions = stat_line[/(\d+) insertions?/, 1].to_i
|
33
|
+
@deletions = stat_line[/(\d+) deletes?/, 1].to_i
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require_relative '../hash_initializable'
|
2
|
+
require_relative '../inspector'
|
3
|
+
|
4
|
+
module GitStats
|
5
|
+
module GitData
|
6
|
+
class Commit
|
7
|
+
include GitStats::HashInitializable
|
8
|
+
include GitStats::Inspector
|
9
|
+
|
10
|
+
attr_reader :repo, :sha, :stamp, :date, :author
|
11
|
+
|
12
|
+
def files
|
13
|
+
@files ||= repo.run_and_parse("git ls-tree -r #{sha} -- #{repo.tree_path}").map do |file|
|
14
|
+
Blob.new(repo: repo, filename: file[:filename], sha: file[:sha])
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def binary_files
|
19
|
+
@binary_files ||= files.select(&:binary?)
|
20
|
+
end
|
21
|
+
|
22
|
+
def text_files
|
23
|
+
@text_files ||= files - binary_files
|
24
|
+
end
|
25
|
+
|
26
|
+
def files_by_extension
|
27
|
+
@files_by_extension ||= files.each_with_object({}) do |f, acc|
|
28
|
+
acc[f.extension] ||= []
|
29
|
+
acc[f.extension] << f
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def files_by_extension_count
|
34
|
+
@files_by_extension_count ||= files_by_extension.transform_values(&:count)
|
35
|
+
end
|
36
|
+
|
37
|
+
def lines_by_extension
|
38
|
+
@lines_by_extension ||= files_by_extension.map do |ext, files|
|
39
|
+
next if (lines_count = files.sum(&:lines_count)) == 0
|
40
|
+
|
41
|
+
[ext, lines_count]
|
42
|
+
end.compact.to_h
|
43
|
+
end
|
44
|
+
|
45
|
+
def files_count
|
46
|
+
@files_count ||= repo.run("git ls-tree -r --name-only #{sha} -- #{repo.tree_path}| wc -l").to_i
|
47
|
+
end
|
48
|
+
|
49
|
+
def lines_count
|
50
|
+
command = "git diff --shortstat --no-renames `git hash-object -t tree /dev/null` #{sha} -- #{repo.tree_path}"
|
51
|
+
@lines_count ||= repo.run(command).lines.sum do |line|
|
52
|
+
line[/(\d+) insertions?/, 1].to_i
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def short_stat
|
57
|
+
@short_stat ||= ShortStat.new(self)
|
58
|
+
end
|
59
|
+
|
60
|
+
def comment_stat
|
61
|
+
@comment_stat ||= CommentStat.new(self)
|
62
|
+
end
|
63
|
+
|
64
|
+
def ==(other)
|
65
|
+
[repo, sha, stamp, date, author] ==
|
66
|
+
[other.repo, other.sha, other.stamp, other.date, other.author]
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def ivars_to_be_displayed
|
72
|
+
[:@sha, :@stamp, :@date, :@author]
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
require_relative '../hash_initializable'
|
2
|
+
require_relative '../inspector'
|
3
|
+
|
4
|
+
module GitStats
|
5
|
+
module GitData
|
6
|
+
class Repo
|
7
|
+
include GitStats::HashInitializable
|
8
|
+
include GitStats::Inspector
|
9
|
+
|
10
|
+
delegate :files, :files_by_extension, :files_by_extension_count, :lines_by_extension,
|
11
|
+
:files_count, :binary_files, :text_files, :lines_count, :comments_count, to: :last_commit
|
12
|
+
|
13
|
+
def initialize(params)
|
14
|
+
super(params)
|
15
|
+
@path = File.expand_path(@path)
|
16
|
+
@tree_path ||= '.'
|
17
|
+
end
|
18
|
+
|
19
|
+
def path
|
20
|
+
@path ||= '.'
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_reader :first_commit_sha
|
24
|
+
|
25
|
+
def last_commit_sha
|
26
|
+
@last_commit_sha ||= 'HEAD'
|
27
|
+
end
|
28
|
+
|
29
|
+
def tree_path
|
30
|
+
@tree_path ||= '.'
|
31
|
+
end
|
32
|
+
|
33
|
+
def comment_string
|
34
|
+
@comment_string ||= '//'
|
35
|
+
end
|
36
|
+
|
37
|
+
def tree
|
38
|
+
@tree ||= Tree.new(repo: self, relative_path: @tree_path)
|
39
|
+
end
|
40
|
+
|
41
|
+
def authors
|
42
|
+
@authors ||= run_and_parse("git shortlog -se #{commit_range} #{tree_path}").map do |author|
|
43
|
+
Author.new(repo: self, name: author[:name], email: author[:email])
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def commits
|
48
|
+
command = "git rev-list --pretty=format:'%H|%at|%ai|%aE' #{commit_range} #{tree_path} | grep -v commit"
|
49
|
+
@commits ||= run_and_parse(command).map do |commit_line|
|
50
|
+
Commit.new(
|
51
|
+
repo: self,
|
52
|
+
sha: commit_line[:sha],
|
53
|
+
stamp: commit_line[:stamp],
|
54
|
+
date: DateTime.parse(commit_line[:date]),
|
55
|
+
author: authors.first! { |a| a.email == commit_line[:author_email] }
|
56
|
+
)
|
57
|
+
end.sort_by!(&:date)
|
58
|
+
end
|
59
|
+
|
60
|
+
def commits_period
|
61
|
+
commits.map(&:date).minmax
|
62
|
+
end
|
63
|
+
|
64
|
+
# TODO: This method is called from nowhere
|
65
|
+
def commits_count_by_author(limit = 4)
|
66
|
+
(authors.map { |author| [author, author.commits.size] }.sort_by { |_author, commits| -commits }[0..limit]).to_h
|
67
|
+
end
|
68
|
+
|
69
|
+
# TODO: These methods are called from nowhere
|
70
|
+
[:insertions, :deletions, :changed_lines].each do |method|
|
71
|
+
define_method "#{method}_by_author" do |limit = 4|
|
72
|
+
(authors.map { |author| [author, author.send(method)] }.sort_by { |_author, lines| -lines }[0..limit]).to_h
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def files_count_by_date
|
77
|
+
@files_count_by_date ||= commits.map do |commit|
|
78
|
+
[commit.date.to_date, commit.files_count]
|
79
|
+
end.to_h
|
80
|
+
end
|
81
|
+
|
82
|
+
def lines_count_by_date
|
83
|
+
sum = 0
|
84
|
+
@lines_count_by_date ||= commits.map do |commit|
|
85
|
+
sum += commit.short_stat.insertions
|
86
|
+
sum -= commit.short_stat.deletions
|
87
|
+
[commit.date.to_date, sum]
|
88
|
+
end.to_h
|
89
|
+
end
|
90
|
+
|
91
|
+
def comments_count_by_date
|
92
|
+
sum = 0
|
93
|
+
@comments_count_by_date ||= commits.map do |commit|
|
94
|
+
sum += commit.comment_stat.insertions
|
95
|
+
sum -= commit.comment_stat.deletions
|
96
|
+
[commit.date.to_date, sum]
|
97
|
+
end.to_h.fill_empty_days!(aggregated: true)
|
98
|
+
end
|
99
|
+
|
100
|
+
def last_commit
|
101
|
+
commits.last
|
102
|
+
end
|
103
|
+
|
104
|
+
def commit_range
|
105
|
+
first_commit_sha.blank? ? last_commit_sha : "#{first_commit_sha}..#{last_commit_sha}"
|
106
|
+
end
|
107
|
+
|
108
|
+
def short_stats
|
109
|
+
@short_stats ||= commits.map(&:short_stat)
|
110
|
+
end
|
111
|
+
|
112
|
+
def comment_stats
|
113
|
+
@comment_stats ||= commits.map(&:comment_stat)
|
114
|
+
end
|
115
|
+
|
116
|
+
def activity
|
117
|
+
@activity ||= Activity.new(commits)
|
118
|
+
end
|
119
|
+
|
120
|
+
def project_version
|
121
|
+
@project_version ||= run("git rev-parse #{commit_range}").strip
|
122
|
+
end
|
123
|
+
|
124
|
+
def project_name
|
125
|
+
# TODO: WHAT
|
126
|
+
@project_name ||= File.expand_path(File.join(path, tree_path)).sub(File.join(File.dirname(File.expand_path(path)), ''), '')
|
127
|
+
end
|
128
|
+
|
129
|
+
def run(command)
|
130
|
+
result = command_runner.run(path, command)
|
131
|
+
invoke_command_observers(command, result)
|
132
|
+
result
|
133
|
+
end
|
134
|
+
|
135
|
+
def run_and_parse(command)
|
136
|
+
result = run(command)
|
137
|
+
command_parser.parse(command, result)
|
138
|
+
end
|
139
|
+
|
140
|
+
def command_runner
|
141
|
+
@command_runner ||= CommandRunner.new
|
142
|
+
end
|
143
|
+
|
144
|
+
def command_parser
|
145
|
+
@command_parser ||= CommandParser.new
|
146
|
+
end
|
147
|
+
|
148
|
+
def add_command_observer(proc = nil, &block)
|
149
|
+
command_observers << block if block
|
150
|
+
command_observers << proc if proc
|
151
|
+
end
|
152
|
+
|
153
|
+
def ==(other)
|
154
|
+
path == other.path
|
155
|
+
end
|
156
|
+
|
157
|
+
private
|
158
|
+
|
159
|
+
def command_observers
|
160
|
+
@command_observers ||= []
|
161
|
+
end
|
162
|
+
|
163
|
+
def invoke_command_observers(command, result)
|
164
|
+
command_observers.each { |o| o.call(command, result) }
|
165
|
+
end
|
166
|
+
|
167
|
+
def ivars_to_be_displayed
|
168
|
+
[:@path, :@tree_path, :@last_commit_sha]
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module GitStats
|
2
|
+
module GitData
|
3
|
+
class ShortStat
|
4
|
+
attr_reader :commit, :files_changed, :insertions, :deletions
|
5
|
+
|
6
|
+
def initialize(commit)
|
7
|
+
@commit = commit
|
8
|
+
calculate_stat
|
9
|
+
end
|
10
|
+
|
11
|
+
def changed_lines
|
12
|
+
insertions + deletions
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def calculate_stat
|
18
|
+
stat_line = commit.repo.run("git show --shortstat --oneline --no-renames #{commit.sha} -- #{commit.repo.tree_path}").lines.to_a[1]
|
19
|
+
if stat_line.blank?
|
20
|
+
@files_changed = @insertions = @deletions = 0
|
21
|
+
else
|
22
|
+
@files_changed = stat_line[/(\d+) files? changed/, 1].to_i
|
23
|
+
@insertions = stat_line[/(\d+) insertions?/, 1].to_i
|
24
|
+
@deletions = stat_line[/(\d+) deletions?/, 1].to_i
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require_relative '../hash_initializable'
|
2
|
+
|
3
|
+
module GitStats
|
4
|
+
module GitData
|
5
|
+
class Tree
|
6
|
+
include GitStats::HashInitializable
|
7
|
+
|
8
|
+
attr_reader :repo, :relative_path
|
9
|
+
|
10
|
+
def authors
|
11
|
+
@authors ||= run_and_parse("git shortlog -se #{commit_range}").map do |author|
|
12
|
+
Author.new(repo: self, name: author[:name], email: author[:email])
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def ==(other)
|
17
|
+
((repo == other.repo) && (relative_path == other.relative_path))
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|