git_statistics 0.5.1 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,12 +1,9 @@
1
1
  require 'json'
2
- require 'trollop'
3
2
  require 'grit'
4
3
  require 'linguist'
5
- require 'os'
6
4
  require 'pathname'
7
5
 
8
6
  # Must be required before all other files
9
- require 'git_statistics/core_ext/string'
10
7
  require 'git_statistics/blob'
11
8
  require 'git_statistics/regex_matcher'
12
9
 
@@ -0,0 +1,59 @@
1
+ require 'logger'
2
+ require 'singleton'
3
+
4
+ module GitStatistics
5
+ class Log
6
+ include Singleton
7
+
8
+ attr_accessor :logger, :base_directory, :debugging
9
+
10
+ def initialize
11
+ @base_directory = File.expand_path("../..", __FILE__) + "/"
12
+ @debugging = false
13
+ @logger = Logger.new(STDOUT)
14
+ @logger.level = Logger::ERROR
15
+ @logger.formatter = proc do |sev, datetime, progname, msg|
16
+ "#{msg}\n"
17
+ end
18
+ end
19
+
20
+ def self.use_debug
21
+ instance.debugging = true
22
+ instance.logger.formatter = proc do |sev, datetime, progname, msg|
23
+ "#{sev} [#{progname}]: #{msg}\n"
24
+ end
25
+ end
26
+
27
+ # Determine the file, method, line number of the caller
28
+ def self.parse_caller(message)
29
+ if /^(?<file>.+?):(?<line>\d+)(?::in `(?<method>.*)')?/ =~ message
30
+ file = Regexp.last_match[:file]
31
+ line = Regexp.last_match[:line]
32
+ method = Regexp.last_match[:method]
33
+ "#{file.sub(instance.base_directory, "")}:#{line}"
34
+ end
35
+ end
36
+
37
+ def self.method_missing(method, *args, &blk)
38
+ if valid_method? method
39
+ instance.logger.progname = parse_caller(caller(1).first) if instance.debugging
40
+ instance.logger.send(method, *args, &blk)
41
+ else
42
+ super
43
+ end
44
+ end
45
+
46
+ def self.respond_to_missing?(method, include_all=false)
47
+ if valid_method? method
48
+ true
49
+ else
50
+ super
51
+ end
52
+ end
53
+
54
+ def self.valid_method?(method)
55
+ instance.logger.respond_to? method
56
+ end
57
+
58
+ end
59
+ end
@@ -0,0 +1,29 @@
1
+ module GitStatistics
2
+ class Pipe
3
+ include Enumerable
4
+
5
+ def initialize(command)
6
+ @command = command
7
+ end
8
+
9
+ def command
10
+ @command.dup.gsub(/\A\|/i, '')
11
+ end
12
+
13
+ def each(&block)
14
+ lines.each(&block)
15
+ end
16
+
17
+ def empty?
18
+ lines.empty?
19
+ end
20
+
21
+ def lines
22
+ io.map { |line| line.strip.force_encoding("iso-8859-1").encode("utf-8") }
23
+ end
24
+
25
+ def io
26
+ open("|#{command} 2>/dev/null")
27
+ end
28
+ end
29
+ end
@@ -1,17 +1,18 @@
1
+ require 'rbconfig'
2
+
1
3
  module GitStatistics
2
4
  module Utilities
5
+
6
+ class NotInRepository < StandardError; end
7
+
3
8
  def self.get_repository(path = Dir.pwd)
4
- # Connect to git repository if it exists
5
- directory = Pathname.new(path)
6
- repo = nil
7
- while !directory.root? do
8
- begin
9
- repo = Grit::Repo.new(directory)
10
- return repo
11
- rescue
12
- directory = directory.parent
13
- end
14
- end
9
+ ascender = Pathname.new(path).to_enum(:ascend)
10
+ repo_path = ascender.detect { |path| (path + '.git').exist? }
11
+ raise NotInRepository unless repo_path
12
+ Grit::Repo.new(repo_path.to_s)
13
+ rescue NotInRepository
14
+ Log.error "You must be within a Git project to run git-statistics."
15
+ exit 0
15
16
  end
16
17
 
17
18
  def self.max_length_in_list(list, max = nil)
@@ -22,10 +23,6 @@ module GitStatistics
22
23
  max
23
24
  end
24
25
 
25
- def self.clean_string(string)
26
- string.strip.force_encoding("iso-8859-1").encode("utf-8")
27
- end
28
-
29
26
  def self.split_old_new_file(old, new)
30
27
  # Split the old and new chunks up (separted by the =>)
31
28
  split_old = old.split('{')
@@ -84,11 +81,26 @@ module GitStatistics
84
81
  end
85
82
 
86
83
  def self.get_modified_time(file)
87
- command = case
88
- when OS.mac? then "stat -f %m #{file}"
89
- when OS.linux? then "stat -c %Y #{file}"
90
- else raise "Update on the Windows operating system is not supported"; end
91
- time_at(command)
84
+ if os == :windows
85
+ raise "`stat` is not supported on the Windows operating system"
86
+ end
87
+ flags = os == :mac ? "-f %m" : "-c %Y"
88
+ time_at("stat #{flags} #{file}")
89
+ end
90
+
91
+ def os
92
+ case RbConfig::CONFIG['host_os']
93
+ when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
94
+ :windows
95
+ when /darwin|mac os/
96
+ :mac
97
+ when /linux/
98
+ :linux
99
+ when /solaris|bsd/
100
+ :unix
101
+ else
102
+ :unknown
103
+ end
92
104
  end
93
105
 
94
106
  def self.time_at(cmd)
@@ -96,9 +108,7 @@ module GitStatistics
96
108
  end
97
109
 
98
110
  def self.number_of_matching_files(directory, pattern)
99
- Dir.entries(directory)
100
- .select { |file| file =~ pattern }
101
- .size
111
+ Dir.entries(directory).grep(pattern).size
102
112
  rescue SystemCallError
103
113
  warn "No such directory #{File.expand_path(directory)}"
104
114
  0
@@ -1,3 +1,3 @@
1
1
  module GitStatistics
2
- VERSION = "0.5.1"
2
+ VERSION = "0.6.0"
3
3
  end
@@ -0,0 +1,44 @@
1
+ require 'spec_helper'
2
+ include GitStatistics
3
+
4
+ describe Branches do
5
+ subject { described_class }
6
+
7
+ before do
8
+ subject.stub(:pipe) { fixture(branches) }
9
+ end
10
+
11
+ context "with many branches" do
12
+ let(:branches) {"git_many_branches.txt"}
13
+ its(:all) { should have(2).items }
14
+ its(:all) { should include "issue_2" }
15
+ its(:all) { should include "master" }
16
+ its(:current) { should == "issue_2" }
17
+ its(:detached?) { should be_false }
18
+ end
19
+
20
+ context "with zero branches" do
21
+ let(:branches) {"git_zero_branches.txt"}
22
+ its(:all) { should have(1).items }
23
+ its(:all) { should include "master" }
24
+ its(:current) { should == "master" }
25
+ its(:detached?) { should be_false }
26
+ end
27
+
28
+ context "with many branches in detached state" do
29
+ let(:branches) {"git_many_branches_detached_state.txt"}
30
+ its(:all) { should have(2).items }
31
+ its(:all) { should include "issue_2" }
32
+ its(:all) { should include "master" }
33
+ its(:current) { should == "(none)" }
34
+ its(:detached?) { should be_true }
35
+ end
36
+
37
+ context "with zero branches in detached state" do
38
+ let(:branches) {"git_zero_branches_detached_state.txt"}
39
+ its(:all) { should have(1).items }
40
+ its(:all) { should include "master" }
41
+ its(:current) { should == "(none)" }
42
+ its(:detached?) { should be_true }
43
+ end
44
+ end
@@ -2,17 +2,14 @@ require 'spec_helper'
2
2
  include GitStatistics
3
3
 
4
4
  describe Collector do
5
- let(:verbose) {false}
6
5
  let(:limit) {100}
7
6
  let(:fresh) {true}
8
7
  let(:pretty) {false}
9
- let(:collector) {Collector.new(verbose, limit, fresh, pretty)}
8
+ let(:collector) {Collector.new(limit, fresh, pretty)}
10
9
 
11
10
  # Create buffer which is an array of cleaned lines
12
11
  let(:buffer) {
13
- fixture(fixture_file).readlines.collect do |line|
14
- line.clean_for_authors
15
- end
12
+ fixture(fixture_file).lines
16
13
  }
17
14
 
18
15
  describe "#collect" do
@@ -83,26 +80,8 @@ describe Collector do
83
80
  end
84
81
  end
85
82
 
86
- describe "#collect_branches" do
87
- let(:branches) {collector.collect_branches(fixture(fixture_file))}
88
-
89
- context "with many branches" do
90
- let(:fixture_file) {"git_many_branches.txt"}
91
- it {branches.size.should == 2}
92
- it {branches[0].should == "issue_2"}
93
- it {branches[1].should == "master"}
94
- end
95
-
96
- context "with zero branches" do
97
- let(:fixture_file) {"git_zero_branches.txt"}
98
- it {branches.size.should == 1}
99
- it {branches[0].should == "master"}
100
- end
101
- end
102
-
103
83
  describe "#acquire_commit_data" do
104
- let(:input) {fixture(fixture_file).read}
105
- let(:data) {collector.acquire_commit_data(input)}
84
+ let(:data) {collector.acquire_commit_data(buffer.first)}
106
85
 
107
86
  context "no parent, first commit" do
108
87
  let(:fixture_file) {"commit_buffer_information_first.txt"}
@@ -231,38 +210,38 @@ describe Collector do
231
210
  end
232
211
 
233
212
  describe "#fall_back_collect_commit" do
234
- let(:results) {collector.fall_back_collect_commit(sha)}
213
+ subject { collector.fall_back_collect_commit(sha) }
235
214
  context "with valid sha" do
236
- let(:fixture_file) {"commit_buffer_whole.txt"}
237
- let(:sha) {"260bc61e2c42930d91f3503c5849b0a2351275cf"}
238
- it {results.should == buffer}
215
+ let(:fixture_file) { "commit_buffer_whole.txt" }
216
+ let(:sha) { "260bc61e2c42930d91f3503c5849b0a2351275cf" }
217
+ it { should == buffer }
239
218
  end
240
219
 
241
220
  context "with invalid sha" do
242
- let(:sha) {"111111aa111a11111a11aa11aaaa11a111111a11"}
243
- it {results.should.nil?}
221
+ let(:sha) { "111111aa111a11111a11aa11aaaa11a111111a11" }
222
+ it { should be_empty }
244
223
  end
245
224
  end
246
225
 
247
226
  describe "#get_blob" do
248
- let(:sha) {"695b487432e8a1ede765b4e3efda088ab87a77f8"} # Commit within repository
249
- let(:blob) {collector.get_blob(sha, file)}
227
+ let(:sha) { "695b487432e8a1ede765b4e3efda088ab87a77f8" } # Commit within repository
228
+ subject { collector.get_blob(sha, file) }
250
229
 
251
230
  context "with valid blob" do
252
231
  let(:file) {{:file => "Gemfile.lock"}}
253
- it {blob.instance_of?(Grit::Blob).should be_true}
254
- it {blob.name.should == file[:file].split(File::Separator).last}
255
- end
256
-
257
- context "with invalid blob" do
258
- let(:file) {{:file => "dir/nothing.rb"}}
259
- it {blob.should.nil?}
232
+ it { should be_a Grit::Blob }
233
+ its(:name) { should == File.basename(file[:file]) }
260
234
  end
261
235
 
262
236
  context "with deleted file" do
263
237
  let(:file) {{:file => "spec/collector_spec.rb"}}
264
- it {blob.instance_of?(Grit::Blob).should be_true}
265
- it {blob.name.should == file[:file].split(File::Separator).last}
238
+ it { should be_a Grit::Blob }
239
+ its(:name) { should == File.basename(file[:file]) }
240
+ end
241
+
242
+ context "with invalid blob" do
243
+ let(:file) {{:file => "dir/nothing.rb"}}
244
+ it { should be_nil }
266
245
  end
267
246
  end
268
247
 
@@ -1,17 +1,17 @@
1
1
  require 'spec_helper'
2
+ require 'fileutils'
2
3
  include GitStatistics
3
4
 
4
5
  describe Commits do
5
- let(:verbose) {false}
6
6
  let(:limit) {100}
7
7
  let(:fresh) {true}
8
8
  let(:pretty) {false}
9
- let(:collector) {Collector.new(verbose, limit, fresh, pretty)}
9
+ let(:collector) {Collector.new(limit, fresh, pretty)}
10
10
 
11
11
  let(:commits) {collector.commits}
12
12
 
13
13
  let(:fixture_file) {"multiple_authors.json"}
14
- let(:save_file) {collector.commits_path + "0.json"}
14
+ let(:save_file) { File.join(collector.commits_path, "0.json") }
15
15
  let(:email) {false}
16
16
  let(:merge) {false}
17
17
  let(:sort) {:commits}
@@ -21,56 +21,66 @@ describe Commits do
21
21
  commits.author_top_n_type(sort)
22
22
  end
23
23
 
24
+ describe "#files_in_path" do
25
+ let(:path) { '/tmp/example' }
26
+ subject { commits.files_in_path }
27
+ before do
28
+ FileUtils.mkdir_p(path)
29
+ Dir.chdir(path) do
30
+ FileUtils.touch '0.json'
31
+ FileUtils.touch '1.json'
32
+ end
33
+ commits.stub(:path) { path }
34
+ end
35
+ after do
36
+ FileUtils.rm_rf(path)
37
+ end
38
+ its(:count) { should == 2 }
39
+ it { should_not include '.' }
40
+ it { should_not include '..' }
41
+ end
42
+
24
43
  describe "#flush_commits" do
25
- let(:commits) {collector.commits.load(fixture(fixture_file))}
44
+ let(:commits) {collector.commits.load(fixture(fixture_file).file)}
45
+
46
+ def commit_size_changes_from(beginning, opts = {})
47
+ commits.size.should == beginning
48
+ commits.flush_commits(opts[:force] || false)
49
+ commits.size.should == opts[:to]
50
+ end
26
51
 
27
52
  context "with commits exceeding limit" do
28
53
  let(:limit) {2}
29
- it do
30
- commits.size.should == 3
31
- commits.flush_commits
32
- commits.size.should == 0
33
- end
54
+ it { commit_size_changes_from(3, to: 0) }
34
55
  end
35
56
 
36
57
  context "with commits equal to limit" do
37
58
  let(:limit) {3}
38
- it do
39
- commits.size.should == 3
40
- commits.flush_commits
41
- commits.size.should == 0
42
- end
59
+ it { commit_size_changes_from(3, to: 0) }
43
60
  end
44
61
 
45
62
  context "with commits less than limit" do
46
63
  let(:limit) {5}
47
- it do
48
- commits.size.should == 3
49
- commits.flush_commits
50
- commits.size.should == 3
51
- end
64
+ it { commit_size_changes_from(3, to: 3) }
52
65
  end
53
66
 
54
67
  context "with commits less than limit but forced" do
55
68
  let(:limit) {5}
56
- it do
57
- commits.size.should == 3
58
- commits.flush_commits(true)
59
- commits.size.should == 0
60
- end
69
+ it { commit_size_changes_from(3, to: 0, force: true) }
61
70
  end
62
71
  end
63
72
 
64
73
  describe "#process_commits" do
65
- let(:commits) {collector.commits.load(fixture(fixture_file))}
74
+ let(:commits) {collector.commits.load(fixture(fixture_file).file)}
66
75
  let(:type) {:author}
76
+ subject { commits.stats[author_name] }
77
+
78
+ before do
79
+ commits.process_commits(type, merge)
80
+ end
67
81
 
68
82
  context "with merge" do
69
83
  let(:merge) {true}
70
- subject {
71
- commits.process_commits(type, merge)
72
- commits.stats[author_name]
73
- }
74
84
 
75
85
  context "on first author" do
76
86
  let(:author_name) {"Kevin Jalbert"}
@@ -102,10 +112,6 @@ describe Commits do
102
112
 
103
113
  context "without merge" do
104
114
  let(:merge) {false}
105
- subject {
106
- commits.process_commits(type, merge)
107
- commits.stats[author_name]
108
- }
109
115
 
110
116
  context "on first author" do
111
117
  let(:author_name) {"Kevin Jalbert"}
@@ -136,11 +142,11 @@ describe Commits do
136
142
 
137
143
  describe "#author_top_n_type" do
138
144
  let(:sort) {:deletions}
145
+ subject {stats[author]}
139
146
 
140
147
  context "with valid data" do
141
148
  context "on first author" do
142
- author = "John Smith"
143
- subject {stats[author]}
149
+ let(:author) { 'John Smith' }
144
150
  it {stats.has_key?(author).should be_true}
145
151
  it {subject[:commits].should == 1}
146
152
  it {subject[:deletions].should == 16}
@@ -151,8 +157,7 @@ describe Commits do
151
157
  end
152
158
 
153
159
  context "on second author" do
154
- author = "Kevin Jalbert"
155
- subject {stats[author]}
160
+ let(:author) { "Kevin Jalbert" }
156
161
  it {stats.has_key?(author).should be_true}
157
162
  it {subject[:commits].should == 1}
158
163
  it {subject[:additions].should == 73}
@@ -170,22 +175,22 @@ describe Commits do
170
175
 
171
176
  context "with invalid type" do
172
177
  let(:sort) {:wrong}
173
- it {stats.should.nil?}
178
+ it { stats.should be_nil }
174
179
  end
175
180
 
176
181
  context "with invalid data" do
177
182
  let(:fixture_file) {nil}
178
- it {stats.should.nil?}
183
+ it { stats.should be_nil }
179
184
  end
180
185
  end
181
186
 
182
187
  describe "#calculate_statistics" do
183
188
  let(:fixture_file) {"single_author_pretty.json"}
189
+ subject {stats[author]}
184
190
 
185
191
  context "with email" do
186
192
  let(:email) {true}
187
- author = "kevin.j.jalbert@gmail.com"
188
- subject {stats[author]}
193
+ let(:author) { "kevin.j.jalbert@gmail.com" }
189
194
 
190
195
  it {stats.has_key?(author).should be_true}
191
196
  it {subject[:commits].should == 1}
@@ -202,8 +207,7 @@ describe Commits do
202
207
 
203
208
  context "with merge" do
204
209
  let(:merge) {true}
205
- author = "Kevin Jalbert"
206
- subject {stats[author]}
210
+ let(:author) { 'Kevin Jalbert' }
207
211
 
208
212
  it {stats.has_key?(author).should be_true}
209
213
  it {subject[:commits].should == 2}
@@ -334,10 +338,10 @@ describe Commits do
334
338
  let(:pretty) {true}
335
339
 
336
340
  it do
337
- commits.load(fixture(fixture_file))
341
+ commits.load(fixture(fixture_file).file)
338
342
  commits.save("tmp.json", pretty)
339
343
 
340
- same = FileUtils.compare_file("tmp.json", fixture(fixture_file))
344
+ same = FileUtils.compare_file("tmp.json", fixture(fixture_file).file)
341
345
  FileUtils.remove_file("tmp.json")
342
346
 
343
347
  same.should be_true
@@ -349,10 +353,10 @@ describe Commits do
349
353
  let(:pretty) {false}
350
354
 
351
355
  it do
352
- commits.load(fixture(fixture_file))
356
+ commits.load(fixture(fixture_file).file)
353
357
  commits.save("tmp.json", pretty)
354
358
 
355
- same = FileUtils.compare_file("tmp.json", fixture(fixture_file))
359
+ same = FileUtils.compare_file("tmp.json", fixture(fixture_file).file)
356
360
  FileUtils.remove_file("tmp.json")
357
361
 
358
362
  same.should be_true