git_stats 1.0.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 (92) hide show
  1. data/.gitignore +21 -0
  2. data/.gitmodules +3 -0
  3. data/.rspec +1 -0
  4. data/Gemfile +13 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +44 -0
  7. data/Rakefile +1 -0
  8. data/bin/git_stats +11 -0
  9. data/config/locales/en.yml +56 -0
  10. data/config/locales/pl.yml +56 -0
  11. data/config/locales/pl_default.yml +224 -0
  12. data/git_stats.gemspec +27 -0
  13. data/lib/git_stats.rb +15 -0
  14. data/lib/git_stats/base.rb +30 -0
  15. data/lib/git_stats/by_field_finder.rb +7 -0
  16. data/lib/git_stats/cli.rb +17 -0
  17. data/lib/git_stats/core_extensions/hash.rb +9 -0
  18. data/lib/git_stats/core_extensions/string.rb +6 -0
  19. data/lib/git_stats/core_extensions/symbol.rb +6 -0
  20. data/lib/git_stats/generator.rb +32 -0
  21. data/lib/git_stats/git_data/activity.rb +78 -0
  22. data/lib/git_stats/git_data/author.rb +67 -0
  23. data/lib/git_stats/git_data/blob.rb +38 -0
  24. data/lib/git_stats/git_data/command_parser.rb +33 -0
  25. data/lib/git_stats/git_data/command_runner.rb +10 -0
  26. data/lib/git_stats/git_data/commit.rb +63 -0
  27. data/lib/git_stats/git_data/repo.rb +137 -0
  28. data/lib/git_stats/git_data/short_stat.rb +33 -0
  29. data/lib/git_stats/hash_initializable.rb +7 -0
  30. data/lib/git_stats/i18n.rb +2 -0
  31. data/lib/git_stats/stats_view/charts/activity_charts.rb +70 -0
  32. data/lib/git_stats/stats_view/charts/authors_charts.rb +35 -0
  33. data/lib/git_stats/stats_view/charts/chart.rb +119 -0
  34. data/lib/git_stats/stats_view/charts/charts.rb +35 -0
  35. data/lib/git_stats/stats_view/charts/repo_charts.rb +53 -0
  36. data/lib/git_stats/stats_view/template.rb +20 -0
  37. data/lib/git_stats/stats_view/view.rb +72 -0
  38. data/lib/git_stats/stats_view/view_data.rb +31 -0
  39. data/lib/git_stats/version.rb +4 -0
  40. data/spec/by_field_finder_spec.rb +25 -0
  41. data/spec/factories.rb +25 -0
  42. data/spec/git_data/activity_spec.rb +38 -0
  43. data/spec/git_data/author_spec.rb +25 -0
  44. data/spec/git_data/blob_spec.rb +25 -0
  45. data/spec/git_data/cli_spec.rb +20 -0
  46. data/spec/git_data/command_observer_spec.rb +35 -0
  47. data/spec/git_data/commit_range_spec.rb +47 -0
  48. data/spec/git_data/commit_spec.rb +48 -0
  49. data/spec/git_data/generator_spec.rb +46 -0
  50. data/spec/git_data/repo_spec.rb +46 -0
  51. data/spec/git_data/short_stat_spec.rb +28 -0
  52. data/spec/hash_extension_spec.rb +26 -0
  53. data/spec/integration/activity_spec.rb +33 -0
  54. data/spec/integration/author_spec.rb +67 -0
  55. data/spec/integration/file_spec.rb +31 -0
  56. data/spec/integration/repo_spec.rb +68 -0
  57. data/spec/integration/shared.rb +37 -0
  58. data/spec/spec_helper.rb +13 -0
  59. data/templates/activity/_activity.haml +101 -0
  60. data/templates/activity/by_date.haml +1 -0
  61. data/templates/activity/day_of_week.haml +1 -0
  62. data/templates/activity/hour_of_day.haml +1 -0
  63. data/templates/activity/hour_of_week.haml +1 -0
  64. data/templates/activity/month_of_year.haml +1 -0
  65. data/templates/activity/year.haml +1 -0
  66. data/templates/activity/year_month.haml +1 -0
  67. data/templates/assets/bootstrap/css/bootstrap-responsive.css +1058 -0
  68. data/templates/assets/bootstrap/css/bootstrap-responsive.min.css +9 -0
  69. data/templates/assets/bootstrap/css/bootstrap.css +5774 -0
  70. data/templates/assets/bootstrap/css/bootstrap.min.css +9 -0
  71. data/templates/assets/bootstrap/img/glyphicons-halflings-white.png +0 -0
  72. data/templates/assets/bootstrap/img/glyphicons-halflings.png +0 -0
  73. data/templates/assets/bootstrap/js/bootstrap.js +2027 -0
  74. data/templates/assets/bootstrap/js/bootstrap.min.js +6 -0
  75. data/templates/assets/highstock.js +305 -0
  76. data/templates/assets/jquery.min.js +2 -0
  77. data/templates/authors/_authors.haml +66 -0
  78. data/templates/authors/best_authors.haml +1 -0
  79. data/templates/authors/changed_lines_by_author_by_date.haml +1 -0
  80. data/templates/authors/commits_sum_by_author_by_date.haml +1 -0
  81. data/templates/authors/deletions_by_author_by_date.haml +1 -0
  82. data/templates/authors/insertions_by_author_by_date.haml +1 -0
  83. data/templates/files/_files.haml +15 -0
  84. data/templates/files/by_date.haml +1 -0
  85. data/templates/files/by_extension.haml +1 -0
  86. data/templates/general.haml +28 -0
  87. data/templates/layout.haml +29 -0
  88. data/templates/lines/_lines.haml +15 -0
  89. data/templates/lines/by_date.haml +1 -0
  90. data/templates/lines/by_extension.haml +1 -0
  91. data/templates/static/index.html +5 -0
  92. metadata +268 -0
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'git_stats/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "git_stats"
8
+ gem.version = GitStats::VERSION
9
+ gem.authors = ["Tomasz Gieniusz"]
10
+ gem.email = ["tomasz.gieniusz@gmail.com"]
11
+ gem.description = %q{Git history statistics generator}
12
+ gem.summary = %q{HTML statistics generator from git repository}
13
+ gem.homepage = "https://github.com/tomgi/git_stats"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_dependency('activesupport')
21
+ gem.add_dependency('actionpack')
22
+ gem.add_dependency('tilt')
23
+ gem.add_dependency('haml')
24
+ gem.add_dependency('launchy')
25
+ gem.add_dependency('lazy_high_charts_tomgi')
26
+ gem.add_dependency('i18n')
27
+ end
@@ -0,0 +1,15 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module GitStats
3
+ end
4
+
5
+ require 'active_support/all'
6
+ require 'action_pack'
7
+ require 'action_view'
8
+ require 'fileutils'
9
+ require 'tilt'
10
+ require 'pathname'
11
+ require 'lazy_high_charts'
12
+ require 'launchy'
13
+ require 'i18n'
14
+
15
+ require 'git_stats/base'
@@ -0,0 +1,30 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'git_stats/core_extensions/hash'
3
+ require 'git_stats/core_extensions/string'
4
+ require 'git_stats/core_extensions/symbol'
5
+
6
+ require 'git_stats/version'
7
+ require 'git_stats/i18n'
8
+ require 'git_stats/by_field_finder'
9
+ require 'git_stats/cli'
10
+ require 'git_stats/generator'
11
+
12
+ require 'git_stats/git_data/activity'
13
+ require 'git_stats/git_data/author'
14
+ require 'git_stats/git_data/blob'
15
+ require 'git_stats/git_data/command_parser'
16
+ require 'git_stats/git_data/command_runner'
17
+ require 'git_stats/git_data/commit'
18
+ require 'git_stats/git_data/repo'
19
+ require 'git_stats/git_data/short_stat'
20
+
21
+ require 'git_stats/stats_view/template'
22
+ require 'git_stats/stats_view/view'
23
+ require 'git_stats/stats_view/view_data'
24
+
25
+ require 'git_stats/stats_view/charts/activity_charts'
26
+ require 'git_stats/stats_view/charts/authors_charts'
27
+ require 'git_stats/stats_view/charts/chart'
28
+ require 'git_stats/stats_view/charts/charts'
29
+ require 'git_stats/stats_view/charts/repo_charts'
30
+
@@ -0,0 +1,7 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module ByFieldFinder
3
+ def method_missing(name, *args, &block)
4
+ field = name[/^by_(.*)$/, 1]
5
+ field ? find { |e| e.send(field) == args.first } : super
6
+ end
7
+ end
@@ -0,0 +1,17 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require "git_stats"
3
+
4
+ class GitStats::CLI
5
+
6
+ def start(*args)
7
+ raise "Wrong number of arguments\nUsage: git_stats repo_path output_path <output_lang>" unless [2, 3].include? args.size
8
+
9
+ repo_path, out_path, lang = args
10
+ I18n.locale = lang || :en
11
+
12
+ GitStats::Generator.new(repo_path, out_path) { |g|
13
+ g.add_command_observer { |command, result| puts "#{command}" }
14
+ }.render_all
15
+ end
16
+
17
+ end
@@ -0,0 +1,9 @@
1
+ # -*- encoding : utf-8 -*-
2
+ class Hash
3
+ def to_key_indexed_array(params = {})
4
+ raise ArgumentError.new('all the keys must be numbers to convert to key indexed array') unless all? { |k, v| k.is_a? Numeric }
5
+ min_size = params[:min_size] || 0
6
+ default = params[:default]
7
+ inject(Array.new(min_size, default)) { |acc, (k, v)| acc[k] = v; acc }.map { |e| e || default }
8
+ end
9
+ end
@@ -0,0 +1,6 @@
1
+ # -*- encoding : utf-8 -*-
2
+ class String
3
+ def absolute_path
4
+ Pathname.new(__FILE__).expand_path.join(self).cleanpath.to_s
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ # -*- encoding : utf-8 -*-
2
+ class Symbol
3
+ def t
4
+ I18n.t self
5
+ end
6
+ end
@@ -0,0 +1,32 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module GitStats
3
+ class Generator
4
+ delegate :add_command_observer, to: :@repo
5
+ delegate :render_all, to: :@view
6
+
7
+ def initialize(repo_path, out_path)
8
+ validate_paths(repo_path, out_path)
9
+
10
+ @repo = GitData::Repo.new(path: repo_path)
11
+ view_data = StatsView::ViewData.new(@repo)
12
+ @view = StatsView::View.new(view_data, out_path)
13
+
14
+ yield self if block_given?
15
+ end
16
+
17
+ private
18
+ def validate_paths(repo_path, out_path)
19
+ validate_repo_path(repo_path)
20
+ validate_out_path(out_path)
21
+ end
22
+
23
+ def validate_repo_path(repo_path)
24
+ raise ArgumentError, "#{repo_path} is not a git repository" unless Dir.exists?("#{repo_path}/.git")
25
+ end
26
+
27
+ def validate_out_path(out_path)
28
+ raise ArgumentError, "#{out_path} is not a directory" unless Dir.exists?(out_path)
29
+ end
30
+ end
31
+
32
+ end
@@ -0,0 +1,78 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module GitStats
3
+ module GitData
4
+ class Activity
5
+
6
+ def initialize(commits)
7
+ add_commits(commits)
8
+ end
9
+
10
+ def by_date
11
+ @by_date ||= default_hash
12
+ end
13
+
14
+ def by_hour
15
+ @by_hour ||= default_hash
16
+ end
17
+
18
+ def by_hour_array
19
+ by_hour.to_key_indexed_array(min_size: 24, default: 0)
20
+ end
21
+
22
+ def by_wday
23
+ @by_wday ||= default_hash
24
+ end
25
+
26
+ def by_wday_array
27
+ by_wday.to_key_indexed_array(min_size: 7, default: 0)
28
+ end
29
+
30
+ def by_wday_hour
31
+ @by_wday_hour ||= default_double_hash
32
+ end
33
+
34
+ def by_month
35
+ @by_month ||= default_hash
36
+ end
37
+
38
+ def by_month_array
39
+ by_month.to_key_indexed_array(min_size: 13, default: 0)[1..-1]
40
+ end
41
+
42
+ def by_year
43
+ @by_year ||= default_hash
44
+ end
45
+
46
+ def by_year_month
47
+ @by_year_week ||= default_double_hash
48
+ end
49
+
50
+ private
51
+ def add_commits(commits)
52
+ commits = commits.values if commits.is_a? Hash
53
+ commits.each do |commit|
54
+ add_commit_at(commit.date)
55
+ end
56
+ end
57
+
58
+ def add_commit_at(date)
59
+ self.by_date[date] += 1
60
+ self.by_hour[date.hour] += 1
61
+ self.by_wday[date.wday] += 1
62
+ self.by_wday_hour[date.wday][date.hour] += 1
63
+ self.by_month[date.month] += 1
64
+ self.by_year[date.year] += 1
65
+ self.by_year_month[date.year][date.month] += 1
66
+ end
67
+
68
+ def default_hash
69
+ Hash.new(0)
70
+ end
71
+
72
+ def default_double_hash
73
+ Hash.new { |h, k| h[k] = Hash.new(0) }
74
+ end
75
+
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,67 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'git_stats/hash_initializable'
3
+
4
+ module GitStats
5
+ module GitData
6
+ class Author
7
+ include HashInitializable
8
+
9
+ attr_reader :repo, :name, :email
10
+
11
+ def commits
12
+ @commits ||= repo.commits.select { |commit| commit.author == self }
13
+ end
14
+
15
+ def changed_lines
16
+ insertions + deletions
17
+ end
18
+
19
+ def insertions
20
+ short_stats.map(&:insertions).sum
21
+ end
22
+
23
+ def deletions
24
+ short_stats.map(&:deletions).sum
25
+ end
26
+
27
+ def commits_sum_by_date
28
+ sum = 0
29
+ commits.map { |commit|
30
+ sum += 1
31
+ [commit.date, sum]
32
+ }
33
+ end
34
+
35
+ [:insertions, :deletions, :changed_lines].each do |method|
36
+ define_method "#{method}_by_date" do
37
+ sum = 0
38
+ commits.map { |commit|
39
+ sum += commit.short_stat.send(method)
40
+ [commit.date, sum]
41
+ }
42
+ end
43
+ end
44
+
45
+ def short_stats
46
+ commits.map(&:short_stat)
47
+ end
48
+
49
+ def activity
50
+ @activity ||= Activity.new(commits)
51
+ end
52
+
53
+ def dirname
54
+ @name.underscore.split.join '_'
55
+ end
56
+
57
+ def to_s
58
+ "#{self.class} #@name <#@email>"
59
+ end
60
+
61
+ def ==(other)
62
+ [self.repo, self.name, self.email] == [other.repo, other.name, other.email]
63
+ end
64
+
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,38 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ require 'git_stats/hash_initializable'
4
+
5
+ module GitStats
6
+ module GitData
7
+ class Blob
8
+ include HashInitializable
9
+
10
+ attr_reader :repo, :sha, :filename
11
+
12
+ def lines_count
13
+ @lines_count ||= binary? ? 0 : repo.run("git cat-file blob #{self.sha} | wc -l").to_i
14
+ end
15
+
16
+ def content
17
+ @content ||= repo.run("git cat-file blob #{self.sha}")
18
+ end
19
+
20
+ def extension
21
+ @ext ||= File.extname(filename)
22
+ end
23
+
24
+ def binary?
25
+ repo.run("git cat-file blob #{self.sha} | grep -m 1 '^'") =~ /Binary file/
26
+ end
27
+
28
+ def to_s
29
+ "#{self.class} #@sha #@filename"
30
+ end
31
+
32
+ def ==(other)
33
+ [self.repo, self.sha, self.filename] == [other.repo, other.sha, other.filename]
34
+ end
35
+
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,33 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module GitStats
3
+ module GitData
4
+ class CommandParser
5
+ def parse(command, result)
6
+ cmd, params = command.scan(/git (.*) (.*)/).first.map(&:split).flatten
7
+ send("parse_#{cmd.underscore}", result, params)
8
+ end
9
+
10
+ def parse_shortlog(result, params)
11
+ result.lines.map do |line|
12
+ commits, name, email = line.scan(/(.*)\t(.*)<(.*)>/).first.map(&:strip)
13
+ {commits: commits.to_i, name: name, email: email}
14
+ end
15
+ end
16
+
17
+ def parse_ls_tree(result, params)
18
+ result.lines.map do |line|
19
+ mode, type, sha, filename = line.scan(/(.*) (.*) (.*)\t(.*)/).first.map(&:strip)
20
+ {mode: mode, type: type, sha: sha, filename: filename}
21
+ end
22
+ end
23
+
24
+ def parse_rev_list(result, params)
25
+ result.lines.map do |line|
26
+ sha, stamp, date, author_email = line.split('|').map(&:strip)
27
+ {sha: sha, stamp: stamp, date: date, author_email: author_email}
28
+ end
29
+ end
30
+
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,10 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module GitStats
3
+ module GitData
4
+ class CommandRunner
5
+ def run(path, command)
6
+ Dir.chdir(path) { %x[#{command}] }
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,63 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'git_stats/hash_initializable'
3
+
4
+ module GitStats
5
+ module GitData
6
+ class Commit
7
+ include HashInitializable
8
+
9
+ attr_reader :repo, :sha, :stamp, :date, :author
10
+
11
+ def files
12
+ @files ||= repo.run_and_parse("git ls-tree -r #{self.sha}").map do |file|
13
+ Blob.new(repo: repo, filename: file[:filename], sha: file[:sha])
14
+ end.extend(ByFieldFinder)
15
+ end
16
+
17
+ def binary_files
18
+ @binary_files ||= files.select { |f| f.binary? }
19
+ end
20
+
21
+ def text_files
22
+ @text_files ||= files - binary_files
23
+ end
24
+
25
+ def files_by_extension
26
+ @files_by_extension ||= files.inject({}) { |acc, f| acc[f.extension] ||= []; acc[f.extension] << f; acc }
27
+ end
28
+
29
+ def files_by_extension_count
30
+ @files_by_extension_count ||= Hash[files_by_extension.map { |ext, files| [ext, files.count] }]
31
+ end
32
+
33
+ def lines_by_extension
34
+ @lines_by_extension ||= Hash[files_by_extension.map { |ext, files|
35
+ [ext, files.map(&:lines_count).sum]
36
+ }.delete_if { |ext, lines_count| lines_count == 0 }]
37
+ end
38
+
39
+ def files_count
40
+ @files_count ||= repo.run("git ls-tree -r --name-only #{self.sha} | wc -l").to_i
41
+ end
42
+
43
+ def lines_count
44
+ @lines_count ||= repo.run("git diff --shortstat `git hash-object -t tree /dev/null` #{self.sha}").lines.map do |line|
45
+ line[/(\d+) insertions?/, 1].to_i
46
+ end.sum
47
+ end
48
+
49
+ def short_stat
50
+ @short_stat ||= ShortStat.new(self)
51
+ end
52
+
53
+ def to_s
54
+ "#{self.class} #@sha"
55
+ end
56
+
57
+ def ==(other)
58
+ [self.repo, self.sha, self.stamp, self.date, self.author] ==
59
+ [other.repo, other.sha, other.stamp, other.date, other.author]
60
+ end
61
+ end
62
+ end
63
+ end