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,53 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module GitStats
3
+ module StatsView
4
+ module Charts
5
+ class RepoCharts
6
+ def initialize(repo)
7
+ @repo = repo
8
+ end
9
+
10
+ def files_by_extension
11
+ Chart.new do |f|
12
+ f.column_hash_chart(
13
+ data: @repo.files_by_extension_count,
14
+ title: :files_by_extension.t,
15
+ y_text: :files.t
16
+ )
17
+ end
18
+ end
19
+
20
+ def lines_by_extension
21
+ Chart.new do |f|
22
+ f.column_hash_chart(
23
+ data: @repo.lines_by_extension,
24
+ title: :lines_by_extension.t,
25
+ y_text: :lines.t
26
+ )
27
+ end
28
+ end
29
+
30
+ def files_by_date
31
+ Chart.new do |f|
32
+ f.date_chart(
33
+ data: @repo.files_count_by_date,
34
+ title: :files_by_date.t,
35
+ y_text: :files.t
36
+ )
37
+ end
38
+ end
39
+
40
+ def lines_by_date
41
+ Chart.new do |f|
42
+ f.date_chart(
43
+ data: @repo.lines_count_by_date,
44
+ title: :lines_by_date.t,
45
+ y_text: :lines.t
46
+ )
47
+ end
48
+ end
49
+
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,20 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module GitStats
3
+ module StatsView
4
+ class Template
5
+ def initialize(name, layout=nil)
6
+ @name = name
7
+ @layout = layout
8
+ @template = Tilt.new("../../../../templates/#@name.haml".absolute_path)
9
+ end
10
+
11
+ def render(data, params={})
12
+ if @layout
13
+ @layout.render(data, :active_page => params[:active_page] || @name, :links => params[:links]) { @template.render(data, params) }
14
+ else
15
+ @template.render(data, params)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,72 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module GitStats
3
+ module StatsView
4
+ class View
5
+ def initialize(view_data, out_path)
6
+ @view_data, @out_path = view_data, out_path
7
+ @layout = Tilt.new("../../../../templates/layout.haml".absolute_path)
8
+ end
9
+
10
+ def render_all
11
+ prepare_static_content
12
+ prepare_assets
13
+
14
+ all_templates.each do |template|
15
+ output = Template.new(template, @layout).render(@view_data, author: @view_data.repo, links: links)
16
+ write(output, "#@out_path/#{template}.html")
17
+ end
18
+
19
+ render_authors_activity
20
+ end
21
+
22
+ def render_authors_activity
23
+ done = []
24
+ @view_data.repo.authors.sort_by { |a| -a.commits.size }.each do |author|
25
+ next if done.include? author.dirname
26
+ done << author.dirname
27
+ all_templates('activity/').each do |template|
28
+ output = Template.new(template, @layout).render(@view_data,
29
+ author: author,
30
+ links: links,
31
+ active_page: "/authors/#{author.dirname}/#{template}")
32
+ write(output, "#@out_path/authors/#{author.dirname}/#{template}.html")
33
+ end
34
+ end
35
+ end
36
+
37
+ def all_templates(root = '')
38
+ Dir["../../../../templates/#{root}**/[^_]*.haml".absolute_path].map {
39
+ |f| Pathname.new(f)
40
+ }.map { |f|
41
+ f.relative_path_from(Pathname.new('../../../../templates'.absolute_path)).sub_ext('')
42
+ }.map(&:to_s) - %w(layout)
43
+ end
44
+
45
+ private
46
+
47
+ def write(output, write_file)
48
+ FileUtils.mkdir_p(File.dirname(write_file))
49
+ File.open(write_file, 'w') { |f| f.write output }
50
+ end
51
+
52
+ def links
53
+ {
54
+ general: 'general.html',
55
+ activity: 'activity/by_date.html',
56
+ authors: 'authors/best_authors.html',
57
+ files: 'files/by_date.html',
58
+ lines: 'lines/by_date.html'
59
+ }
60
+ end
61
+
62
+ def prepare_static_content
63
+ FileUtils.cp_r(Dir["../../../../templates/static/*".absolute_path], @out_path)
64
+ end
65
+
66
+ def prepare_assets
67
+ FileUtils.cp_r('../../../../templates/assets'.absolute_path, @out_path)
68
+ end
69
+
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,31 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module GitStats
3
+ module StatsView
4
+ class ViewData
5
+ include ActionView::Helpers::TagHelper
6
+ include LazyHighCharts::LayoutHelper
7
+ attr_reader :repo
8
+
9
+ def initialize(repo)
10
+ @repo = repo
11
+ end
12
+
13
+ def charts
14
+ @charts ||= Charts::All.new(repo)
15
+ end
16
+
17
+ def render_partial(template_name, params = {})
18
+ Template.new(template_name).render(self, params)
19
+ end
20
+
21
+ def asset_path(asset, active_page)
22
+ Pathname.new("/assets/#{asset}").relative_path_from(Pathname.new("/#{active_page}").dirname)
23
+ end
24
+
25
+ def link_to(href, active_page)
26
+ Pathname.new("/#{href}").relative_path_from(Pathname.new("/#{active_page}").dirname)
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,4 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module GitStats
3
+ VERSION = "1.0.0"
4
+ end
@@ -0,0 +1,25 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'spec_helper'
3
+
4
+ describe ByFieldFinder do
5
+ let(:sut) { [double(field1: 'aa', field2: 'bb'), double(field1: 'cc', field2: 'bb'), double(field1: 'aa', field2: 'dd')].extend(ByFieldFinder) }
6
+
7
+ [
8
+ {field: :field1, search_value: 'aa', matching_index: 0},
9
+ {field: :field1, search_value: 'cc', matching_index: 1},
10
+ {field: :field2, search_value: 'bb', matching_index: 0},
11
+ {field: :field2, search_value: 'dd', matching_index: 2},
12
+ ].each do |test_params|
13
+ it 'should return first matching object' do
14
+ sut.send("by_#{test_params[:field]}", test_params[:search_value]).should == sut[test_params[:matching_index]]
15
+ end
16
+ end
17
+
18
+ it 'should return nil if no object matches' do
19
+ sut.by_field1('xx').should == nil
20
+ end
21
+
22
+ it 'should throw exception if elements doesnt respond to given field' do
23
+ expect { sut.by_non_existing_field }.to raise_error
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ # -*- encoding : utf-8 -*-
2
+ FactoryGirl.define do
3
+ initialize_with { new(attributes) }
4
+
5
+ factory :repo, class: GitStats::GitData::Repo do
6
+ path "repo_path"
7
+ factory :test_repo do
8
+ path 'spec/integration/test_repo'
9
+ end
10
+ end
11
+
12
+ factory :author, class: GitStats::GitData::Author do
13
+ sequence(:name) { |i| "author#{i}" }
14
+ sequence(:email) { |i| "author#{i}@gmail.com" }
15
+ association :repo, strategy: :build
16
+ end
17
+
18
+ factory :commit, class: GitStats::GitData::Commit do
19
+ sequence(:sha) { |i| i }
20
+ sequence(:stamp) { |i| i }
21
+ sequence(:date) { |i| Date.new(i) }
22
+ association :repo, strategy: :build
23
+ association :author, strategy: :build
24
+ end
25
+ end
@@ -0,0 +1,38 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'spec_helper'
3
+
4
+ describe GitStats::GitData::Activity do
5
+ let(:dates) { [
6
+ '10.05.2012 12:37',
7
+ '10.05.2012 13:53',
8
+ '06.05.2012 13:23',
9
+ '15.06.2011 15:02',
10
+ '27.09.2011 15:34'
11
+ ] }
12
+ let(:commits) { dates.map { |d| GitStats::GitData::Commit.new(:date => DateTime.parse(d)) } }
13
+ let(:activity) { GitStats::GitData::Activity.new(commits) }
14
+
15
+ it 'by_hour should count commits by hour' do
16
+ activity.by_hour.should == {12 => 1, 13 => 2, 15 => 2}
17
+ end
18
+
19
+ it 'by_wday should count commits by day of week where 0 = sunday, 1 = monday, ...' do
20
+ activity.by_wday.should == {0 => 1, 2 => 1, 3 => 1, 4 => 2}
21
+ end
22
+
23
+ it 'by_wday_hour should count commits by day of week and by hour' do
24
+ activity.by_wday_hour.should == {0 => {13 => 1}, 2 => {15 => 1}, 3 => {15 => 1}, 4 => {12 => 1, 13 => 1}}
25
+ end
26
+
27
+ it 'by_month should count commits by month' do
28
+ activity.by_month.should == {5 => 3, 6 => 1, 9 => 1}
29
+ end
30
+
31
+ it 'by_year should count commits by year' do
32
+ activity.by_year.should == {2011 => 2, 2012 => 3}
33
+ end
34
+
35
+ it 'by_year_month should count commits by day of year and by month' do
36
+ activity.by_year_month.should == {2011 => {6 => 1, 9 => 1}, 2012 => {5 => 3}}
37
+ end
38
+ end
@@ -0,0 +1,25 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'spec_helper'
3
+
4
+ describe GitStats::GitData::Author do
5
+ let(:repo) { build(:repo) }
6
+ let(:author) { build(:author, repo: repo) }
7
+ let(:other_author) { build(:author, repo: repo) }
8
+ let(:my_commits) { 10.times.map { |i| double("my_commit #{i}", author: author, short_stat: double("my_short_stat #{i}", insertions: 5, deletions: 10)) } }
9
+ let(:other_commits) { 10.times.map { |i| double("other_commit #{i}", author: other_author) } }
10
+
11
+ before { repo.stub(:commits => my_commits + other_commits) }
12
+
13
+ it 'commits should give repo commits filtered to this author' do
14
+ author.commits.should == my_commits
15
+ end
16
+
17
+ it 'should count lines added from short stat' do
18
+ author.insertions.should == 50
19
+ end
20
+
21
+ it 'should count lines deleted from short stat' do
22
+ author.deletions.should == 100
23
+ end
24
+
25
+ end
@@ -0,0 +1,25 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'spec_helper'
3
+
4
+ describe GitStats::GitData::Blob do
5
+ let(:repo) { double }
6
+ let(:png_blob) { GitStats::GitData::Blob.new(filename: 'abc.png', sha: 'hash_png', repo: repo) }
7
+ let(:txt_blob) { GitStats::GitData::Blob.new(filename: 'abc.txt', sha: 'hash_txt', repo: repo) }
8
+
9
+ it 'should return 0 as lines count when files is binary' do
10
+ png_blob.should_receive(:binary?).and_return true
11
+ png_blob.lines_count.should == 0
12
+ end
13
+
14
+ it 'should return actual lines count when files is not binary' do
15
+ txt_blob.should_receive(:binary?).and_return false
16
+ repo.should_receive(:run).with("git cat-file blob hash_txt | wc -l").and_return 42
17
+ txt_blob.lines_count.should == 42
18
+ end
19
+
20
+ it 'should invoke grep to check if file is binary' do
21
+ repo.should_receive(:run).with("git cat-file blob hash_png | grep -m 1 '^'").and_return "Binary file matches"
22
+ png_blob.should be_binary
23
+ end
24
+
25
+ end
@@ -0,0 +1,20 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'spec_helper'
3
+
4
+ describe GitStats::CLI do
5
+ let(:repo_path) { 'repo_path' }
6
+ let(:out_path) { 'out_path' }
7
+
8
+ it 'should invoke generator with console arguments given' do
9
+ generator = double('generator')
10
+ GitStats::Generator.should_receive(:new).with(repo_path, out_path).and_return(generator)
11
+ generator.should_receive(:render_all)
12
+
13
+ subject.start(repo_path, out_path)
14
+ end
15
+
16
+ it 'should raise error when 2 arguments are not given' do
17
+ expect { subject.start("only one argument") }.to raise_error
18
+ expect { subject.start("too", "much", "arguments") }.to raise_error
19
+ end
20
+ end
@@ -0,0 +1,35 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'spec_helper'
3
+
4
+ describe GitStats::GitData::Repo do
5
+ let(:repo) { build(:repo) }
6
+
7
+ describe 'command observers' do
8
+ context 'should be invoked after every command' do
9
+ it 'should accept block' do
10
+ command_runner = double('command_runner')
11
+ repo = build(:repo, command_runner: command_runner)
12
+
13
+ observer = double('observer')
14
+ repo.add_command_observer { |command, result| observer.invoked(command, result) }
15
+ command_runner.should_receive(:run).with(repo.path, 'aa').and_return('bb')
16
+ observer.should_receive(:invoked).with('aa', 'bb')
17
+
18
+ repo.run('aa')
19
+ end
20
+
21
+ it 'should accept Proc' do
22
+ command_runner = double('command_runner')
23
+ repo = build(:repo, command_runner: command_runner)
24
+
25
+ observer = double('observer')
26
+ repo.add_command_observer(observer)
27
+ command_runner.should_receive(:run).with(repo.path, 'aa').and_return('bb')
28
+ observer.should_receive(:call).with('aa', 'bb')
29
+
30
+ repo.run('aa')
31
+ end
32
+ end
33
+ end
34
+
35
+ end
@@ -0,0 +1,47 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'spec_helper'
3
+
4
+ describe GitStats::GitData::Repo do
5
+ let(:repo) { build(:repo) }
6
+
7
+ describe 'commit range' do
8
+ it 'should return HEAD by default' do
9
+ repo.commit_range.should == 'HEAD'
10
+ end
11
+
12
+ it 'should return last_commit if it was given' do
13
+ repo = build(:repo, last_commit_sha: 'abc')
14
+ repo.commit_range.should == 'abc'
15
+ end
16
+
17
+ it 'should return range from first_commit to HEAD if first_commit was given' do
18
+ repo = build(:repo, first_commit_sha: 'abc')
19
+ repo.commit_range.should == 'abc..HEAD'
20
+ end
21
+
22
+ it 'should return range from first to last commit if both were given' do
23
+ repo = build(:repo, first_commit_sha: 'abc', last_commit_sha: 'def')
24
+ repo.commit_range.should == 'abc..def'
25
+ end
26
+
27
+ context 'git commands using range' do
28
+ let(:repo) { build(:repo, first_commit_sha: 'abc', last_commit_sha: 'def') }
29
+
30
+ it 'should affect authors command' do
31
+ repo.should_receive(:run).with('git shortlog -se abc..def').and_return("")
32
+ repo.authors
33
+ end
34
+
35
+ it 'should affect commits command' do
36
+ repo.should_receive(:run).with("git rev-list --pretty=format:'%h|%at|%ai|%aE' abc..def | grep -v commit").and_return("")
37
+ repo.commits
38
+ end
39
+
40
+ it 'should affect project version command' do
41
+ repo.should_receive(:run).with('git rev-parse --short abc..def').and_return("")
42
+ repo.project_version
43
+ end
44
+ end
45
+ end
46
+
47
+ end
@@ -0,0 +1,48 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'spec_helper'
3
+
4
+ describe GitStats::GitData::Commit do
5
+ let(:commit) { build(:commit, sha: 'abc') }
6
+
7
+ describe 'git output parsing' do
8
+ context 'parsing git ls-tree output' do
9
+ before {
10
+ commit.repo.should_receive(:run).with('git ls-tree -r abc').and_return("100644 blob 5ade7ad51a75ee7db4eb06cecd3918d38134087d lib/git_stats/git_data/commit.rb
11
+ 100644 blob db01e94677a8f72289848e507a52a43de2ea109a lib/git_stats/git_data/repo.rb
12
+ 100644 blob 1463eacb3ac9f95f21f360f1eb935a84a9ee0895 templates/index.haml
13
+ 100644 blob 31d8b960a67f195bdedaaf9e7aa70b2389f3f1a8 templates/assets/bootstrap/css/bootstrap.min.css
14
+ ") }
15
+
16
+ it 'should be parsed to files' do
17
+ commit.files.should == [
18
+ GitStats::GitData::Blob.new(repo: commit.repo, sha: "5ade7ad51a75ee7db4eb06cecd3918d38134087d", filename: "lib/git_stats/git_data/commit.rb"),
19
+ GitStats::GitData::Blob.new(repo: commit.repo, sha: "db01e94677a8f72289848e507a52a43de2ea109a", filename: "lib/git_stats/git_data/repo.rb"),
20
+ GitStats::GitData::Blob.new(repo: commit.repo, sha: "1463eacb3ac9f95f21f360f1eb935a84a9ee0895", filename: "templates/index.haml"),
21
+ GitStats::GitData::Blob.new(repo: commit.repo, sha: "31d8b960a67f195bdedaaf9e7aa70b2389f3f1a8", filename: "templates/assets/bootstrap/css/bootstrap.min.css"),
22
+ ]
23
+ end
24
+
25
+ it 'should group files by extension' do
26
+ commit.files_by_extension.should == {'.rb' => [
27
+ GitStats::GitData::Blob.new(repo: commit.repo, sha: "5ade7ad51a75ee7db4eb06cecd3918d38134087d", filename: "lib/git_stats/git_data/commit.rb"),
28
+ GitStats::GitData::Blob.new(repo: commit.repo, sha: "db01e94677a8f72289848e507a52a43de2ea109a", filename: "lib/git_stats/git_data/repo.rb")
29
+ ], '.haml' => [
30
+ GitStats::GitData::Blob.new(repo: commit.repo, sha: "1463eacb3ac9f95f21f360f1eb935a84a9ee0895", filename: "templates/index.haml")
31
+ ], '.css' => [
32
+ GitStats::GitData::Blob.new(repo: commit.repo, sha: "31d8b960a67f195bdedaaf9e7aa70b2389f3f1a8", filename: "templates/assets/bootstrap/css/bootstrap.min.css")
33
+ ]
34
+ }
35
+ end
36
+
37
+ it 'should count lines by extension excluding empty or binary files' do
38
+ GitStats::GitData::Blob.should_receive(:new).and_return(
39
+ double(lines_count: 40, extension: '.rb'),
40
+ double(lines_count: 60, extension: '.rb'),
41
+ double(lines_count: 0, extension: '.haml'),
42
+ double(lines_count: 20, extension: '.css'),
43
+ )
44
+ commit.lines_by_extension.should == {'.rb' => 100, '.css' => 20}
45
+ end
46
+ end
47
+ end
48
+ end