Permalink
Browse files

Initial commit

This includes basic support for Git repositories.
  • Loading branch information...
0 parents commit 04a0a0c5526b6d6e6b9cdb0f4832cfc8184fb0ac @koraktor committed Mar 29, 2011
Showing with 710 additions and 0 deletions.
  1. +4 −0 .gitignore
  2. +1 −0 .yardopts
  3. +5 −0 Changelog.md
  4. +7 −0 Gemfile
  5. +16 −0 Gemfile.lock
  6. +25 −0 LICENSE
  7. +76 −0 README.md
  8. +46 −0 Rakefile
  9. +40 −0 lib/metior.rb
  10. +67 −0 lib/metior/actor.rb
  11. +51 −0 lib/metior/commit.rb
  12. +29 −0 lib/metior/git.rb
  13. +43 −0 lib/metior/git/actor.rb
  14. +47 −0 lib/metior/git/commit.rb
  15. +81 −0 lib/metior/git/repository.rb
  16. +69 −0 lib/metior/repository.rb
  17. +72 −0 lib/metior/vcs.rb
  18. +11 −0 lib/metior/version.rb
  19. +20 −0 metior.gemspec
4 .gitignore
@@ -0,0 +1,4 @@
+.bundle/
+.yardoc/
+doc/
+pkg/
1 .yardopts
@@ -0,0 +1 @@
+--files Changelog.md,LICENSE
5 Changelog.md
@@ -0,0 +1,5 @@
+# Changelog
+
+## `master` branch
+
+ * Basic stats support for Git repositories
7 Gemfile
@@ -0,0 +1,7 @@
+source :rubygems
+
+gem 'grit', '~> 2.4.0'
+
+group :development do
+ gem 'yard'
+end
16 Gemfile.lock
@@ -0,0 +1,16 @@
+GEM
+ remote: http://rubygems.org/
+ specs:
+ diff-lcs (1.1.2)
+ grit (2.4.1)
+ diff-lcs (~> 1.1)
+ mime-types (~> 1.15)
+ mime-types (1.16)
+ yard (0.6.5)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ grit (~> 2.4.0)
+ yard
25 LICENSE
@@ -0,0 +1,25 @@
+Copyright (c) 2011, Sebastian Staudt
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+* Neither the name of the author nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
76 README.md
@@ -0,0 +1,76 @@
+Metior
+======
+
+Metior is a source code history analyzer API that provides various statistics
+about a source code repository and its change over time.
+
+Currently Metior provides basic support for Git repositories.
+
+If you're interested in Metior, feel free to join the discussion on Convore in
+[Metior's group](https://convore.com/metior).
+
+## Examples
+
+### One-liner for some basic statistics
+
+ Metior.simple_stats :git, '~/open-source/metior'
+
+### More fine-grained access to repository statistics
+
+ repo = Metior::Git::Repository '~/open-source/metior'
+ repo.commits 'development' # Get all commits in development
+ repo.top_committers 'master', 5 # Get the top 5 committers in master
+
+## Requirements
+
+* Grit — a Ruby API for Git
+* [Git](http://git-scm.com) >= 1.6
+
+## Documentation
+
+The documentation of the Ruby API can be seen at [RubyDoc.info][1]. The API
+documentation of the current development version is also available [there][5].
+
+## Future plans
+
+* More statistics and analyses
+* More supported VCSs, like Subversion or Mercurial
+* Support for creating graphs
+* Console and web application to accompany this API
+* Code analysis to show programming languages, effective lines of code, etc.
+
+## Contribute
+
+Metior is a open-source project. Therefore you are free to help improving it.
+There are several ways of contributing to Metior's development:
+
+* Build apps using it and spread the word.
+* Report problems and request features using the [issue tracker][2].
+* Write patches yourself to fix bugs and implement new functionality.
+* Create a Metior fork on [GitHub][1] and start hacking. Extra points for using
+ Metior pull requests and feature branches.
+
+## License
+
+This code is free software; you can redistribute it and/or modify it under the
+terms of the new BSD License. A copy of this license can be found in the
+LICENSE file.
+
+## Credits
+
+* Sebastian Staudt – koraktor(at)gmail.com
+
+## See Also
+
+* [API documentation][1]
+* [Metior's homepage][2]
+* [GitHub project page][3]
+* [GitHub issue tracker][4]
+
+Follow Metior on Twitter [@metiorstats](http://twitter.com/metiorstats).
+
+ [1]: http://rubydoc.info/gems/metior/frames
+ [2]: http://koraktor.de/metior
+ [3]: http://github.com/koraktor/metior
+ [4]: http://github.com/koraktor/metior/issues
+ [5]: http://rubydoc.info/github/koraktor/metior/master/frames
46 Rakefile
@@ -0,0 +1,46 @@
+# This code is free software; you can redistribute it and/or modify it under
+# the terms of the new BSD License.
+#
+# Copyright (c) 2011, Sebastian Staudt
+
+require 'rake/gempackagetask'
+require 'rake/testtask'
+
+task :default => :test
+
+# Rake tasks for building the gem
+spec = Gem::Specification.load('metior.gemspec')
+Rake::GemPackageTask.new(spec) do |pkg|
+end
+
+# Rake task for running the test suite
+Rake::TestTask.new do |t|
+ t.libs << 'lib' << 'test'
+ t.pattern = 'test/**/test_*.rb'
+ t.verbose = true
+end
+
+# Check if YARD is installed
+begin
+ require 'yard'
+
+ # Create a rake task +:doc+ to build the documentation using YARD
+ YARD::Rake::YardocTask.new do |yardoc|
+ yardoc.name = 'doc'
+ yardoc.files = [ 'lib/**/*.rb', 'LICENSE', 'README.md' ]
+ yardoc.options = [ '--private', '--title', 'Metior — API Documentation' ]
+ end
+rescue LoadError
+ # Create a rake task +:doc+ to show that YARD is not installed
+ desc 'Generate YARD Documentation (not available)'
+ task :doc do
+ $stderr.puts 'You need YARD to build the documentation. Install it using `gem install yard`.'
+ end
+end
+
+# Task for cleaning documentation and package directories
+desc 'Clean documentation and package directories'
+task :clean do
+ FileUtils.rm_rf 'doc'
+ FileUtils.rm_rf 'pkg'
+end
40 lib/metior.rb
@@ -0,0 +1,40 @@
+# This code is free software; you can redistribute it and/or modify it under
+# the terms of the new BSD License.
+#
+# Copyright (c) 2011, Sebastian Staudt
+
+libdir = File.dirname(__FILE__)
+$LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
+
+require 'rubygems'
+require 'bundler'
+Bundler.setup :default
+
+require 'metior/git'
+
+# Metior is a source code history analyzer that provides various statistics
+# about a source code repository and its change over time.
+#
+# @author Sebastian Staudt
+module Metior
+
+ # Calculates simplistic stats for the given repository and branch
+ #
+ # @param [Symbol] type The type of the repository, e.g. +:git+
+ # @param [String] path The file system path of the repository
+ # @param [String] branch The repository's 'branch to analyze. +nil+ will use
+ # the VCS's default branch
+ # @return [Hash] The calculated stats for the given repository and branch
+ def self.simple_stats(type, path, branch = nil)
+ vcs = vcs(type)
+ repo = vcs::Repository.new path
+ branch ||= vcs::DEFAULT_BRANCH
+
+ {
+ :authors => repo.authors(branch).values,
+ :commit_count => repo.commits(branch).size,
+ :top_committers => repo.top_contributors(branch, 3)
+ }
+ end
+
+end
67 lib/metior/actor.rb
@@ -0,0 +1,67 @@
+# This code is free software; you can redistribute it and/or modify it under
+# the terms of the new BSD License.
+#
+# Copyright (c) 2011, Sebastian Staudt
+
+module Metior
+
+ # Represents an actor in a source code repository
+ #
+ # Depending on the repository's VCS this may be for example an author or
+ # committer.
+ #
+ # @abstract It has to be subclassed to implement a actor representation for a
+ # specific VCS.
+ # @author Sebastian Staudt
+ class Actor
+
+ # @return [Array<Commit>] The list of commits this actor has contributed to
+ # the source code repository
+ attr_reader :commits
+
+ # @return [String] The full name of the actor
+ attr_reader :name
+
+ # @return [String] A unqiue identifier for the actor
+ attr_reader :id
+
+ # Extracts a unique identifier from the given, VCS dependent actor object
+ #
+ # @abstract Different VCSs use different identifiers for users, so this
+ # method must be implemented for each supported VCS.
+ # @param [Object] actor The actor object retrieved from the VCS
+ # @return [String] A unique identifier for the given actor
+ def self.id_for(actor)
+ end
+
+ # Creates a new actor linked to the given source code repository
+ #
+ # @param [Repository] repo The repository this actor belongs to
+ def initialize(repo)
+ @commits = []
+ @repo = repo
+ end
+
+ # Adds a new commit to the list of commits this actor has contributed to
+ # the analyzed source code repository
+ #
+ # @param [Commit] commit The commit to add to the list
+ def add_commit(commit)
+ @commits << commit
+ end
+
+ # Creates a string representation for this actor without recursing into
+ # commit and repository details
+ #
+ # @return [String] A minimal string representation for this actor
+ def inspect
+ '<#%s:0x%x: @commits=%d @id="%s" @name="%s" @repo=<#%s:0x%x ...>>' %
+ [
+ self.class.name, __id__ * 2, @commits.size, @id, @name,
+ @repo.class.name, @repo.__id__ * 2
+ ]
+ end
+
+ end
+
+end
51 lib/metior/commit.rb
@@ -0,0 +1,51 @@
+# This code is free software; you can redistribute it and/or modify it under
+# the terms of the new BSD License.
+#
+# Copyright (c) 2011, Sebastian Staudt
+
+
+module Metior
+
+ # This class represents a commit in a source code repository
+ #
+ # Although not all VCSs distinguish authors from committers this
+ # implementation forces a differentiation between the both.
+ #
+ # @abstract It has to be subclassed to implement a commit representation for
+ # a specific VCS.
+ # @author Sebastian Staudt
+ class Commit
+
+ # @return [Actor] This commit's author
+ attr_reader :author
+
+ # @return [Time] The date this commit has been authored
+ attr_reader :authored_date
+
+ # @return [String] The branch this commit belongs to
+ attr_reader :branch
+
+ # @return [Time] The date this commit has been committed
+ attr_reader :committed_date
+
+ # @return [Actor] This commit's committer
+ attr_reader :committer
+
+ # @return [String] The commit message of this commit
+ attr_reader :message
+
+ # @return [Repository] The repository this commit belongs to
+ attr_reader :repo
+
+ # Creates a new commit instance linked to the given repository and branch
+ #
+ # @param [Repository] repo The repository this commit belongs to
+ # @param [String] branch The branch this commit belongs to
+ def initialize(repo, branch)
+ @repo = repo
+ @branch = branch
+ end
+
+ end
+
+end
29 lib/metior/git.rb
@@ -0,0 +1,29 @@
+# This code is free software; you can redistribute it and/or modify it under
+# the terms of the new BSD License.
+#
+# Copyright (c) 2011, Sebastian Staudt
+
+require 'metior/vcs'
+
+module Metior
+
+ # The Metior implementation for Git
+ #
+ # @author Sebastian Staudt
+ module Git
+
+ # Git will be registered as +:git+
+ NAME = :git
+
+ include Metior::VCS
+
+ # Git's default branch is _master_
+ DEFAULT_BRANCH = 'master'
+
+ end
+
+end
+
+require 'metior/git/actor'
+require 'metior/git/commit'
+require 'metior/git/repository'
43 lib/metior/git/actor.rb
@@ -0,0 +1,43 @@
+# This code is free software; you can redistribute it and/or modify it under
+# the terms of the new BSD License.
+#
+# Copyright (c) 2011, Sebastian Staudt
+
+require 'metior/actor'
+
+module Metior
+
+ module Git
+
+ # Represents an actor in a Git source code repository, i.e. an author or
+ # committer.
+ #
+ # @author Sebastian Staudt
+ class Actor < Metior::Actor
+
+ alias_method :email, :id
+
+ # Returns the email address as an identifier for the given actor.
+ # Git uses email addresses as identifiers for its actors.
+ #
+ # @param [Grit::Actor] actor The actor object from Grit
+ # @return [String] The email address of the given actor
+ def self.id_for(actor)
+ actor.email
+ end
+
+ # Creates a new actor instance
+ #
+ # @param [Repository] repo The repository this actor belongs to
+ # @param [Grit::Actor] actor The actor object from Grit
+ def initialize(repo, actor)
+ super repo
+ @id = actor.email
+ @name = actor.name
+ end
+
+ end
+
+ end
+
+end
47 lib/metior/git/commit.rb
@@ -0,0 +1,47 @@
+# This code is free software; you can redistribute it and/or modify it under
+# the terms of the new BSD License.
+#
+# Copyright (c) 2011, Sebastian Staudt
+
+require 'metior/commit'
+require 'metior/git'
+require 'metior/git/actor'
+
+module Metior
+
+ module Git
+
+ # Represents a commit in a Git source code repository
+ #
+ # @author Sebastian Staudt
+ class Commit < Metior::Commit
+
+ include Metior::Git
+
+ # Creates a new Git commit object linked to the repository and branch it
+ # belongs to and the data from the corresponding +Grit::Commit+ object
+ #
+ # @param [Repository] repo The Git repository this commit belongs to
+ # @param [String] branch The branch this commits belongs to
+ # @param [Grit::Commit] commit The commit object from Grit
+ def initialize(repo, branch, commit)
+ super repo, branch
+
+ authors = repo.authors(branch)
+ author = authors[Actor.id_for commit.author]
+ author = Actor.new repo, commit.author if author.nil?
+ author.add_commit self
+
+ @author = author
+ @authored_date = commit.authored_date
+ @committer = Actor.new repo, commit.committer
+ @committed_date = commit.committed_date
+ @id = commit.id
+ @message = commit.message
+ end
+
+ end
+
+ end
+
+end
81 lib/metior/git/repository.rb
@@ -0,0 +1,81 @@
+# This code is free software; you can redistribute it and/or modify it under
+# the terms of the new BSD License.
+#
+# Copyright (c) 2011, Sebastian Staudt
+
+require 'grit'
+
+require 'metior/git'
+require 'metior/git/commit'
+require 'metior/repository'
+
+module Metior
+
+ module Git
+
+ # Represents a Git source code repository
+ #
+ # @author Sebastian Staudt
+ class Repository < Metior::Repository
+
+ include Metior::Git
+
+ # Creates a new Git repository based on the given path
+ #
+ # This creates a new +Grit::Repo+ instance to interface with the
+ # repository.
+ #
+ # @param [String] path The file system path of the repository
+ def initialize(path)
+ super path
+
+ @grit_repo = Grit::Repo.new(path)
+ end
+
+ # Loads all commits including their authors from the given branch
+ #
+ # @note Grit will choke on huge repositories, like Homebrew or the Linux
+ # kernel. You will have to raise the timeout limit using
+ # +Grit.git_timeout=+.
+ # @param [String] branch The branch to load commits from
+ # @return [Array<Commit>] All commits from the given branch
+ def commits(branch = DEFAULT_BRANCH)
+ if @commits[branch].nil?
+ @authors[branch] = {}
+ @commits[branch] = []
+ load_commits(branch).each do |git_commit|
+ commit = Commit.new(self, branch, git_commit)
+ @commits[branch] << commit
+ @authors[branch][commit.author.id] = commit.author
+ end
+ end
+
+ @commits[branch]
+ end
+
+ private
+
+ # This method uses Grit to load all commits from the given branch.
+ #
+ # Because of some Grit internal limitations, the commits have to be
+ # loaded in batches of up to 500 commits.
+ #
+ #
+ # @param [String] branch The branch to load commits from
+ # @return [Array<Commit>] All commits from the given branch
+ # @see Grit::Repo#commits
+ def load_commits(branch)
+ commits = []
+ skip = 0
+ begin
+ commits += @grit_repo.commits(branch, 500, skip)
+ skip += 500
+ end while commits.size == skip
+ commits
+ end
+
+ end
+
+ end
+
+end
69 lib/metior/repository.rb
@@ -0,0 +1,69 @@
+# This code is free software; you can redistribute it and/or modify it under
+# the terms of the new BSD License.
+#
+# Copyright (c) 2011, Sebastian Staudt
+
+require 'metior/actor'
+
+module Metior
+
+ # This class represents a source code repository.
+ #
+ # @abstract It has to be subclassed to implement a repository representation
+ # for a specific VCS.
+ # @author Sebastian Staudt
+ class Repository
+
+ # @return [String] The file system path of this repository
+ attr_reader :path
+
+ # Creates a new repository instance with the given file system path
+ #
+ # @param [String] path The file system path of the repository
+ def initialize(path)
+ @authors = {}
+ @commits = {}
+ @path = path
+ end
+
+ # Returns all authors from the given branch
+ #
+ # This will call +commits(branch)+ if the authors for the branch are not
+ # known yet.
+ #
+ # @param [String] The branch from which the authors should be retrieved
+ # @return [Array<Actor>] All authors from the given branch
+ # @see #commits
+ def authors(branch = vcs::DEFAULT_BRANCH)
+ commits(branch) if @authors[branch].nil?
+ @authors[branch]
+ end
+
+ # Loads all commits including their authors from the given branch
+ #
+ # @abstract It has to be implemented by VCS specific subclasses
+ # @param [String] branch The branch to load commits from
+ # @return [Array<Commit>] All commits from the given branch
+ def commits(branch = vcs::DEFAULT_BRANCH)
+ end
+
+ # Returns a list of top contributors in the given branch
+ #
+ # This will first have to load all authors (and i.e. commits) from the
+ # given branch.
+ #
+ # @param [String] The branch from which the top contributors should be
+ # retrieved
+ # @param [Fixnum] The number of contributors to return
+ # @return [Array<Actor>] An array of the given number of top contributors
+ # in the given branch
+ # @see #authors
+ def top_contributors(branch = vcs::DEFAULT_BRANCH, count = 3)
+ authors = authors(branch).values.sort_by { |author| author.commits.size }
+ count = [count, authors.size].min
+ authors[-count..-1].reverse
+ end
+
+ end
+
+end
72 lib/metior/vcs.rb
@@ -0,0 +1,72 @@
+# This code is free software; you can redistribute it and/or modify it under
+# the terms of the new BSD License.
+#
+# Copyright (c) 2011, Sebastian Staudt
+
+require 'metior'
+
+module Metior
+
+ # This hash will be dynamically filled with all available VCS types and the
+ # corresponding implementation modules
+ @@vcs_types = {}
+
+ # Returns the VCS implementation +Module+ for a given symbolic VCS name
+ #
+ # @param [Symbol] type The symbolic type name of the VCS
+ # @return [VCS] The VCS for the given name
+ def self.vcs(type)
+ type = type.to_sym
+ unless @@vcs_types.key? type
+ raise 'No VCS registered for :%s' % type
+ end
+ @@vcs_types[type]
+ end
+
+ # Returns a Hash with all available VCS types as keys and the implementation
+ # modules as values
+ #
+ # @return [Hash<Symbol, VCS>] All available VCS implementations and their
+ # corresponding names
+ def self.vcs_types
+ @@vcs_types
+ end
+
+ # This module provides functionality to automatically register new VCS
+ # implementations +Module+s
+ #
+ # @author Sebastian Staudt
+ module VCS
+
+ # Including +VCS+ will make a +Module+ available as a supported VCS type in
+ # Metior
+ #
+ # @example This will automatically register +ExoticVCS+ as +:exotic+
+ # module ExoticVCS
+ #
+ # NAME = :exotic
+ #
+ # include Metior::VCS
+ #
+ # end
+ #
+ # @param [Module] mod The +Module+ that provides a Metior implementation
+ # for a specific VCS
+ # @see Metior#vcs_types
+ def self.included(mod)
+ Metior.vcs_types[mod::NAME.to_sym] = mod
+ @@vcs = mod
+ end
+
+ # This is a helper method to easily refer to the currently used VCS
+ # implementation +Module+ from outside its scope, e.g. inside of
+ # +Metior::Repository+.
+ #
+ # @return [VCS] The current VCS implementation +Module+
+ def vcs
+ @@vcs
+ end
+
+ end
+
+end
11 lib/metior/version.rb
@@ -0,0 +1,11 @@
+# This code is free software; you can redistribute it and/or modify it under
+# the terms of the new BSD License.
+#
+# Copyright (c) 2011, Sebastian Staudt
+
+module Metior
+
+ # The current version of the Metior gem
+ VERSION = '0.1.0'
+
+end
20 metior.gemspec
@@ -0,0 +1,20 @@
+require 'bundler'
+
+require File.expand_path(File.dirname(__FILE__) + '/lib/metior/version')
+
+Gem::Specification.new do |s|
+ s.name = "metior"
+ s.version = Metior::VERSION
+ s.platform = Gem::Platform::RUBY
+ s.authors = [ 'Sebastian Staudt' ]
+ s.email = [ 'koraktor@gmail.com' ]
+ s.homepage = 'http://koraktor.de/metior'
+ s.summary = 'A source code history analyzer API'
+ s.description = 'Metior is a source code history analyzer that provides various statistics about a source code repository and its change over time.'
+
+ s.add_bundler_dependencies
+
+ s.files = `git ls-files`.split("\n")
+ s.test_files = `git ls-files -- test/*`.split("\n")
+ s.require_paths = [ 'lib' ]
+end

0 comments on commit 04a0a0c

Please sign in to comment.