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
@@ -6,8 +6,6 @@
6
6
  require 'time'
7
7
 
8
8
  require 'metior/commit'
9
- require 'metior/github'
10
- require 'metior/github/actor'
11
9
 
12
10
  module Metior
13
11
 
@@ -18,16 +16,13 @@ module Metior
18
16
  # @author Sebastian Staudt
19
17
  class Commit < Metior::Commit
20
18
 
21
- include Metior::GitHub
22
-
23
19
  # Creates a new GitHub commit object linked to the repository and branch
24
20
  # it belongs to and the data parsed from the corresponding JSON data
25
21
  #
26
22
  # @param [Repository] repo The GitHub repository this commit belongs to
27
- # @param [String] range The commit range this commits belongs to
28
23
  # @param [Hashie:Mash] commit The commit data parsed from the JSON API
29
- def initialize(repo, range, commit)
30
- super repo, range
24
+ def initialize(repo, commit)
25
+ super repo
31
26
 
32
27
  @added_files = []
33
28
  @additions = 0
@@ -38,16 +33,10 @@ module Metior
38
33
  @id = commit.id
39
34
  @message = commit.message
40
35
  @modified_files = []
36
+ @parents = commit.parents.map { |parent| parent.id }
41
37
 
42
- authors = repo.authors range
43
- @author = authors[Actor.id_for commit.author]
44
- @author = Actor.new repo, commit.author if author.nil?
45
- @author.add_commit self
46
-
47
- committers = repo.committers range
48
- @committer = committers[Actor.id_for commit.committer]
49
- @committer = Actor.new repo, commit.committer if @committer.nil?
50
- @committer.add_commit self
38
+ self.author = commit.author
39
+ self.committer = commit.committer
51
40
  end
52
41
 
53
42
  end
@@ -5,8 +5,6 @@
5
5
 
6
6
  require 'octokit'
7
7
 
8
- require 'metior/github'
9
- require 'metior/github/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::GitHub
22
-
23
19
  # @return [String] The project name of the repository
24
20
  attr_reader :project
25
21
 
@@ -31,23 +27,42 @@ module Metior
31
27
  #
32
28
  # @param [String] user The GitHub username of repository's owner
33
29
  # @param [String] project The name of the project
34
- def initialize(user, project)
30
+ def initialize(user, project = nil)
31
+ user, project = user.split('/') if user.include? '/'
32
+
35
33
  super "#{user}/#{project}"
36
34
 
37
- @project = project
38
- @user = user
35
+ @project = project
36
+ @user = user
39
37
  end
40
38
 
41
39
  private
42
40
 
41
+ # Returns the unique identifier for the commit the given reference – like
42
+ # a branch name – is pointing to
43
+ #
44
+ # Returns the given ref name immediately if it is a full SHA1 commit ID.
45
+ #
46
+ # @param [String] ref A symbolic reference name
47
+ # @return [String] The SHA1 ID of the commit the reference is pointing to
48
+ def id_for_ref(ref)
49
+ return ref if ref.match(/[0-9a-f]{40}/)
50
+ @refs[ref] = Octokit.commit(@path, ref).id unless @refs.key? ref
51
+ @refs[ref]
52
+ end
53
+
54
+ # Loads all branches and the corresponding commit IDs of this repository
55
+ #
56
+ # @return [Hash<String, String>] The names of all branches and the
57
+ # corresponding commit IDs
58
+ # @see Octokit#branches
59
+ def load_branches
60
+ Octokit.branches(@path)
61
+ end
62
+
43
63
  # This method uses Octokit to load all commits from the given commit
44
64
  # range
45
65
  #
46
- # If you want to compare a branch with another (i.e. if you supply a
47
- # range of commits), it needs two calls to the GitHub API to get all
48
- # commits of each branch. The comparison is done in the code, so the
49
- # limits (see below) will be effectively cut in half.
50
- #
51
66
  # @note GitHub API is currently limited to 60 calls a minute, so you
52
67
  # won't be able to query branches with more than 2100 commits
53
68
  # (35 commits per call).
@@ -56,42 +71,55 @@ module Metior
56
71
  # (`'master..development'`), a range (`'master'..'development'`)
57
72
  # or as a single ref (`'master'`). A single ref name means all
58
73
  # commits reachable from that ref.
59
- # @return [Array<Commit>] All commits in the given commit range
60
- # @see #load_ref_commits
61
- def load_commits(range)
62
- commits = load_ref_commits(range.last)
63
- if range.first == ''
64
- base_commits = []
65
- else
66
- base_commits = load_ref_commits(range.first).map! do |commit|
67
- commit.id
68
- end
69
- end
70
- commits.reject { |commit| base_commits.include? commit.id }
71
- end
72
-
73
- # This method uses Octokit to load all commits from the given ref
74
- #
75
- # Because of GitHub API limitations, the commits have to be loaded in
76
- # batches.
77
- #
78
- # @note GitHub API is currently limited to 60 calls a minute, so you
79
- # won't be able to query refs with more than 2100 commits (35
80
- # commits per call).
81
- # @param [String] ref The ref to load commits from
82
- # @return [Array<Commit>] All commits from the given ref
74
+ # @return [Hashie::Mash, nil] The base commit of the requested range or
75
+ # `nil` if the the range starts at the beginning of the history
76
+ # @return [Array<Hashie::Mash>] All commits in the given commit range
83
77
  # @see Octokit::Commits#commits
84
- def load_ref_commits(ref)
78
+ def load_commits(range)
79
+ base_commit = nil
85
80
  commits = []
86
81
  page = 1
87
82
  begin
88
83
  loop do
89
- commits += Octokit.commits(@path, ref, :page => page)
84
+ new_commits = Octokit.commits(@path, range.last, :page => page)
85
+ base_commit_index = new_commits.find_index do |commit|
86
+ commit.id == range.first
87
+ end
88
+ unless base_commit_index.nil?
89
+ commits += new_commits[0..base_commit_index-1]
90
+ base_commit = new_commits[base_commit_index]
91
+ break
92
+ end
93
+ commits += new_commits
90
94
  page += 1
91
95
  end
92
- rescue Octokit::NotFound, Faraday::Error::ResourceNotFound
96
+ rescue Octokit::NotFound
93
97
  end
94
- 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 GitHub
104
+ #
105
+ # @see #description
106
+ # @see #name
107
+ # @see Octokit.repo
108
+ def load_name_and_description
109
+ github_repo = Octokit.repo @path
110
+ @description = github_repo.description
111
+ @name = github_repo.name
112
+ end
113
+ alias_method :load_description, :load_name_and_description
114
+ alias_method :load_name, :load_name_and_description
115
+
116
+ # Loads all tags and the corresponding commit IDs of this repository
117
+ #
118
+ # @return [Hash<String, String>] The names of all tags and the
119
+ # corresponding commit IDs
120
+ # @see Octokit#tags
121
+ def load_tags
122
+ Octokit.tags @path
95
123
  end
96
124
 
97
125
  end
@@ -0,0 +1,139 @@
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 'fileutils'
7
+ require 'mustache'
8
+
9
+ require 'metior/report/view'
10
+
11
+ module Metior
12
+
13
+ # This class represents a report
14
+ #
15
+ # A report is a collection of Mustache views that have access to the
16
+ # repository attached to this report.
17
+ #
18
+ # @author Sebastian Staudt
19
+ # @see View
20
+ class Report
21
+
22
+ # The path where the reports bundled with Metior live
23
+ REPORTS_PATH = File.expand_path File.join File.dirname(__FILE__), '..', '..', 'reports'
24
+
25
+ # Returns the range of commits that should be analyzed by this report
26
+ #
27
+ # @return [String, Range] The range of commits covered by this report
28
+ attr_reader :range
29
+
30
+ # Returns the repository that should be analyzed by this report
31
+ #
32
+ # @return [Repository] The repository attached to this report
33
+ attr_reader :repository
34
+
35
+ # Create a new report instance for the given report name, repository and
36
+ # commit range
37
+ #
38
+ # @param [String, Symbol] name The name of the report to load and
39
+ # initialize
40
+ # @param [Repository] repository The repository to analyze
41
+ # @param [String, Range] range The commit range to analyze
42
+ def self.create(name, repository, range = repository.vcs::DEFAULT_BRANCH)
43
+ require File.join(REPORTS_PATH, name.to_s)
44
+ name = name.to_s.split('_').map { |n| n.capitalize }.join('')
45
+ const_get(name.to_sym).new(repository, range)
46
+ end
47
+
48
+ # Returns the name of this report
49
+ #
50
+ # @return [String] The name of this report
51
+ def self.name
52
+ class_variable_get(:@@name).to_s
53
+ end
54
+
55
+ # Returns the file system path this report resides in
56
+ #
57
+ # @return [String] The path of this report
58
+ def self.path
59
+ File.join REPORTS_PATH, name
60
+ end
61
+
62
+ # Returns the file system path this report's templates reside in
63
+ #
64
+ # @return [String] The path of this report's templates
65
+ def self.template_path
66
+ File.join path, 'templates'
67
+ end
68
+
69
+ # Returns the file system path this report's views reside in
70
+ #
71
+ # @return [String] The path of this report's views
72
+ def self.view_path
73
+ File.join path, 'views'
74
+ end
75
+
76
+ # Returns the symbolic names of the main views this report consists of
77
+ #
78
+ # @reeturn [Array<Symbol>] This report's views
79
+ def self.views
80
+ class_variable_get :@@views
81
+ end
82
+
83
+ # Creates a new report for the given repository and commit range
84
+ #
85
+ # @param [Repository] repository The repository to analyze
86
+ # @param [String, Range] range The commit range to analyze
87
+ def initialize(repository, range = repository.vcs::DEFAULT_BRANCH)
88
+ @range = range
89
+ @repository = repository
90
+ end
91
+
92
+ # Generates this report's output into the given target directory
93
+ #
94
+ # This will generate individual HTML files for the main views of the
95
+ # report.
96
+ #
97
+ # @param [String] target_dir The target directory of the report
98
+ def generate(target_dir)
99
+ target_dir = File.expand_path target_dir
100
+ copy_assets(target_dir)
101
+
102
+ Mustache.template_path = self.class.template_path
103
+ Mustache.view_path = self.class.view_path
104
+ Mustache.view_namespace = self.class
105
+
106
+ self.class.views.each do |view_name|
107
+ file_name = File.join target_dir, view_name.to_s.downcase + '.html'
108
+ begin
109
+ output_file = File.open file_name, 'w'
110
+ output_file.write Mustache.view_class(view_name).new(self).render
111
+ ensure
112
+ output_file.close
113
+ end
114
+ end
115
+ end
116
+
117
+ private
118
+
119
+ # Copies the assets coming with this report to the given target directory
120
+ #
121
+ # This will copy the contents of the `images`, `javascript` and
122
+ # `stylesheets` directories inside the report's path into the target
123
+ # directory.
124
+ #
125
+ # @param [String] target_dir The target directory of the report
126
+ def copy_assets(target_dir)
127
+ FileUtils.mkdir_p target_dir
128
+
129
+ %w{images javascripts stylesheets}.map do |type|
130
+ File.join(self.class.path, type)
131
+ end.each do |src|
132
+ next unless File.directory? src
133
+ FileUtils.cp_r src, target_dir
134
+ end
135
+ end
136
+
137
+ end
138
+
139
+ end
@@ -0,0 +1,120 @@
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/report/view_helper'
7
+
8
+ class Metior::Report
9
+
10
+ # This class is an extended Mustache view
11
+ #
12
+ # A view represents a whole page or a section of a page that displays
13
+ # information about a repository. It is always attached to a specific report
14
+ # and can access the information of the report and the repository.
15
+ #
16
+ # @author Sebastian Staudt
17
+ class View < Mustache
18
+
19
+ include ViewHelper
20
+
21
+ # This will initialize new view classes
22
+ #
23
+ # @param [Class] subclass The inheriting view class
24
+ def self.inherited(subclass)
25
+ subclass.send :class_variable_set, :@@required_features, []
26
+ end
27
+
28
+ # Specifies one or more VCS features that are required to generate this
29
+ # view
30
+ #
31
+ # @example
32
+ # class LineStatsView < View
33
+ #
34
+ # requires :line_stats
35
+ #
36
+ # ...
37
+ #
38
+ # end
39
+ # @param [Symbol, ...] features One ore more features that are required for
40
+ # this view
41
+ def self.requires(*features)
42
+ required_features = class_variable_get :@@required_features
43
+ required_features += features
44
+ class_variable_set :@@required_features, required_features
45
+ end
46
+
47
+ # Initializes this view with the given report
48
+ #
49
+ # @param [Report] report The report this view belongs to
50
+ def initialize(report)
51
+ @report = report
52
+ end
53
+
54
+ # This will try to render a view as a partial of the current view or call a
55
+ # method of the repository
56
+ #
57
+ # The partial view will either be aquired from the current view namespace,
58
+ # i.e. the report this view belongs to, or from the default report.
59
+ #
60
+ # @param [Symbol] name The name of the view to render or the repository
61
+ # method to call
62
+ # @param [Object, ...] args The arguments to pass to the method
63
+ # @param [Proc] block The block to pass to the method
64
+ # @see Default
65
+ # @see http://rubydoc.info/gems/mustache/Mustache#view_class-class_method
66
+ # Mustache.view_class
67
+ def method_missing(name, *args, &block)
68
+ view_class = Mustache.view_class name
69
+ return view_class.new(@report).render if view_class != Mustache
70
+
71
+ repository.send name, *args, &block
72
+ end
73
+
74
+ # This checks if all required VCS features of this view are available for
75
+ # this report's repository
76
+ #
77
+ # @param [Object, ...] args The arguments expected by {Mustache#render}
78
+ # @see .requires
79
+ # @see http://rubydoc.info/gems/mustache/Mustache#render-instance_method
80
+ # Mustache#render
81
+ def render(*args)
82
+ features = self.class.send :class_variable_get, :@@required_features
83
+ super if features.all? { |feature| repository.supports? feature }
84
+ end
85
+
86
+ # Returns the repository that is analyzed by the report this view belongs
87
+ # to
88
+ #
89
+ # @return [Repository] The repository belonging to this view's report
90
+ def repository
91
+ @report.repository
92
+ end
93
+
94
+ # Returns whether the given name refers a partial view that can be rendered
95
+ # or method that can be called
96
+ #
97
+ # This checks whether this view has a method with the given name, or if
98
+ # another view with this name exists, or if the repository has a method
99
+ # with this name.
100
+ #
101
+ # @param [Symbol] name The name of the partial or method
102
+ # @return [Boolean] `true` if the given name refers a partial or method
103
+ # @see http://rubydoc.info/gems/mustache/Mustache#view_class-class_method
104
+ # Mustache.view_class
105
+ def respond_to?(name)
106
+ methods.include?(name.to_s) ||
107
+ Mustache.view_class(name) != Mustache ||
108
+ repository.respond_to?(name)
109
+ end
110
+
111
+ # Returns the name of the VCS the analyzed repository is using
112
+ #
113
+ # @return [Symbol] The name of the current VCS
114
+ def vcs_name
115
+ repository.vcs::NAME
116
+ end
117
+
118
+ end
119
+
120
+ end