git_statistics 0.5.1 → 0.6.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.
@@ -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