metior 0.1.4 → 0.2.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 (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