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