git_stats 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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