nova_git_stats 2.2.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/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
|