metior 0.1.4 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +2 -0
- data/.yardopts +1 -0
- data/Changelog.md +23 -0
- data/Gemfile +1 -17
- data/Gemfile.lock +28 -21
- data/README.md +66 -20
- data/lib/metior.rb +30 -14
- data/lib/metior/actor.rb +28 -22
- data/lib/metior/auto_include_vcs.rb +43 -0
- data/lib/metior/collections/actor_collection.rb +97 -0
- data/lib/metior/collections/collection.rb +84 -0
- data/lib/metior/collections/commit_collection.rb +309 -0
- data/lib/metior/commit.rb +128 -48
- data/lib/metior/errors.rb +2 -2
- data/lib/metior/git/commit.rb +32 -31
- data/lib/metior/git/repository.rb +71 -6
- data/lib/metior/github/commit.rb +5 -16
- data/lib/metior/github/repository.rb +68 -40
- data/lib/metior/report.rb +139 -0
- data/lib/metior/report/view.rb +120 -0
- data/lib/metior/report/view_helper.rb +47 -0
- data/lib/metior/repository.rb +225 -56
- data/lib/metior/vcs.rb +12 -3
- data/lib/metior/version.rb +1 -1
- data/metior.gemspec +28 -26
- data/reports/default.rb +17 -0
- data/reports/default/images/favicon.png +0 -0
- data/reports/default/stylesheets/default.css +128 -0
- data/reports/default/templates/actor/minimal.mustache +1 -0
- data/reports/default/templates/commit/minimal.mustache +1 -0
- data/reports/default/templates/index.mustache +27 -0
- data/reports/default/templates/most_significant_authors.mustache +11 -0
- data/reports/default/templates/most_significant_commits.mustache +13 -0
- data/reports/default/templates/repository_information.mustache +17 -0
- data/reports/default/templates/top_committers.mustache +11 -0
- data/reports/default/views/index.rb +33 -0
- data/reports/default/views/most_significant_authors.rb +19 -0
- data/reports/default/views/most_significant_commits.rb +19 -0
- data/reports/default/views/repository_information.rb +47 -0
- data/reports/default/views/top_committers.rb +21 -0
- data/test/fixtures.rb +54 -36
- data/test/helper.rb +10 -3
- data/test/{test_class_loading.rb → test_1st_class_loading.rb} +1 -1
- data/test/test_actor_colletion.rb +78 -0
- data/test/test_collection.rb +61 -0
- data/test/test_commit_collection.rb +139 -0
- data/test/test_git.rb +58 -5
- data/test/test_github.rb +52 -9
- data/test/test_metior.rb +22 -1
- data/test/test_report.rb +49 -0
- data/test/test_repository.rb +46 -9
- data/test/test_vcs.rb +36 -13
- metadata +105 -43
data/lib/metior/github/commit.rb
CHANGED
@@ -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,
|
30
|
-
super repo
|
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
|
-
|
43
|
-
|
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
|
38
|
-
@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 [
|
60
|
-
#
|
61
|
-
|
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
|
78
|
+
def load_commits(range)
|
79
|
+
base_commit = nil
|
85
80
|
commits = []
|
86
81
|
page = 1
|
87
82
|
begin
|
88
83
|
loop do
|
89
|
-
|
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
|
96
|
+
rescue Octokit::NotFound
|
93
97
|
end
|
94
|
-
|
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
|