metior 0.1.4 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. data/.travis.yml +2 -0
  2. data/.yardopts +1 -0
  3. data/Changelog.md +23 -0
  4. data/Gemfile +1 -17
  5. data/Gemfile.lock +28 -21
  6. data/README.md +66 -20
  7. data/lib/metior.rb +30 -14
  8. data/lib/metior/actor.rb +28 -22
  9. data/lib/metior/auto_include_vcs.rb +43 -0
  10. data/lib/metior/collections/actor_collection.rb +97 -0
  11. data/lib/metior/collections/collection.rb +84 -0
  12. data/lib/metior/collections/commit_collection.rb +309 -0
  13. data/lib/metior/commit.rb +128 -48
  14. data/lib/metior/errors.rb +2 -2
  15. data/lib/metior/git/commit.rb +32 -31
  16. data/lib/metior/git/repository.rb +71 -6
  17. data/lib/metior/github/commit.rb +5 -16
  18. data/lib/metior/github/repository.rb +68 -40
  19. data/lib/metior/report.rb +139 -0
  20. data/lib/metior/report/view.rb +120 -0
  21. data/lib/metior/report/view_helper.rb +47 -0
  22. data/lib/metior/repository.rb +225 -56
  23. data/lib/metior/vcs.rb +12 -3
  24. data/lib/metior/version.rb +1 -1
  25. data/metior.gemspec +28 -26
  26. data/reports/default.rb +17 -0
  27. data/reports/default/images/favicon.png +0 -0
  28. data/reports/default/stylesheets/default.css +128 -0
  29. data/reports/default/templates/actor/minimal.mustache +1 -0
  30. data/reports/default/templates/commit/minimal.mustache +1 -0
  31. data/reports/default/templates/index.mustache +27 -0
  32. data/reports/default/templates/most_significant_authors.mustache +11 -0
  33. data/reports/default/templates/most_significant_commits.mustache +13 -0
  34. data/reports/default/templates/repository_information.mustache +17 -0
  35. data/reports/default/templates/top_committers.mustache +11 -0
  36. data/reports/default/views/index.rb +33 -0
  37. data/reports/default/views/most_significant_authors.rb +19 -0
  38. data/reports/default/views/most_significant_commits.rb +19 -0
  39. data/reports/default/views/repository_information.rb +47 -0
  40. data/reports/default/views/top_committers.rb +21 -0
  41. data/test/fixtures.rb +54 -36
  42. data/test/helper.rb +10 -3
  43. data/test/{test_class_loading.rb → test_1st_class_loading.rb} +1 -1
  44. data/test/test_actor_colletion.rb +78 -0
  45. data/test/test_collection.rb +61 -0
  46. data/test/test_commit_collection.rb +139 -0
  47. data/test/test_git.rb +58 -5
  48. data/test/test_github.rb +52 -9
  49. data/test/test_metior.rb +22 -1
  50. data/test/test_report.rb +49 -0
  51. data/test/test_repository.rb +46 -9
  52. data/test/test_vcs.rb +36 -13
  53. metadata +105 -43
@@ -0,0 +1,47 @@
1
+ # This code is free software; you can redistribute it and/or modify it under
2
+ # the terms of the new BSD License.
3
+ #
4
+ # Copyright (c) 2011, Sebastian Staudt
5
+
6
+ class Metior::Report
7
+
8
+ # This helper module implements generic functionality that is included in all
9
+ # report views
10
+ #
11
+ # @author Sebastian Staudt
12
+ module ViewHelper
13
+
14
+ # Increases the counter by one and returns it
15
+ #
16
+ # This can, for example, be used to count the values in an array that is
17
+ # iterated in a view.
18
+ #
19
+ # @return [Fixnum] The current counter value
20
+ # @see #reset_count
21
+ def count
22
+ @count ||= 0
23
+ @count += 1
24
+ end
25
+
26
+ # Returns whether the current counter value is even or odd
27
+ #
28
+ # This is specifically useful for e.g. generating alternating colors of
29
+ # table rows in the output of the view.
30
+ #
31
+ # @return ['even', 'odd'] `'even'` if the current counter value is even,
32
+ # `'odd'` otherwise.
33
+ # @see #reset_count
34
+ def even_odd
35
+ (count % 2 == 0) ? 'even' : 'odd'
36
+ end
37
+
38
+ # Resets the current counter to 0
39
+ #
40
+ # @see #count
41
+ def reset_count
42
+ @count = 0
43
+ end
44
+
45
+ end
46
+
47
+ end
@@ -3,7 +3,9 @@
3
3
  #
4
4
  # Copyright (c) 2011, Sebastian Staudt
5
5
 
6
- require 'metior/actor'
6
+ require 'metior/auto_include_vcs'
7
+ require 'metior/collections/actor_collection'
8
+ require 'metior/collections/commit_collection'
7
9
 
8
10
  module Metior
9
11
 
@@ -14,6 +16,8 @@ module Metior
14
16
  # @author Sebastian Staudt
15
17
  class Repository
16
18
 
19
+ include AutoIncludeVCS
20
+
17
21
  # @return [String] The file system path of this repository
18
22
  attr_reader :path
19
23
 
@@ -21,10 +25,26 @@ module Metior
21
25
  #
22
26
  # @param [String] path The file system path of the repository
23
27
  def initialize(path)
24
- @authors = {}
25
- @commits = {}
26
- @committers = {}
27
- @path = path
28
+ @actors = {}
29
+ @commits = {}
30
+ @description = nil
31
+ @name = nil
32
+ @path = path
33
+ @refs = {}
34
+ end
35
+
36
+ # Returns a single VCS specific actor object from the raw data of the actor
37
+ # provided by the VCS implementation
38
+ #
39
+ # The actor object is either created from the given raw data or retrieved
40
+ # from the cache using the VCS specific unique identifier of the actor.
41
+ #
42
+ # @param [Object] actor The raw data of the actor provided by the VCS
43
+ # @return [Actor] A object representing the actor
44
+ # @see Actor.id_for
45
+ def actor(actor)
46
+ id = self.class::Actor.id_for(actor)
47
+ @actors[id] ||= self.class::Actor.new(self, actor)
28
48
  end
29
49
 
30
50
  # Returns all authors from the given commit range in a hash where the IDs
@@ -38,15 +58,20 @@ module Metior
38
58
  # (`'master..development'`), a range (`'master'..'development'`) or
39
59
  # as a single ref (`'master'`). A single ref name means all commits
40
60
  # reachable from that ref.
41
- # @return [Hash<String, Actor>] All authors from the given commit range
61
+ # @return [ActorCollection] All authors from the given commit range
42
62
  # @see #commits
43
63
  def authors(range = self.class::DEFAULT_BRANCH)
44
- range = parse_range range
45
- commits(range) if @authors[range].nil?
46
- @authors[range]
64
+ commits(range).authors
47
65
  end
48
66
  alias_method :contributors, :authors
49
67
 
68
+ # Returns the names of all branches of this repository
69
+ #
70
+ # @return [Array<String>] The names of all branches
71
+ def branches
72
+ load_branches.each { |name, id| @refs[name] = id }.keys.sort
73
+ end
74
+
50
75
  # Loads all commits including their committers and authors from the given
51
76
  # commit range
52
77
  #
@@ -55,22 +80,38 @@ module Metior
55
80
  # (`'master..development'`), a range (`'master'..'development'`) or
56
81
  # as a single ref (`'master'`). A single ref name means all commits
57
82
  # reachable from that ref.
58
- # @return [Array<Commit>] All commits from the given commit range
83
+ # @return [CommitCollection] All commits from the given commit range
59
84
  def commits(range = self.class::DEFAULT_BRANCH)
60
85
  range = parse_range range
61
- if @commits[range].nil?
62
- @authors[range] = {}
63
- @committers[range] = {}
64
- @commits[range] = []
65
- load_commits(range).each do |commit|
66
- commit = self.class::Commit.new(self, range, commit)
67
- @commits[range] << commit
68
- @authors[range][commit.author.id] = commit.author
69
- @committers[range][commit.committer.id] = commit.committer
86
+ commits = cached_commits range
87
+
88
+ if commits.empty?
89
+ base_commit, raw_commits = load_commits(range)
90
+ commits = commits + build_commits(raw_commits)
91
+ unless base_commit.nil?
92
+ base_commit = self.class::Commit.new(self, base_commit)
93
+ base_commit.add_child commits.last.id
94
+ @commits[base_commit.id] = base_commit
95
+ end
96
+ else
97
+ if range.first == ''
98
+ unless commits.last.parents.empty?
99
+ raw_commits = load_commits(''..commits.last.id).last
100
+ commits += build_commits raw_commits[0..-2]
101
+ end
102
+ else
103
+ if commits.first.id != range.last
104
+ raw_commits = load_commits(commits.first.id..range.last).last
105
+ commits = build_commits(raw_commits) + commits
106
+ end
107
+ unless commits.last.parents.include? range.first
108
+ raw_commits = load_commits(range.first..commits.last.id).last
109
+ commits += build_commits raw_commits
110
+ end
70
111
  end
71
112
  end
72
113
 
73
- @commits[range]
114
+ CommitCollection.new commits
74
115
  end
75
116
 
76
117
  # Returns all committers from the given commit range in a hash where the
@@ -84,15 +125,25 @@ module Metior
84
125
  # (`'master..development'`), a range (`'master'..'development'`) or
85
126
  # as a single ref (`'master'`). A single ref name means all commits
86
127
  # reachable from that ref.
87
- # @return [Hash<String, Actor>] All committers from the given commit range
128
+ # @return [ActorCollection] All committers from the given commit range
88
129
  # @see #commits
89
130
  def committers(range = self.class::DEFAULT_BRANCH)
90
- range = parse_range range
91
- commits(range) if @committers[range].nil?
92
- @committers[range]
131
+ commits(range).committers
93
132
  end
94
133
  alias_method :collaborators, :committers
95
134
 
135
+ # Returns the description of the project contained in the repository
136
+ #
137
+ # This will load the description through a VCS specific mechanism if
138
+ # required.
139
+ #
140
+ # @return [String] The description of the project in the repository
141
+ # @see #load_description
142
+ def description
143
+ load_description if @description.nil?
144
+ @description
145
+ end
146
+
96
147
  # This evaluates basic statistics about the files in a given commit range.
97
148
  #
98
149
  # @example
@@ -122,7 +173,7 @@ module Metior
122
173
  support! :line_stats
123
174
 
124
175
  stats = {}
125
- commits(range).each do |commit|
176
+ commits(range).each_value do |commit|
126
177
  commit.added_files.each do |file|
127
178
  stats[file] = { :modifications => 0 } unless stats.key? file
128
179
  stats[file][:added_date] = commit.authored_date
@@ -160,18 +211,20 @@ module Metior
160
211
  # @return [Hash<Symbol, Array>] Added lines are returned in an `Array`
161
212
  # assigned to key `:additions`, deleted lines are assigned to
162
213
  # `:deletions`
163
- # @see Commit#additions
164
- # @see Commit#deletions
214
+ # @see CommitCollection#line_history
165
215
  def line_history(range = self.class::DEFAULT_BRANCH)
166
- support! :line_stats
167
-
168
- history = { :additions => [], :deletions => [] }
169
- commits(range).reverse.each do |commit|
170
- history[:additions] << commit.additions
171
- history[:deletions] << -commit.deletions
172
- end
216
+ commits(range).line_history
217
+ end
173
218
 
174
- history
219
+ # Returns the name of the project contained in the repository
220
+ #
221
+ # This will load the name through a VCS specific mechanism if required.
222
+ #
223
+ # @return [String] The name of the project in the repository
224
+ # @see #load_name
225
+ def name
226
+ load_name if @name.nil?
227
+ @name
175
228
  end
176
229
 
177
230
  # Returns a list of authors with the biggest impact on the repository, i.e.
@@ -187,11 +240,7 @@ module Metior
187
240
  # @return [Array<Actor>] An array of the given number of the most
188
241
  # significant authors in the given commit range
189
242
  def significant_authors(range = self.class::DEFAULT_BRANCH, count = 3)
190
- support! :line_stats
191
-
192
- authors = authors(range).values.sort_by { |author| author.modifications }
193
- count = [count, authors.size].min
194
- authors[-count..-1].reverse
243
+ authors(range).most_significant(count)
195
244
  end
196
245
  alias_method :significant_contributors, :significant_authors
197
246
 
@@ -208,11 +257,14 @@ module Metior
208
257
  # @return [Array<Actor>] An array of the given number of the most
209
258
  # significant commits in the given commit range
210
259
  def significant_commits(range = self.class::DEFAULT_BRANCH, count = 10)
211
- support! :line_stats
260
+ commits(range).most_significant(count)
261
+ end
212
262
 
213
- commits = commits(range).sort_by { |commit| commit.modifications }
214
- count = [count, commits.size].min
215
- commits[-count..-1].reverse
263
+ # Returns the names of all tags of this repository
264
+ #
265
+ # @return [Array<String>] The names of all tags
266
+ def tags
267
+ load_tags.each { |name, id| @refs[name] = id }.keys.sort
216
268
  end
217
269
 
218
270
  # Returns a list of top contributors in the given commit range
@@ -230,17 +282,109 @@ module Metior
230
282
  # in the given commit range
231
283
  # @see #authors
232
284
  def top_authors(range = self.class::DEFAULT_BRANCH, count = 3)
233
- authors = authors(range).values.sort_by { |author| author.commits.size }
234
- count = [count, authors.size].min
235
- authors[-count..-1].reverse
285
+ authors(range).top(count)
236
286
  end
237
287
  alias_method :top_contributors, :top_authors
238
288
 
239
289
  private
240
290
 
291
+ # Builds VCS specific commit objects for each given commit's raw data that
292
+ # is provided by the VCS implementation
293
+ #
294
+ # The raw data will be transformed into commit objects that will also be
295
+ # saved into the commit cache. Authors and committers of the given commits
296
+ # will be created and stored into the cache or loaded from the cache if
297
+ # they already exist. Additionally this method will establish an
298
+ # association between the commits and their children.
299
+ #
300
+ # @param [Array<Object>] raw_commits The commits' raw data provided by the
301
+ # VCS implementation
302
+ # @return [Array<Commit>] The commit objects representing the given commits
303
+ # @see Commit
304
+ # @see Commit#add_child
305
+ def build_commits(raw_commits)
306
+ child_commit_id = nil
307
+ raw_commits.map do |commit|
308
+ commit = self.class::Commit.new(self, commit)
309
+ commit.add_child child_commit_id unless child_commit_id.nil?
310
+ child_commit_id = commit.id
311
+ @commits[commit.id] = commit
312
+ @actors[commit.author.id] ||= commit.author
313
+ commit
314
+ end
315
+ end
316
+
317
+ # Tries to retrieve as many commits as possible in the given commit range
318
+ # from the commit cache
319
+ #
320
+ # This method calls itself recursively to walk the given commit range
321
+ # either from the start to the end or vice versa depending on which commit
322
+ # could be found in the cache.
323
+ #
324
+ # @param [Range] range The range of commits which should be retrieved from
325
+ # the cache. This may be given a range of commit IDs
326
+ # (`'master'..'development'`).
327
+ # @return [Array<Commit>] A list of commit objects that could be retrieved
328
+ # from the cache
329
+ # @see Commit#children
330
+ def cached_commits(range)
331
+ commits = []
332
+
333
+ direction = nil
334
+ if @commits.key? range.last
335
+ current_commits = [@commits[range.last]]
336
+ direction = :parents
337
+ elsif @commits.key? range.first
338
+ current_commits = [@commits[range.first]]
339
+ direction = :children
340
+ end
341
+
342
+ unless direction.nil?
343
+ while !current_commits.empty? do
344
+ new_commits = []
345
+ current_commits.each do |commit|
346
+ new_commits += commit.send direction
347
+ commits << commit if commit.id != range.first
348
+ if direction == :parents && new_commits.include?(range.first)
349
+ new_commits = []
350
+ break
351
+ end
352
+ end
353
+ unless new_commits.include? range.first
354
+ current_commits = new_commits.uniq.map do |commit|
355
+ commit = @commits[commit]
356
+ commits.include?(commit) ? nil : commit
357
+ end.compact
358
+ end
359
+ end
360
+ end
361
+
362
+ commits.sort_by { |c| c.committed_date }.reverse
363
+ end
364
+
365
+ # Returns the unique identifier for the commit the given reference – like a
366
+ # branch name – is pointing to
367
+ #
368
+ # @abstract Has to be implemented by VCS subclasses
369
+ # @param [String] ref A symbolic reference name
370
+ # @return [Object] The unique identifier of the commit the reference is
371
+ # pointing to
372
+ def id_for_ref(ref)
373
+ raise NotImplementedError
374
+ end
375
+
376
+ # Loads all branches and the corresponding commit IDs of this repository
377
+ #
378
+ # @abstract Has to be implemented by VCS specific subclasses
379
+ # @return [Hash<String, Object>] The names of all branches and the
380
+ # corresponding commit IDs
381
+ def load_branches
382
+ raise NotImplementedError
383
+ end
384
+
241
385
  # Loads all commits from the given commit range
242
386
  #
243
- # @abstract It has to be implemented by VCS specific subclasses
387
+ # @abstract Has to be implemented by VCS specific subclasses
244
388
  # @param [String, Range] range The range of commits for which the commits
245
389
  # should be retrieved. This may be given as a string
246
390
  # (`'master..development'`), a range (`'master'..'development'`) or
@@ -251,21 +395,46 @@ module Metior
251
395
  raise NotImplementedError
252
396
  end
253
397
 
254
- # Parses a string with a single ref name into
398
+ # Loads the description of the project contained in the repository
399
+ #
400
+ # @abstract Has to be implemented by VCS specific subclasses
401
+ # @see #description
402
+ def load_description
403
+ raise NotImplementedError
404
+ end
405
+
406
+ # Loads the name of the project contained in the repository
407
+ #
408
+ # @abstract Has to be implemented by VCS specific subclasses
409
+ # @see #description
410
+ def load_name
411
+ raise NotImplementedError
412
+ end
413
+
414
+ # Loads all tags and the corresponding commit IDs of this repository
255
415
  #
256
- # If a range is given it will be returned as-is.
416
+ # @abstract Has to be implemented by VCS specific subclasses
417
+ # @return [Hash<String, Object>] The names of all tags and the
418
+ # corresponding commit IDs
419
+ def load_tags
420
+ raise NotImplementedError
421
+ end
422
+
423
+ # Parses a string or range of commit IDs or ref names into the coresponding
424
+ # range of unique commit IDs
257
425
  #
258
426
  # @param [String, Range] range The string that should be parsed for a range
259
427
  # or an existing range
260
- # @return [Range] The range parsed from a string or unchanged from the
261
- # given parameter
428
+ # @return [Range] The range of commit IDs parsed from the given parameter
429
+ # @see #id_for_ref
262
430
  def parse_range(range)
263
- if range.is_a? Range
264
- range
265
- else
431
+ unless range.is_a? Range
266
432
  range = range.to_s.split '..'
267
- ((range.size == 1) ? '' : range.first)..range.last
433
+ range = ((range.size == 1) ? '' : range.first)..range.last
268
434
  end
435
+
436
+ range = id_for_ref(range.first)..range.last if range.first != ''
437
+ range.first..id_for_ref(range.last)
269
438
  end
270
439
 
271
440
  end
@@ -3,7 +3,6 @@
3
3
  #
4
4
  # Copyright (c) 2011, Sebastian Staudt
5
5
 
6
- require 'metior'
7
6
  require 'metior/errors'
8
7
 
9
8
  module Metior
@@ -41,6 +40,8 @@ module Metior
41
40
 
42
41
  # This module provided class methods for VCS implementation `Module`s that
43
42
  # implement smart auto-loading of dependencies and classes.
43
+ #
44
+ # @author Sebastian Staudt
44
45
  module ClassMethods
45
46
 
46
47
  # Missing constants may indicate
@@ -128,7 +129,7 @@ module Metior
128
129
  # its implementation)
129
130
  # @see #supports?
130
131
  def support!(feature)
131
- raise UnsupportedError unless supports? feature
132
+ raise UnsupportedError.new(vcs) unless supports? feature
132
133
  end
133
134
 
134
135
  # Checks if a specific feature is supported by the VCS (or its
@@ -137,7 +138,15 @@ module Metior
137
138
  # @return [true, false] `true` if the feature is supported
138
139
  # @see ClassMethods#supports?
139
140
  def supports?(feature)
140
- singleton_class.included_modules.first.supports? feature
141
+ vcs.supports? feature
142
+ end
143
+
144
+ # Returns the VCS module that is included by this object
145
+ #
146
+ # @return [Metior::VCS] The VCS implementation module of this object
147
+ # @see Metior.vcs_types
148
+ def vcs
149
+ Metior.vcs_types[singleton_class::NAME.to_sym]
141
150
  end
142
151
 
143
152
  end