nova_git_stats 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (107) hide show
  1. checksums.yaml +7 -0
  2. data/bin/git_stats +6 -0
  3. data/config/locales/bg.yml +63 -0
  4. data/config/locales/bg_default.yml +70 -0
  5. data/config/locales/de.yml +63 -0
  6. data/config/locales/de_default.yml +224 -0
  7. data/config/locales/en.yml +63 -0
  8. data/config/locales/es.yml +63 -0
  9. data/config/locales/es_deafult.yml +70 -0
  10. data/config/locales/pl.yml +63 -0
  11. data/config/locales/pl_default.yml +224 -0
  12. data/config/locales/tr.yml +63 -0
  13. data/config/locales/tr_default.yml +70 -0
  14. data/config/locales/zh_cn.yml +65 -0
  15. data/config/locales/zh_cn_default.yml +70 -0
  16. data/config/locales/zh_tw.yml +63 -0
  17. data/config/locales/zh_tw_default.yml +76 -0
  18. data/lib/git_stats/base.rb +28 -0
  19. data/lib/git_stats/cli.rb +35 -0
  20. data/lib/git_stats/command_parser.rb +30 -0
  21. data/lib/git_stats/command_runner.rb +13 -0
  22. data/lib/git_stats/core_extensions/enumerable.rb +8 -0
  23. data/lib/git_stats/core_extensions/hash.rb +24 -0
  24. data/lib/git_stats/core_extensions/symbol.rb +5 -0
  25. data/lib/git_stats/generator.rb +38 -0
  26. data/lib/git_stats/git_data/activity.rb +76 -0
  27. data/lib/git_stats/git_data/author.rb +77 -0
  28. data/lib/git_stats/git_data/blob.rb +41 -0
  29. data/lib/git_stats/git_data/comment_stat.rb +38 -0
  30. data/lib/git_stats/git_data/commit.rb +76 -0
  31. data/lib/git_stats/git_data/repo.rb +172 -0
  32. data/lib/git_stats/git_data/short_stat.rb +29 -0
  33. data/lib/git_stats/git_data/tree.rb +21 -0
  34. data/lib/git_stats/hash_initializable.rb +9 -0
  35. data/lib/git_stats/i18n.rb +2 -0
  36. data/lib/git_stats/inspector.rb +52 -0
  37. data/lib/git_stats/stats_view/charts/activity_charts.rb +69 -0
  38. data/lib/git_stats/stats_view/charts/authors_charts.rb +49 -0
  39. data/lib/git_stats/stats_view/charts/chart.rb +126 -0
  40. data/lib/git_stats/stats_view/charts/charts.rb +36 -0
  41. data/lib/git_stats/stats_view/charts/repo_charts.rb +61 -0
  42. data/lib/git_stats/stats_view/template.rb +21 -0
  43. data/lib/git_stats/stats_view/view.rb +89 -0
  44. data/lib/git_stats/stats_view/view_data.rb +28 -0
  45. data/lib/git_stats/version.rb +7 -0
  46. data/lib/git_stats.rb +14 -0
  47. data/templates/activity/_activity.haml +102 -0
  48. data/templates/activity/by_date.haml +1 -0
  49. data/templates/activity/day_of_week.haml +1 -0
  50. data/templates/activity/hour_of_day.haml +1 -0
  51. data/templates/activity/hour_of_week.haml +1 -0
  52. data/templates/activity/month_of_year.haml +1 -0
  53. data/templates/activity/year.haml +1 -0
  54. data/templates/activity/year_month.haml +1 -0
  55. data/templates/assets/bootstrap/css/bootstrap-3.3.7.min.css +6 -0
  56. data/templates/assets/bootstrap/css/bootstrap-theme-3.3.7.min.css +6 -0
  57. data/templates/assets/bootstrap/css/bootstrap-theme.min.css.map +1 -0
  58. data/templates/assets/bootstrap/css/bootstrap.min.css.map +1 -0
  59. data/templates/assets/bootstrap/js/bootstrap-3.3.7.min.js +7 -0
  60. data/templates/assets/bootstrap-4.6.0-dist/css/bootstrap-grid.css +3872 -0
  61. data/templates/assets/bootstrap-4.6.0-dist/css/bootstrap-grid.css.map +1 -0
  62. data/templates/assets/bootstrap-4.6.0-dist/css/bootstrap-grid.min.css +7 -0
  63. data/templates/assets/bootstrap-4.6.0-dist/css/bootstrap-grid.min.css.map +1 -0
  64. data/templates/assets/bootstrap-4.6.0-dist/css/bootstrap-reboot.css +325 -0
  65. data/templates/assets/bootstrap-4.6.0-dist/css/bootstrap-reboot.css.map +1 -0
  66. data/templates/assets/bootstrap-4.6.0-dist/css/bootstrap-reboot.min.css +8 -0
  67. data/templates/assets/bootstrap-4.6.0-dist/css/bootstrap-reboot.min.css.map +1 -0
  68. data/templates/assets/bootstrap-4.6.0-dist/css/bootstrap.css +10298 -0
  69. data/templates/assets/bootstrap-4.6.0-dist/css/bootstrap.css.map +1 -0
  70. data/templates/assets/bootstrap-4.6.0-dist/css/bootstrap.min.css +7 -0
  71. data/templates/assets/bootstrap-4.6.0-dist/css/bootstrap.min.css.map +1 -0
  72. data/templates/assets/bootstrap-4.6.0-dist/js/bootstrap.bundle.js +7045 -0
  73. data/templates/assets/bootstrap-4.6.0-dist/js/bootstrap.bundle.js.map +1 -0
  74. data/templates/assets/bootstrap-4.6.0-dist/js/bootstrap.bundle.min.js +7 -0
  75. data/templates/assets/bootstrap-4.6.0-dist/js/bootstrap.bundle.min.js.map +1 -0
  76. data/templates/assets/bootstrap-4.6.0-dist/js/bootstrap.js +4432 -0
  77. data/templates/assets/bootstrap-4.6.0-dist/js/bootstrap.js.map +1 -0
  78. data/templates/assets/bootstrap-4.6.0-dist/js/bootstrap.min.js +7 -0
  79. data/templates/assets/bootstrap-4.6.0-dist/js/bootstrap.min.js.map +1 -0
  80. data/templates/assets/export-data.js +17 -0
  81. data/templates/assets/exporting.js +27 -0
  82. data/templates/assets/highstock.js +525 -0
  83. data/templates/assets/jquery-3.6.0.min.js +2 -0
  84. data/templates/assets/menu.css +28 -0
  85. data/templates/author_details/_author_details.haml +31 -0
  86. data/templates/author_details/changed_lines_by_date.haml +1 -0
  87. data/templates/author_details/commits_by_date.haml +1 -0
  88. data/templates/author_details/deletions_by_date.haml +1 -0
  89. data/templates/author_details/insertions_by_date.haml +1 -0
  90. data/templates/authors/_authors.haml +72 -0
  91. data/templates/authors/best_authors.haml +1 -0
  92. data/templates/authors/changed_lines_by_author_by_date.haml +1 -0
  93. data/templates/authors/commits_sum_by_author_by_date.haml +1 -0
  94. data/templates/authors/deletions_by_author_by_date.haml +1 -0
  95. data/templates/authors/insertions_by_author_by_date.haml +1 -0
  96. data/templates/comments/_comments.haml +11 -0
  97. data/templates/comments/by_date.haml +1 -0
  98. data/templates/files/_files.haml +16 -0
  99. data/templates/files/by_date.haml +1 -0
  100. data/templates/files/by_extension.haml +1 -0
  101. data/templates/general.haml +31 -0
  102. data/templates/layout.haml +32 -0
  103. data/templates/lines/_lines.haml +16 -0
  104. data/templates/lines/by_date.haml +1 -0
  105. data/templates/lines/by_extension.haml +1 -0
  106. data/templates/static/index.html +5 -0
  107. 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,13 @@
1
+ module GitStats
2
+ class CommandRunner
3
+ def run(path, command)
4
+ execute(command, path).encode!('UTF-8', 'UTF-8', invalid: :replace)
5
+ end
6
+
7
+ private
8
+
9
+ def execute(command, path)
10
+ Dir.chdir(path) { `#{command}` }
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,8 @@
1
+ module Enumerable
2
+ def first!(&block)
3
+ matching = find(&block)
4
+ raise 'Sequence contains no matching elements' if matching.nil?
5
+
6
+ matching
7
+ end
8
+ 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,5 @@
1
+ class Symbol
2
+ def t
3
+ I18n.t self
4
+ end
5
+ 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
@@ -0,0 +1,9 @@
1
+ module GitStats
2
+ module HashInitializable
3
+ def initialize(params = {})
4
+ raise "pass a Hash to initialize #{self.class}" unless params.is_a? Hash
5
+
6
+ params.each { |k, v| instance_variable_set("@#{k}", v) }
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,2 @@
1
+ I18n.load_path += Dir[GitStats.root.join('config/locales/*.yml')]
2
+ I18n.enforce_available_locales = true