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
@@ -3,6 +3,8 @@
3
3
  #
4
4
  # Copyright (c) 2011, Sebastian Staudt
5
5
 
6
+ require 'metior/auto_include_vcs'
7
+
6
8
  module Metior
7
9
 
8
10
  # This class represents a commit in a source code repository
@@ -15,11 +17,7 @@ module Metior
15
17
  # @author Sebastian Staudt
16
18
  class Commit
17
19
 
18
- # @return [Array<String>] A list of file paths added in this commit
19
- attr_reader :added_files
20
-
21
- # @return [Fixnum] The lines of code that have been added in this commit
22
- attr_reader :additions
20
+ include AutoIncludeVCS
23
21
 
24
22
  # @return [Actor] This commit's author
25
23
  attr_reader :author
@@ -30,73 +28,116 @@ module Metior
30
28
  # @return [Object] A unique identifier of the commit in the repository
31
29
  attr_reader :id
32
30
 
31
+ # @return [Array<Object>] The unique identifiers of the children of this
32
+ # commit
33
+ attr_reader :children
34
+
33
35
  # @return [Time] The date this commit has been committed
34
36
  attr_reader :committed_date
35
37
 
36
38
  # @return [Actor] This commit's committer
37
39
  attr_reader :committer
38
40
 
39
- # @return [Array<String>] A list of file paths deleted in this commit
40
- attr_reader :deleted_files
41
-
42
- # @return [Fixnum] The lines of code that have been deleted in this commit
43
- attr_reader :deletions
44
-
45
41
  # @return [String] The commit message of this commit
46
42
  attr_reader :message
47
43
 
48
- # @return [Array<String>] A list of file paths modified in this commit
49
- attr_reader :modified_files
50
-
51
- # @return [Range] The commit range this commit belongs to
52
- attr_reader :range
44
+ # @return [Array<Object>] The unique identifiers of one or more more
45
+ # parents of this commit
46
+ attr_reader :parents
53
47
 
54
48
  # @return [Repository] The repository this commit belongs to
55
49
  attr_reader :repo
56
50
 
57
- # Calculate some predefined activity statistics for the given set of
58
- # commits
51
+ # Creates a new commit instance linked to the given repository and branch
59
52
  #
60
- # @param [Array<Commit>] commits The commits to analyze
61
- # @return [Hash<Symbol, Object>] The calculated statistics for the commits
62
- def self.activity(commits)
63
- activity = {}
53
+ # @param [Repository] repo The repository this commit belongs to
54
+ def initialize(repo)
55
+ @additions = nil
56
+ @added_files = nil
57
+ @children = []
58
+ @deletions = nil
59
+ @deleted_files = nil
60
+ @modified_files = nil
61
+ @repo = repo
62
+ end
64
63
 
65
- commit_count = commits.size
64
+ # Sets the author of this commit
65
+ #
66
+ # This also adds the commit to the commits this actor has authored.
67
+ #
68
+ # @param [Object] author The data of the author of this commit
69
+ # @see Actor#authored_commits
70
+ # @see Repository#actor
71
+ def author=(author)
72
+ @author = @repo.actor author
73
+ @author.authored_commits << self
74
+ end
66
75
 
67
- active_days = {}
68
- commits.each do |commit|
69
- date = commit.committed_date.utc
70
- day = Time.utc(date.year, date.month, date.day)
71
- if active_days.key? day
72
- active_days[day] += 1
73
- else
74
- active_days[day] = 1
75
- end
76
- end
76
+ # Adds the unique identifier of a child of this commit to the list of child
77
+ # commits
78
+ #
79
+ # @param [Object] child The unique identifier of the child commit to add
80
+ def add_child(child)
81
+ @children << child
82
+ end
77
83
 
78
- most_active_day = active_days.sort_by { |day, count| count }.last.first
84
+ # Returns the paths of all files that have been modified in this commit
85
+ #
86
+ # This will load the file stats from the repository if not done yet.
87
+ #
88
+ # @return [Array<String>] A list of file paths added in this commit
89
+ # @see #load_file_stats
90
+ def added_files
91
+ load_file_stats if @added_files.nil?
92
+ @added_files
93
+ end
79
94
 
80
- activity[:first_commit_date] = commits.last.committed_date
81
- activity[:last_commit_date] = commits.first.committed_date
95
+ # Returnes the number of lines of code added in this commit
96
+ #
97
+ # @return [Fixnum] The lines of code that have been added
98
+ # @see #load_line_stats
99
+ def additions
100
+ load_line_stats if @additions.nil?
101
+ @additions
102
+ end
82
103
 
83
- age_in_days = (Time.now - activity[:first_commit_date]) / 86400.0
104
+ # Sets the comitter of this commit
105
+ #
106
+ # This also adds the commit to the commits this actor has comitted.
107
+ #
108
+ # @param [Object] committer The data of the comitter of this commit
109
+ # @see Actor#committed_commits
110
+ # @see Repository#actor
111
+ def committer=(committer)
112
+ @committer = @repo.actor committer
113
+ @committer.committed_commits << self
114
+ end
84
115
 
85
- activity[:active_days] = active_days
86
- activity[:most_active_day] = most_active_day
87
- activity[:commits_per_day] = commit_count / age_in_days
88
- activity[:commits_per_active_day] = commit_count.to_f / active_days.size
116
+ # Returns the paths of all files that have been modified in this commit
117
+ #
118
+ # This will load the file stats from the repository if not done yet.
119
+ #
120
+ # @return [Array<String>] A list of file paths deleted in this commit
121
+ # @see #load_file_stats
122
+ def deleted_files
123
+ load_file_stats if @deleted_files.nil?
124
+ @deleted_files
125
+ end
89
126
 
90
- activity
127
+ # Returnes the number of lines of code deleted in this commit
128
+ #
129
+ # @return [Fixnum] The lines of code that have been deleted
130
+ # @see #load_line_stats
131
+ def deletions
132
+ load_line_stats if @deletions.nil?
133
+ @deletions
91
134
  end
92
135
 
93
- # Creates a new commit instance linked to the given repository and branch
136
+ # Returns whether this commits is a merge commit
94
137
  #
95
- # @param [Repository] repo The repository this commit belongs to
96
- # @param [String] range The commit range this commit belongs to
97
- def initialize(repo, range)
98
- @repo = repo
99
- @range = range
138
+ # @return [Boolean] `true` if this commit is a merge commit
139
+ def merge?
140
+ @parents.size > 1
100
141
  end
101
142
 
102
143
  # Returns the total of changed lines in this commit
@@ -106,6 +147,17 @@ module Metior
106
147
  additions + deletions
107
148
  end
108
149
 
150
+ # Returns the paths of all files that have been modified in this commit
151
+ #
152
+ # This will load the file stats from the repository if not done yet.
153
+ #
154
+ # @return [Array<String>] A list of file paths modified in this commit
155
+ # @see #load_file_stats
156
+ def modified_files
157
+ load_file_stats if @modified_files.nil?
158
+ @modified_files
159
+ end
160
+
109
161
  # Returns the subject line of the commit message, i.e. the first line
110
162
  #
111
163
  # @return [String] The subject of the commit
@@ -113,6 +165,34 @@ module Metior
113
165
  @message.split(/$/).first
114
166
  end
115
167
 
168
+ # Creates a string representation for this commit without recursing into
169
+ # actor and repository details
170
+ #
171
+ # @return [String] A minimal string representation for this commit
172
+ def inspect
173
+ '#<%s:0x%x: @author="%s" @committer="%s" @id="%s" @repo="%s" @subject="%s">' %
174
+ [
175
+ self.class.name, __id__ * 2, @author.id, @committer.id, @id,
176
+ @repo.path, subject
177
+ ]
178
+ end
179
+
180
+ protected
181
+
182
+ # Loads the file stats for this commit from the repository
183
+ #
184
+ # @abstract Has to be implemented by VCS specific subclasses
185
+ def load_file_stats
186
+ raise NotImplementedError
187
+ end
188
+
189
+ # Loads the line stats for this commit from the repository
190
+ #
191
+ # @abstract Has to be implemented by VCS specific subclasses
192
+ def load_line_stats
193
+ raise NotImplementedError
194
+ end
195
+
116
196
  end
117
197
 
118
198
  end
@@ -10,8 +10,8 @@ module Metior
10
10
  class UnsupportedError < RuntimeError
11
11
 
12
12
  # Creates a new instance of this error
13
- def initialize
14
- super 'Operation not supported by the current VCS.'
13
+ def initialize(vcs)
14
+ super 'Operation not supported by the current VCS (:%s).' % vcs::NAME
15
15
  end
16
16
 
17
17
  end
@@ -4,8 +4,6 @@
4
4
  # Copyright (c) 2011, Sebastian Staudt
5
5
 
6
6
  require 'metior/commit'
7
- require 'metior/git'
8
- require 'metior/git/actor'
9
7
 
10
8
  module Metior
11
9
 
@@ -16,51 +14,54 @@ module Metior
16
14
  # @author Sebastian Staudt
17
15
  class Commit < Metior::Commit
18
16
 
19
- include Metior::Git
20
-
21
17
  # Creates a new Git commit object linked to the repository and branch it
22
18
  # belongs to and the data from the corresponding `Grit::Commit` object
23
19
  #
24
20
  # @param [Repository] repo The Git repository this commit belongs to
25
- # @param [String] range The commit range this commits belongs to
26
21
  # @param [Grit::Commit] commit The commit object from Grit
27
- def initialize(repo, range, commit)
28
- super repo, range
22
+ def initialize(repo, commit)
23
+ super repo
29
24
 
30
- @additions = commit.stats.additions
31
25
  @authored_date = commit.authored_date
32
26
  @committed_date = commit.committed_date
33
- @deletions = commit.stats.deletions
34
27
  @id = commit.id
35
28
  @message = commit.message
29
+ @parents = commit.parents.map { |parent| parent.id }
36
30
 
37
- authors = repo.authors range
38
- @author = authors[Actor.id_for commit.author]
39
- @author = Actor.new repo, commit.author if author.nil?
40
- @author.add_commit self
31
+ self.author = commit.author
32
+ self.committer = commit.committer
33
+ end
41
34
 
42
- committers = repo.committers range
43
- @committer = committers[Actor.id_for commit.committer]
44
- @committer = Actor.new repo, commit.committer if @committer.nil?
45
- @committer.add_commit self
35
+ end
46
36
 
47
- @added_files = []
48
- @modified_files = []
49
- @deleted_files = []
50
- commit.diffs.each do |diff|
51
- if diff.new_file
52
- @added_files << diff.b_path
53
- elsif diff.deleted_file
54
- @deleted_files << diff.b_path
55
- elsif diff.renamed_file
56
- @added_files << diff.b_path
57
- @deleted_files << diff.a_path
58
- else
59
- @modified_files << diff.b_path
60
- end
37
+ # Loads the file stats for this commit from the repository
38
+ #
39
+ # @see Repository#raw_commit
40
+ def load_file_stats
41
+ @added_files = []
42
+ @modified_files = []
43
+ @deleted_files = []
44
+ @repo.raw_commit(@id).diffs.each do |diff|
45
+ if diff.new_file
46
+ @added_files << diff.b_path
47
+ elsif diff.deleted_file
48
+ @deleted_files << diff.b_path
49
+ elsif diff.renamed_file
50
+ @added_files << diff.b_path
51
+ @deleted_files << diff.a_path
52
+ else
53
+ @modified_files << diff.b_path
61
54
  end
62
55
  end
56
+ end
63
57
 
58
+ # Loads the line stats for this commit from the repository
59
+ #
60
+ # @see Repository#raw_commit
61
+ def load_line_stats
62
+ commit = @repo.raw_commit @id
63
+ @additions = commit.stats.additions
64
+ @deletions = commit.stats.deletions
64
65
  end
65
66
 
66
67
  end
@@ -5,8 +5,6 @@
5
5
 
6
6
  require 'grit'
7
7
 
8
- require 'metior/git'
9
- require 'metior/git/commit'
10
8
  require 'metior/repository'
11
9
 
12
10
  module Metior
@@ -18,8 +16,6 @@ module Metior
18
16
  # @author Sebastian Staudt
19
17
  class Repository < Metior::Repository
20
18
 
21
- include Metior::Git
22
-
23
19
  # Creates a new Git repository based on the given path
24
20
  #
25
21
  # This creates a new `Grit::Repo` instance to interface with the
@@ -32,8 +28,41 @@ module Metior
32
28
  @grit_repo = Grit::Repo.new(path)
33
29
  end
34
30
 
31
+ # Retrieves a raw commit object for the given commit ID
32
+ #
33
+ # @param [String] id The ID of the commit
34
+ # @return [Grit::Commit] The commit object
35
+ # @see Grit::Repo#commit
36
+ def raw_commit(id)
37
+ @grit_repo.commit(id)
38
+ end
39
+
35
40
  private
36
41
 
42
+ # Returns the unique identifier for the commit the given reference – like
43
+ # a branch name – is pointing to
44
+ #
45
+ # Returns the given ref name immediately if it is a full SHA1 commit ID.
46
+ #
47
+ # @param [String] ref A symbolic reference name
48
+ # @return [String] The SHA1 ID of the commit the reference is pointing to
49
+ def id_for_ref(ref)
50
+ return ref if ref.match(/[0-9a-f]{40}/)
51
+ unless @refs.key? ref
52
+ @refs[ref] = @grit_repo.git.rev_parse({}, "#{ref}^{}")
53
+ end
54
+ @refs[ref]
55
+ end
56
+
57
+ # Loads all branches and the corresponding commit IDs of this repository
58
+ #
59
+ # @return [Hash<String, String>] The names of all branches and the
60
+ # corresponding commit IDs
61
+ # @see Grit::Repo#branches
62
+ def load_branches
63
+ Hash[@grit_repo.branches.map { |b| [b.name, b.commit.id] }]
64
+ end
65
+
37
66
  # This method uses Grit to load all commits from the given commit range
38
67
  #
39
68
  # Because of some Grit internal limitations, the commits have to be
@@ -47,12 +76,16 @@ module Metior
47
76
  # (`'master..development'`), a range (`'master'..'development'`)
48
77
  # or as a single ref (`'master'`). A single ref name means all
49
78
  # commits reachable from that ref.
50
- # @return [Array<Commit>] All commits in the given commit range
79
+ # @return [Grit::Commit, nil] The base commit of the requested range or
80
+ # `nil` if the the range starts at the beginning of the history
81
+ # @return [Array<Grit::Commit>] All commits in the given commit range
51
82
  # @see Grit::Repo#commits
52
83
  def load_commits(range)
53
84
  if range.first == ''
85
+ base_commit = nil
54
86
  range = range.last
55
87
  else
88
+ base_commit = @grit_repo.commit(range.first)
56
89
  range = '%s..%s' % [range.first, range.last]
57
90
  end
58
91
 
@@ -62,7 +95,39 @@ module Metior
62
95
  commits += @grit_repo.commits(range, 500, skip)
63
96
  skip += 500
64
97
  end while commits.size == skip
65
- commits
98
+
99
+ [base_commit, commits]
100
+ end
101
+
102
+ # Loads both the name and description of the project contained in the
103
+ # repository from the description file in `GIT_DIR`. The first line of
104
+ # that file is used as the project's name, the remaining text is used as
105
+ # a description of the project.
106
+ #
107
+ # @see #description
108
+ # @see #name
109
+ # @see Grit::Repo#name
110
+ def load_name_and_description
111
+ description = @grit_repo.description
112
+ if description.start_with? 'Unnamed repository'
113
+ @name = ''
114
+ @description = ''
115
+ else
116
+ description = description.lines.to_a
117
+ @name = description.shift.strip
118
+ @description = description.join("\n").strip
119
+ end
120
+ end
121
+ alias_method :load_description, :load_name_and_description
122
+ alias_method :load_name, :load_name_and_description
123
+
124
+ # Loads all tags and the corresponding commit IDs of this repository
125
+ #
126
+ # @return [Hash<String, String>] The names of all tags and the
127
+ # corresponding commit IDs
128
+ # @see Grit::Repo#tags
129
+ def load_tags
130
+ Hash[@grit_repo.tags.map { |b| [b.name, b.commit.id] }]
66
131
  end
67
132
 
68
133
  end