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.
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