contributors_stats 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +6 -0
  3. data/.travis.yml +11 -0
  4. data/Gemfile +5 -0
  5. data/LICENSE.md +14 -0
  6. data/README.md +43 -0
  7. data/Rakefile +28 -0
  8. data/contributors_stats.gemspec +24 -0
  9. data/lib/contributors_stats.rb +28 -0
  10. data/lib/contributors_stats/base.rb +29 -0
  11. data/lib/contributors_stats/calculator.rb +50 -0
  12. data/lib/contributors_stats/formatter.rb +57 -0
  13. data/lib/contributors_stats/json_helper.rb +49 -0
  14. data/lib/contributors_stats/reader.rb +52 -0
  15. data/lib/contributors_stats/version.rb +4 -0
  16. data/lib/example_data.rb +58 -0
  17. data/lib/plugins/contributors_stats/formatter/html.rb +26 -0
  18. data/lib/plugins/contributors_stats/reader/gh_org.rb +21 -0
  19. data/lib/plugins/contributors_stats/reader/gh_repo.rb +19 -0
  20. data/lib/plugins/contributors_stats/updater/html.rb +36 -0
  21. data/lib/plugins/contributors_stats/user_data/fetch.rb +16 -0
  22. data/lib/plugins/contributors_stats/user_data/simple.rb +23 -0
  23. data/test/contributors_stats/base_test.rb +36 -0
  24. data/test/contributors_stats/calculator_test.rb +70 -0
  25. data/test/contributors_stats/formatter_test.rb +77 -0
  26. data/test/contributors_stats/json_helper_test.rb +76 -0
  27. data/test/contributors_stats/reader_test.rb +59 -0
  28. data/test/contributors_stats_test.rb +13 -0
  29. data/test/fixtures-gh/orgs/railsinstaller/repos.json +269 -0
  30. data/test/fixtures-gh/repos/mpapis/rubygems-bundler/contributors.json +173 -0
  31. data/test/fixtures-gh/repos/railsinstaller/railsinstaller-nix/contributors.json +40 -0
  32. data/test/fixtures-gh/repos/railsinstaller/railsinstaller-windows/contributors.json +116 -0
  33. data/test/fixtures-gh/repos/railsinstaller/website/contributors.json +192 -0
  34. data/test/fixtures-gh/repos/rvm/pluginator/contributors.json +40 -0
  35. data/test/fixtures-gh/users/acco.json +31 -0
  36. data/test/fixtures-gh/users/alexch.json +31 -0
  37. data/test/fixtures-gh/users/biow0lf.json +31 -0
  38. data/test/fixtures-gh/users/drnic.json +31 -0
  39. data/test/fixtures-gh/users/ebertech.json +31 -0
  40. data/test/fixtures-gh/users/edwardchiu38.json +31 -0
  41. data/test/fixtures-gh/users/emachnic.json +31 -0
  42. data/test/fixtures-gh/users/envygeeks.json +31 -0
  43. data/test/fixtures-gh/users/gpxl.json +31 -0
  44. data/test/fixtures-gh/users/jc00ke.json +31 -0
  45. data/test/fixtures-gh/users/joshbuddy.json +31 -0
  46. data/test/fixtures-gh/users/luigidr.json +31 -0
  47. data/test/fixtures-gh/users/luislavena.json +31 -0
  48. data/test/fixtures-gh/users/metaskills.json +31 -0
  49. data/test/fixtures-gh/users/mpapis.json +31 -0
  50. data/test/fixtures-gh/users/parndt.json +31 -0
  51. data/test/fixtures-gh/users/pw.json +31 -0
  52. data/test/fixtures-gh/users/tjouan.json +31 -0
  53. data/test/fixtures-gh/users/veganstraightedge.json +31 -0
  54. data/test/fixtures-gh/users/wayneeseguin.json +31 -0
  55. data/test/fixtures-gh/users/whitequark.json +31 -0
  56. data/test/plugins/formatter/html_test.rb +28 -0
  57. data/test/plugins/reader/gh_org_test.rb +32 -0
  58. data/test/plugins/reader/gh_repo_test.rb +31 -0
  59. data/test/plugins/updater/html_test.rb +55 -0
  60. data/test/plugins/user_data/fetch_test.rb +16 -0
  61. data/test/plugins/user_data/simple_test.rb +18 -0
  62. data/test/test_helper.rb +27 -0
  63. metadata +229 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 8c726ad0986d83954b3b2bddde135824e1421dc2
4
+ data.tar.gz: 676fca44931563548b82c5abeb09cf34ae94de8b
5
+ SHA512:
6
+ metadata.gz: 83969f2f0da917ba5d1a5a0ce2c43785e8a7e90073a7b140c98493252fb97228c68017330cdd1bccd3f0d1427cfdfd1ba0bb715793c5d0123262290c2895c39a
7
+ data.tar.gz: acd9a3de698b3903ab65f6950e3c6bb930fcf1f2f6ca736cd806b0245e614d23b88503bbfe4695aad04882018b0ecab61063d97a511649095f832949f9541deb
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ /coverage
2
+ /doc
3
+ /.yardoc
4
+ /vendor
5
+ /*.gem
6
+ /Gemfile.lock
data/.travis.yml ADDED
@@ -0,0 +1,11 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0.0
4
+ notifications:
5
+ irc:
6
+ channels:
7
+ - "irc.freenode.org#rvm-test"
8
+ email:
9
+ recipients:
10
+ - mpapis@gmail.com
11
+ on_failure: change
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ #ruby=2.0.0
4
+
5
+ gemspec
data/LICENSE.md ADDED
@@ -0,0 +1,14 @@
1
+ Copyright (c) 2009-2011 Wayne E. Seguin
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
14
+
data/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # Contributors stats
2
+
3
+ [![Build Status](https://travis-ci.org/mpapis/contributors_stats.png?branch=master)](https://travis-ci.org/mpapis/contributors_stats)
4
+ [![Code Climate](https://codeclimate.com/github/mpapis/contributors_stats.png)](https://codeclimate.com/github/mpapis/contributors_stats)
5
+ [![Dependency Status](https://gemnasium.com/mpapis/contributors_stats.png)](https://gemnasium.com/mpapis/contributors_stats)
6
+
7
+ Update static files with contributors statistics.
8
+
9
+ * Static content update of contributors statistics.
10
+ * Plugable:
11
+ ** backends to load data
12
+ ** user details calculation
13
+ ** output fomratters
14
+ ** target files updating
15
+
16
+ ## Usage
17
+
18
+ Contributors stats consists on two basic parts and few plugins.
19
+
20
+ ```ruby
21
+ calculator = ContributorsStats.load(<options>)
22
+ calculator.load(:<type> => '<name>')
23
+ calculator.user_data_type = :<user_data_type>
24
+ formatter = calculator.format(:<format>)
25
+ formatter.save('<file>', ...)
26
+ formatter.update('<file>', ..., options: <options>)
27
+ puts formatter.content
28
+ ```
29
+
30
+ Plugins are loaded automatically, for example `calculator.user_data_type = :fetch`
31
+ will load `plugins/contributors_stats/user_data/fetch.rb` and use
32
+ `ContributorsStats::UserData::Fetch` and use it to process user details calculation.
33
+
34
+ ## Examples
35
+
36
+ ```ruby
37
+ ContributorsStats.load(gh_org: 'railsisntaller').format(:markdown).save('public/contributors.md')
38
+ ContributorsStats.load(gh_repo: 'railsisntaller/website').format.update('public/index.html')
39
+ ```
40
+
41
+ ```shell
42
+ ruby -I lib:lib/plugins -rcontributors_stats -e "puts ContributorsStats.load(:gh_org => 'rvm', :gh_repo => ['wayneeseguin/rvm', 'wayneeseguin/rvm-capistrano'], :user_data => :fetch).format(:html).content"
43
+ ```
data/Rakefile ADDED
@@ -0,0 +1,28 @@
1
+ require "rake/testtask"
2
+ require "yard"
3
+ require "yard/rake/yardoc_task"
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.verbose = true
7
+ t.libs.push("test")
8
+ t.pattern = "test/**/*_test.rb"
9
+ end
10
+
11
+ YARD::Rake::YardocTask.new do |t|
12
+ t.before = Proc.new do
13
+ puts "\n[YARD] Generating documentation\n\n"
14
+ end
15
+ t.files = ['lib/**/*.rb']
16
+ # --quiet / --verbose =>
17
+ t.options = ['--quiet', '--list-undoc', '--compact', '--verbose']
18
+ end
19
+
20
+ task :default => [:test, :yard]
21
+
22
+ task :example_data do
23
+ require "example_data"
24
+ example = ExampleData.new
25
+ example.parse_org( example.url_builder("orgs/railsinstaller/repos") )
26
+ example.parse_repo( example.url_builder("repos/rvm/pluginator/contributors") )
27
+ example.parse_repo( example.url_builder("repos/mpapis/rubygems-bundler/contributors") )
28
+ end
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require File.expand_path("../lib/contributors_stats/version.rb", __FILE__)
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "contributors_stats"
7
+ spec.version = ContributorsStats::VERSION
8
+ spec.license = 'Apache'
9
+ spec.author = "Michal Papis"
10
+ spec.email = "mpapis@gmail.com"
11
+ spec.homepage = "https://github.com/mpapis/contributors_stats"
12
+ spec.summary = %q{Calculate statics for multiple project contributoions.}
13
+
14
+ spec.files = `git ls-files`.split("\n")
15
+ spec.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
+
17
+ spec.required_ruby_version = ">= 2.0.0"
18
+
19
+ spec.add_dependency "pluginator"
20
+
21
+ %w{rake simplecov coveralls yard redcarpet}.each do |name|
22
+ spec.add_development_dependency(name)
23
+ end
24
+ end
@@ -0,0 +1,28 @@
1
+ #/usr/bin/env ruby
2
+
3
+ ##
4
+ # static content update for speed and avoiding GH limits
5
+ # read GitHub contributions, transform to urls and update files
6
+ # all public methods return self for easy chaining
7
+ #
8
+ # Example:
9
+ #
10
+ # ContributorsStats.load(org: 'railsisntaller').format.update('public/contributors.html')
11
+ # contributors = ContributorsStats.load(repo: 'railsisntaller/website')
12
+ # contributors.format(:html).update('public/index.html', 'public/contributors.html')
13
+ # contributors.format(:markdown).save('public/contriutors.md')
14
+ #
15
+
16
+ require 'pluginator'
17
+ require 'contributors_stats/calculator'
18
+
19
+ # Calculate contribution statistics for projects,
20
+ # by default supports github organizations and repositories,
21
+ # but is easily extendible with plugins.
22
+ module ContributorsStats
23
+ # Initialize ContributorsStats
24
+ # @see ContributorsStats::Calculator.new
25
+ def self.load(options = {})
26
+ ContributorsStats::Calculator.new(options)
27
+ end
28
+ end
@@ -0,0 +1,29 @@
1
+ module ContributorsStats
2
+
3
+ # Basis for ContributorsStats, includes logging and plugins support
4
+ class Base
5
+ attr_accessor :logger, :options
6
+
7
+ def initialize(options = {})
8
+ @logger = $stdout
9
+ @logger = options.delete(:logger) if options[:logger]
10
+ @options = options
11
+ end
12
+
13
+ private
14
+
15
+ def filter_options(type)
16
+ @options.select do |key, value|
17
+ plugins.class_exist?(type, key)
18
+ end
19
+ end
20
+
21
+ def plugins
22
+ @plugins ||= Pluginator.find("contributors_stats", extends: %i{first_class class_exist})
23
+ end
24
+
25
+ def log(text)
26
+ logger.respond_to?(:info) ? logger.info(text) : logger.puts(text)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,50 @@
1
+ require 'contributors_stats/reader'
2
+ require 'contributors_stats/formatter'
3
+
4
+ module ContributorsStats
5
+
6
+ # Placeholder for user details resolution plugins
7
+ module UserData
8
+ end
9
+
10
+ # Calculates statistics gathered from multiple sources
11
+ class Calculator < Reader
12
+ attr_accessor :user_data_type
13
+
14
+ def initialize(options = {})
15
+ @user_data_type = options.delete(:user_data) if options[:user_data]
16
+ super(options)
17
+ end
18
+
19
+ # transform calculated data into asked format
20
+ # @param type [String] name of plugin to use for formatting
21
+ # @param options [Hash] list of options for the plugin to use
22
+ # @return [ContributorsStats::Formatter]
23
+ def format(type = :html, options = {})
24
+ ContributorsStats::Formatter.new(calculated_data, type, options)
25
+ end
26
+
27
+ private
28
+
29
+ # group data, calculate contributions, sort by contributions
30
+ def calculated_data
31
+ @data ||= @raw_data.group_by { |contributor|
32
+ contributor['login']
33
+ }.map {|login, data|
34
+ log "user: #{login}"
35
+ [login, user_data(login, data)]
36
+ }.sort_by{|login, data|
37
+ [1000000/data['contributions'], login]
38
+ }
39
+ end
40
+
41
+ def contributions(data)
42
+ data.map{|repo| repo['contributions'].to_i}.inject(&:+)
43
+ end
44
+
45
+ def user_data(login, data)
46
+ @user_data_plugin ||= plugins.first_class!("user_data", user_data_type || :simple)
47
+ @user_data_plugin.load(login, data.first, contributions(data))
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,57 @@
1
+ module ContributorsStats
2
+
3
+ # Placeholder for updater plugins
4
+ module Updater
5
+ end
6
+
7
+ # Takes care of formatting data and saving it
8
+ class Formatter
9
+ attr_reader :content
10
+
11
+ def initialize(data, type = :html, options = {})
12
+ format(data, type, options)
13
+ end
14
+
15
+ # overwrite target with the formatted content
16
+ # @param targets [Array] list of files to overwrite
17
+ def save(*targets)
18
+ targets.flatten.each do |file|
19
+ File.open(file, 'w') { |f| f.write(content * "\n") }
20
+ end
21
+ end
22
+
23
+ # update target with the formatted content
24
+ # @param targets [Array] list of files to update
25
+ # @param options [Hash] options are passed to updater plugin, check
26
+ def update(*targets, options: {})
27
+ targets.flatten.each do |file|
28
+ plugin = plugins.first_ask!("updater", :handles?, file)
29
+ update_file(file) do |file_content|
30
+ plugin.update(file_content, content, options)
31
+ end
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def plugins
38
+ @plugins ||= Pluginator.find("contributors_stats", extends: %i{first_ask first_class})
39
+ end
40
+
41
+ def format(data, type = :html, options = {})
42
+ formatter_plugin = plugins.first_class!("formatter", type).new(options)
43
+ @content = data.map do |login, user_data|
44
+ formatter_plugin.format(login, user_data)
45
+ end
46
+ end
47
+
48
+ # Allow editing file text in a block
49
+ # @example
50
+ # update_file('some.txt'){|text| text.gsub(/bla/,'ble')}
51
+ def update_file(file)
52
+ text = File.read(file)
53
+ text = yield text
54
+ File.open(file, 'w') { |f| f.write(text) }
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,49 @@
1
+ require 'json'
2
+ require 'open-uri'
3
+
4
+ module ContributorsStats
5
+
6
+ # Support code for loading json urls/files
7
+ module JsonHelper
8
+
9
+ # Build full path to resource to use
10
+ def url_builder(path)
11
+ "#{self.path_prefix}#{path}#{self.path_suffix}"
12
+ end
13
+
14
+ # Load json from url, fallback to prefix
15
+ def load_json(url)
16
+ open(url){ |json| JSON.load(json) }
17
+ rescue Exception => e
18
+ if File.exist?("#{self.path_prefix}#{url}")
19
+ open("#{self.path_prefix}#{url}"){ |json| JSON.load(json) }
20
+ elsif File.exist?("#{self.path_prefix}#{url}#{self.path_suffix}")
21
+ open("#{self.path_prefix}#{url}#{self.path_suffix}"){ |json| JSON.load(json) }
22
+ else
23
+ raise e
24
+ end
25
+ end
26
+
27
+ # get prefix, sets the default if empty, makes sure it's ending with '/'
28
+ def path_prefix
29
+ @path_prefix ||= "https://api.github.com"
30
+ @path_prefix+="/" if @path_prefix != "" && @path_prefix[-1] != "/"
31
+ @path_prefix
32
+ end
33
+
34
+ # get suffix, sets the default if empty, makes sure it's starts with '.'
35
+ def path_suffix
36
+ @path_suffix ||= ""
37
+ @path_suffix = ".#{@path_suffix}" if @path_suffix != "" && @path_suffix[0] != "."
38
+ @path_suffix
39
+ end
40
+
41
+ protected
42
+
43
+ def configure_path(prefix, suffix)
44
+ @path_prefix = prefix
45
+ @path_suffix = suffix
46
+ end
47
+
48
+ end
49
+ end
@@ -0,0 +1,52 @@
1
+ require 'contributors_stats/base'
2
+ require 'contributors_stats/json_helper'
3
+
4
+ module ContributorsStats
5
+
6
+ # Base for reading ContributorsStats data
7
+ class Reader < Base
8
+ include ContributorsStats::JsonHelper
9
+
10
+ attr_reader :data, :raw_data
11
+
12
+ def initialize(options = {})
13
+ configure_path(*options.delete(:configure_path)) if options[:configure_path]
14
+ super(options)
15
+ @raw_data = []
16
+ @data = nil
17
+ parse_readers(filter_options("reader"))
18
+ end
19
+
20
+ # load data using given plugin
21
+ # @param type [String] plugin to use
22
+ # @param name [String] name to pass to the plugin
23
+ def load(type, name)
24
+ reader_plugin(type).load(name) do |data, name|
25
+ log "repository: #{name}"
26
+ @raw_data += data
27
+ end
28
+ @data = nil
29
+ end
30
+
31
+ private
32
+
33
+ def reader_plugin(type)
34
+ plugin = plugins.first_class!("reader", type)
35
+ if plugin.kind_of?(ContributorsStats::JsonHelper)
36
+ plugin.send(:configure_path, path_prefix, path_suffix)
37
+ end
38
+ plugin
39
+ end
40
+
41
+ def parse_readers(options = {})
42
+ options.each do |type, name|
43
+ if name.kind_of?(Array)
44
+ name.each{|n| load(type,n)}
45
+ else
46
+ load(type, name)
47
+ end
48
+ end
49
+ end
50
+
51
+ end
52
+ end
@@ -0,0 +1,4 @@
1
+ module ContributorsStats
2
+ # gem version
3
+ VERSION = "1.0.0"
4
+ end
@@ -0,0 +1,58 @@
1
+ require 'contributors_stats/json_helper'
2
+
3
+ # Dump some test data from github, rather one time, but who knows ;)
4
+ class ExampleData
5
+ include ContributorsStats::JsonHelper
6
+
7
+ # setup proper context for dumping data
8
+ def initialize(target_path = "../../test/fixtures-gh")
9
+ target_path = File.expand_path( target_path, __FILE__ ) unless Dir.exist?(target_path)
10
+ Dir.chdir(target_path)
11
+ `rm -rf *` # clean
12
+ end
13
+
14
+ # parse organization data from given url, parses also repositories
15
+ # @see .parse_repo
16
+ def parse_org(org_url)
17
+ puts "reading: #{org_url}"
18
+ org_data = load_json(org_url)
19
+ write(org_url, org_data)
20
+ org_data.each do |repo|
21
+ parse_repo(repo['contributors_url'], " ")
22
+ end
23
+ end
24
+
25
+ # parse repository data from given url, parses also users
26
+ # @see .parse_repo
27
+ def parse_repo(contributors_url, str_prefix="")
28
+ puts "#{str_prefix}reading: #{contributors_url}"
29
+ contributors_data = load_json(contributors_url)
30
+ write(contributors_url, contributors_data)
31
+ contributors_data.each do |contributor|
32
+ parse_user(contributor['url'], str_prefix+" ")
33
+ end
34
+ end
35
+
36
+ # parse user data from given url
37
+ def parse_user(user_url, str_prefix="")
38
+ puts "#{str_prefix}reading: #{user_url}"
39
+ user_data = load_json(user_url)
40
+ write(user_url, user_data)
41
+ end
42
+
43
+ # saves content to path corresponding to the given file url
44
+ def write(file, content)
45
+ file = remove_prefix(file)
46
+ return if File.exist?(file)
47
+ `mkdir -p #{File.dirname(file)}`
48
+ File.open(file, "w+") { |f|
49
+ f.write(remove_prefix(JSON.pretty_generate(content)))
50
+ }
51
+ end
52
+
53
+ # remove prefix from all urls in the given content
54
+ def remove_prefix(conten)
55
+ conten.gsub(/#{path_prefix}([^"]*)/, "\\1.json")
56
+ end
57
+
58
+ end