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