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,43 @@
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
+ module Metior
7
+
8
+ # This module should be included by all classes that have VCS implementation
9
+ # specific subclasses, like {Repository}.
10
+ #
11
+ # @author Sebastian Staudt
12
+ module AutoIncludeVCS
13
+
14
+ # Will automatically include the class method `inherited`
15
+ #
16
+ # @see ClassMethods
17
+ def self.included(mod)
18
+ mod.extend ClassMethods
19
+ end
20
+
21
+ # This module implements the class method `inherited` that will handle the
22
+ # automatic inclusion of the VCS implementation `Module`
23
+ #
24
+ # @author Sebastian Staudt
25
+ module ClassMethods
26
+
27
+ # This method will automatically include the VCS implementation `Module`
28
+ # corresponding to the subclass that has just been defined
29
+ #
30
+ # @param [Class] subclass The subclass that has been defined
31
+ def inherited(subclass)
32
+ vcs = Object
33
+ subclass.to_s.split('::')[0..-2].each do |mod|
34
+ vcs = vcs.const_get mod.to_sym
35
+ end
36
+ subclass.send :include, vcs if vcs.ancestors.include? VCS
37
+ end
38
+
39
+ end
40
+
41
+ end
42
+
43
+ end
@@ -0,0 +1,97 @@
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
+ require 'metior/collections/collection'
7
+ require 'metior/collections/commit_collection'
8
+
9
+ module Metior
10
+
11
+ # This class implements a collection of actors and provides functionality
12
+ # specific to actors.
13
+ #
14
+ # @author Sebastian Staudt
15
+ # @see Actor
16
+ class ActorCollection < Collection
17
+
18
+ # Returns the commits authored by all or a specific actor in this
19
+ # collection
20
+ #
21
+ # @param [Object] actor_id The ID of the actor, if only the commits of a
22
+ # specific actor should be returned
23
+ # @return [CommitCollection] All commits authored by the actors in this
24
+ # collection or by a specific actor
25
+ def authored_commits(actor_id = nil)
26
+ load_commits :authored_commits, actor_id
27
+ end
28
+ alias_method :commits, :authored_commits
29
+
30
+ # Returns the commits committed by all or a specific actor in this
31
+ # collection
32
+ #
33
+ # @param [Object] actor_id The ID of the actor, if only the commits of a
34
+ # specific actor should be returned
35
+ # @return [CommitCollection] All commits committed by the actors in this
36
+ # collection or by a specific actor
37
+ def committed_commits(actor_id = nil)
38
+ load_commits :committed_commits, actor_id
39
+ end
40
+
41
+ # Returns up to the given number of actors in this collection with the
42
+ # biggest impact on the repository, i.e. changing the most code
43
+ #
44
+ # @param [Numeric] count The number of actors to return
45
+ # @return [ActorCollection] The given number of actors ordered by impact
46
+ # @see Actor#modifications
47
+ def most_significant(count = 3)
48
+ first.support! :line_stats
49
+
50
+ authors = ActorCollection.new
51
+ sort_by { |author| -author.modifications }.each do |author|
52
+ authors << author
53
+ break if authors.size == count
54
+ end
55
+ authors
56
+ end
57
+
58
+ # Returns up to the given number of actors in this collection with the
59
+ # most commits
60
+ #
61
+ # @param [Numeric] count The number of actors to return
62
+ # @return [ActorCollection] The given number of actors ordered by commit
63
+ # count
64
+ # @see Actor#commits
65
+ def top(count = 3)
66
+ authors = ActorCollection.new
67
+ sort_by { |author| -author.authored_commits.size }.each do |author|
68
+ authors << author
69
+ break if authors.size == count
70
+ end
71
+ authors
72
+ end
73
+
74
+ private
75
+
76
+ # Loads the commits authored or committed by all actors in this collection
77
+ # or a specific actor
78
+ #
79
+ # @param [:authored_commits, :committed_commits] commit_type The type of
80
+ # commits to load
81
+ # @param [Object] actor_id The ID of the actor, if only the commits of a
82
+ # specific actor should be returned
83
+ # @return [CommitCollection] All commits authored or committed by the
84
+ # actors in this collection or by a specific actor
85
+ def load_commits(commit_type, actor_id = nil)
86
+ commits = CommitCollection.new
87
+ if actor_id.nil?
88
+ each { |actor| commits.merge! actor.send(commit_type) }
89
+ elsif key? actor_id
90
+ commits = self[actor_id].send commit_type
91
+ end
92
+ commits
93
+ end
94
+
95
+ end
96
+
97
+ end
@@ -0,0 +1,84 @@
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
+ HASH_CLASS = if RUBY_VERSION.match(/^1\.8/)
7
+ require 'hashery/ordered_hash'
8
+ OrderedHash
9
+ else
10
+ Hash
11
+ end
12
+
13
+ module Metior
14
+
15
+ # Represents a hash retaining insertion order
16
+ #
17
+ # On Ruby 1.9 this is a subclass of `Hash` because Ruby 1.9's hashes are
18
+ # already retaining insertion order. For Ruby 1.8 this needs a special
19
+ # parent class `OrderedHash` provided by the Hashery gem.
20
+ #
21
+ # Additionally, it provides some shortcuts to make its interface more
22
+ # array-like.
23
+ #
24
+ # @author Sebastian Staudt
25
+ # @see Hash
26
+ # @see OrderedHash
27
+ class Collection < HASH_CLASS
28
+
29
+ # Creates a new collection with the given objects
30
+ #
31
+ # @param [Array<Object>] objects The objects that should be initially
32
+ # inserted into the collection
33
+ def initialize(objects = [])
34
+ super()
35
+
36
+ objects.each { |obj| self << obj }
37
+ end
38
+
39
+ # Adds an object to this collection
40
+ #
41
+ # The object should provide a `#id` method to generate a key for this
42
+ # object.
43
+ #
44
+ # @param [Object] object The object to add to the collection
45
+ # @return [Collection] The collection itself
46
+ # @see Array#<<
47
+ def <<(object)
48
+ self[object.id] = object
49
+ self
50
+ end
51
+
52
+ # Evaluates the block for each element of the collection
53
+ #
54
+ # @return [Collection] The collection itself
55
+ # @yield [element] Each of the elements of this collection
56
+ # @yieldparam [Object] element The current element of the collection
57
+ def each(&block)
58
+ each_value(&block)
59
+ self
60
+ end
61
+
62
+ # Returns the element that has been added last to this collection
63
+ #
64
+ # @return [Object] The last element of the collection
65
+ # @see Enumerable#last
66
+ def last
67
+ values.last
68
+ end
69
+
70
+ if superclass != Hash
71
+ # Adds all elements of another collection to this one
72
+ #
73
+ # @param [Collection] other_collection The collection to merge into this
74
+ # one
75
+ # @return [Collection] The merged collection
76
+ def merge!(other_collection)
77
+ other_collection.each { |obj| self << obj }
78
+ self
79
+ end
80
+ end
81
+
82
+ end
83
+
84
+ end
@@ -0,0 +1,309 @@
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
+ require 'time'
7
+
8
+ require 'metior/collections/actor_collection'
9
+ require 'metior/collections/collection'
10
+
11
+ module Metior
12
+
13
+ # This class implements a collection of commits and provides functionality
14
+ # specific to commits.
15
+ #
16
+ # @author Sebastian Staudt
17
+ # @see Commit
18
+ class CommitCollection < Collection
19
+
20
+ # Creates a new collection with the given commits
21
+ #
22
+ # @param [Array<Commit>] commits The commits that should be initially
23
+ # inserted into the collection
24
+ def initialize(commits = [])
25
+ @additions = nil
26
+ @deletions = nil
27
+
28
+ super
29
+ end
30
+
31
+ # Adds a commit to this collection
32
+ #
33
+ # @param [Commit] commit The commit to add to this collection
34
+ # @return [CommitCollection] The collection itself
35
+ def <<(commit)
36
+ return self if key? commit.id
37
+
38
+ unless @additions.nil?
39
+ @additions += commit.additions
40
+ @deletions += commit.deletions
41
+ end
42
+
43
+ super
44
+ end
45
+
46
+ # Calculate some predefined activity statistics for the commits in this
47
+ # collection
48
+ #
49
+ # @return [Hash<Symbol, Object>] The calculated statistics for the commits
50
+ # in this collection
51
+ # @see Commit#committed_date
52
+ def activity
53
+ activity = {}
54
+ commit_count = values.size
55
+
56
+ active_days = {}
57
+ each do |commit|
58
+ date = commit.committed_date.utc
59
+ day = Time.utc(date.year, date.month, date.day)
60
+ if active_days.key? day
61
+ active_days[day] += 1
62
+ else
63
+ active_days[day] = 1
64
+ end
65
+ end
66
+
67
+ most_active_day = active_days.sort_by { |day, count| count }.last.first
68
+
69
+ activity[:first_commit_date] = last.committed_date
70
+ activity[:last_commit_date] = first.committed_date
71
+
72
+ age_in_days = (Time.now - activity[:first_commit_date]) / 86400.0
73
+
74
+ activity[:active_days] = active_days
75
+ activity[:most_active_day] = most_active_day
76
+ activity[:commits_per_day] = commit_count / age_in_days
77
+ activity[:commits_per_active_day] = commit_count.to_f / active_days.size
78
+
79
+ activity
80
+ end
81
+
82
+ # Returns the lines of code that have been added by the commits in this
83
+ # collection
84
+ #
85
+ # This will load the line stats from the commits if not done yet.
86
+ #
87
+ # @return [Fixnum] The lines of code that have been added
88
+ # @see #load_line_stats
89
+ def additions
90
+ first.support! :line_stats
91
+
92
+ load_line_stats if @additions.nil?
93
+ @additions
94
+ end
95
+
96
+ # Returns the commits in this collection that have been committed after the
97
+ # given time
98
+ #
99
+ # @param [Time, Date, DateTime, String] date The time to use as the lower
100
+ # limit to filter the commits
101
+ # @return [CommitCollection] The commits that have been committed after the
102
+ # given date
103
+ # @see Commit#committed_date
104
+ # @see Time.parse
105
+ def after(date)
106
+ date = Time.parse date if date.is_a? String
107
+ commits = CommitCollection.new
108
+ each do |commit|
109
+ commits << commit if commit.committed_date > date
110
+ end
111
+ commits
112
+ end
113
+ alias_method :newer, :after
114
+
115
+ # Returns the authors of all or a specific commit in this collection
116
+ #
117
+ # @param [Object] commit_id The ID of the commit, if only the author of a
118
+ # specific commit should be returned
119
+ # @return [ActorCollection] All authors of the commits in this collection
120
+ # or the author of a specific commit
121
+ # @see Commit#author
122
+ def authors(commit_id = nil)
123
+ authors = ActorCollection.new
124
+ if commit_id.nil?
125
+ each { |commit| authors << commit.author }
126
+ elsif key? commit_id
127
+ authors << self[commit_id].author
128
+ end
129
+ authors
130
+ end
131
+
132
+ # Returns the commits in this collection that have been committed before
133
+ # the given time
134
+ #
135
+ # @param [Time, Date, DateTime, String] date The time to use as the upper
136
+ # limit to filter the commits
137
+ # @return [CommitCollection] The commits that have been committed after the
138
+ # given date
139
+ # @see Commit#committed_date
140
+ # @see Time.parse
141
+ def before(date)
142
+ date = Time.parse date if date.is_a? String
143
+ commits = CommitCollection.new
144
+ each do |commit|
145
+ commits << commit if commit.committed_date < date
146
+ end
147
+ commits
148
+ end
149
+ alias_method :older, :before
150
+
151
+ # Returns the list of commits that have been authored by the given authors
152
+ #
153
+ # @param [Array<Actor, Object>] author_ids One or more actual `Actor`
154
+ # instances or IDs of the authors that the commits should be
155
+ # filtered by
156
+ # @return [CommitCollection] The commits that have been authored by the
157
+ # given authors
158
+ # @see Commit#author
159
+ def by(*author_ids)
160
+ author_ids = author_ids.flatten.map do |author_id|
161
+ author_id.is_a?(Actor) ? author_id.id : author_id
162
+ end
163
+ commits = CommitCollection.new
164
+ each do |commit|
165
+ commits << commit if author_ids.include? commit.author.id
166
+ end
167
+ commits
168
+ end
169
+
170
+ # Returns the commits in this collection that change any of the given files
171
+ #
172
+ # @param [Array<String>] files The path of the files to filter commits by
173
+ # @return [CommitCollection] The commits that contain changes to the given
174
+ # files
175
+ # @see Commit#added_files
176
+ # @see Commit#deleted_files
177
+ # @see Commit#modified_files
178
+ def changing(*files)
179
+ first.support! :file_stats
180
+
181
+ commits = CommitCollection.new
182
+ each do |commit|
183
+ commit_files = commit.added_files + commit.deleted_files + commit.modified_files
184
+ commits << commit unless (commit_files & files).empty?
185
+ end
186
+ commits
187
+ end
188
+ alias_method :touching, :changing
189
+
190
+ # Returns the committers of all or a specific commit in this collection
191
+ #
192
+ # @param [Object] commit_id The ID of the commit, if only the committer of
193
+ # a specific commit should be returned
194
+ # @return [ActorCollection] All committers of the commits in this
195
+ # collection or the committer of a specific commit
196
+ # @see Commit#committer
197
+ def committers(commit_id = nil)
198
+ committers = ActorCollection.new
199
+ if commit_id.nil?
200
+ each { |commit| committers << commit.committer }
201
+ elsif key? commit_id
202
+ committers << self[commit_id].committer
203
+ end
204
+ committers
205
+ end
206
+
207
+ # Returns the lines of code that have been deleted by the commits in this
208
+ # collection
209
+ #
210
+ # This will load the line stats from the commits if not done yet.
211
+ #
212
+ # @return [Fixnum] The lines of code that have been deleted
213
+ # @see #load_line_stats
214
+ def deletions
215
+ first.support! :line_stats
216
+
217
+ load_line_stats if @deletions.nil?
218
+ @deletions
219
+ end
220
+
221
+ # This evaluates the changed lines in each commit of this collection
222
+ #
223
+ # For easier use, the values are stored in separate arrays where each
224
+ # number represents the number of changed (i.e. added or deleted) lines in
225
+ # one commit.
226
+ #
227
+ # @example
228
+ # commits.line_history
229
+ # => { :additions => [10, 5, 0], :deletions => [0, -2, -1] }
230
+ # @return [Hash<Symbol, Array>] Added lines are returned in an `Array`
231
+ # assigned to key `:additions`, deleted lines are assigned to
232
+ # `:deletions`
233
+ # @see Commit#additions
234
+ # @see Commit#deletions
235
+ def line_history
236
+ first.support! :line_stats
237
+
238
+ history = { :additions => [], :deletions => [] }
239
+ values.reverse.each do |commit|
240
+ history[:additions] << commit.additions
241
+ history[:deletions] << -commit.deletions
242
+ end
243
+
244
+ history
245
+ end
246
+
247
+ # Returns the total of lines changed by the commits in this collection
248
+ #
249
+ # @return [Fixnum] The total number of lines changed
250
+ # @see #additions
251
+ # @see #deletions
252
+ def modifications
253
+ additions + deletions
254
+ end
255
+
256
+ # Returns the given number of commits with most line changes on the
257
+ # repository
258
+ #
259
+ # @param [Numeric] count The number of commits to return
260
+ # @return [CommitCollection] The given number of commits ordered by impact
261
+ # @see Commit#modifications
262
+ def most_significant(count = 10)
263
+ first.support! :line_stats
264
+
265
+ commits = CommitCollection.new
266
+ sort_by { |commit| -commit.modifications }.each do |commit|
267
+ commits << commit
268
+ break if commits.size == count
269
+ end
270
+ commits
271
+ end
272
+ alias_method :top, :most_significant
273
+
274
+ # Returns the commits in this collection that change at least the given
275
+ # number of lines
276
+ #
277
+ # @param [Numeric] line_count The number of lines that should be
278
+ # changed at least by the commits
279
+ # @return [CommitCollection] The commits that change at least the given
280
+ # number of lines
281
+ # @see Commit#modifications
282
+ def with_impact(line_count)
283
+ first.support! :line_stats
284
+
285
+ commits = CommitCollection.new
286
+ each do |commit|
287
+ commits << commit if commit.modifications >= line_count
288
+ end
289
+ commits
290
+ end
291
+
292
+ private
293
+
294
+ # Loads the line stats for all commits in this collection
295
+ #
296
+ # @see Commit#additions
297
+ # @see Commit#deletions
298
+ def load_line_stats
299
+ @additions = 0
300
+ @deletions = 0
301
+ each do |commit|
302
+ @additions += commit.additions
303
+ @deletions += commit.deletions
304
+ end
305
+ end
306
+
307
+ end
308
+
309
+ end