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