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,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