open_source_stats 0.0.1

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c70f2d8e89dc9d029d1929cbfbd784e91bb95a85
4
+ data.tar.gz: 4b9b9d02d18899a671b999fbe19b1b2455ad42bd
5
+ SHA512:
6
+ metadata.gz: a958a8d47104f7d21e66b26dcd40b18368bdbd604a22f9fc7c81ed3e90bf5aab6227be44f139c6904bcae78a5acc2dcff91d6577071420c814527d74448dfceb
7
+ data.tar.gz: 3956f62f8a04c95cf4a046ec7577982261099ad2c5380c0feb865d139ebb75261ed51f665cab4a6858e971e457fd87b5ba7d34283bab2c14080e27654b4f5477
@@ -0,0 +1,16 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
15
+ .env
16
+ *.gem
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in open_source_stats.gemspec
4
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Ben Balter
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,43 @@
1
+ # Open Source Stats
2
+
3
+ *A quick script to generate metrics about the contribution your organization makes to the open source community in a 24-hour period*.
4
+
5
+ ## What it looks at
6
+
7
+ * Public activity across all public repositories from members of a given team
8
+ * Public activity across all users on repositories owned by a given organization or organizations
9
+
10
+ ## How it works
11
+
12
+ By doing terrible, terrible things to GitHub's events API.
13
+
14
+ ## Setup
15
+
16
+ 1. `gem install open_source_stats`
17
+ 2. Create a `.env` file and add the following:
18
+ * `GITHUB_TOKEN` - A personal access token with `read:org` scope
19
+ * `GITHUB_TEAM_ID` - The numeric ID of the team to pull users from (e.g., 12345)
20
+ * `GITHUB_ORGS` - A comma separated list of the orgs to look at (e.g, `github,atom`)
21
+ 3. Run `bundle exec oss`
22
+
23
+ This will spit markdown formatted results into standard out.
24
+
25
+ ## Output
26
+
27
+ Output might look something like this...
28
+
29
+ | Metric | Count |
30
+ |----------------------|-------|
31
+ | Repositories created | 6 |
32
+ | Issue comments | 309 |
33
+ | Issues opened | 40 |
34
+ | Issues closed | 30 |
35
+ | Repos open sourced | 1 |
36
+ | Pull requests opened | 32 |
37
+ | Pull requests merged | 35 |
38
+ | Versions released | 3 |
39
+ | Pushes | 442 |
40
+ | Commits | 1548 |
41
+ | Total events | 945 |
42
+
43
+ Metrics will also be broken down by organization, by user, and by repository
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
data/bin/oss ADDED
@@ -0,0 +1,38 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ require_relative "../lib/open_source_stats"
4
+ require 'active_support/core_ext/string/inflections'
5
+ require 'terminal-table'
6
+
7
+ oss = OpenSourceStats.new
8
+
9
+ def pretty_print_hash(hash)
10
+ rows = []
11
+ hash.each { |key, value| rows << [key.to_s.humanize, value] }
12
+ table = Terminal::Table.new(:rows => rows, :style => { :border_i => "|" }, :headings => ["Metric", "Count"]).to_s
13
+ puts table.split("\n")[1...-1].join("\n") + "\n\n"
14
+ end
15
+
16
+ puts "# Open source actvity for the 24-hour period begining at #{OpenSourceStats.start_time.strftime("%l:%S %p %Z on %B %-d, %Y")}\n\n"
17
+
18
+ puts "## Overall\n\n"
19
+ pretty_print_hash oss.stats
20
+
21
+ puts "## By organization\n\n"
22
+ oss.orgs.each do |org|
23
+ puts "### #{org.name.humanize}\n\n"
24
+ pretty_print_hash oss.stats(org.events)
25
+ end
26
+
27
+ puts "## Top users\n\n"
28
+ users = oss.users.reject { |u| u.login == "hubot" }.sort_by { |u| u.events.count }.reverse[0...10]
29
+ users.each_with_index do |user, index|
30
+ puts "### #{index+1}. @#{user.login}\n\n"
31
+ pretty_print_hash oss.stats(user.events)
32
+ end
33
+
34
+ puts "## Most active repositories\n\n"
35
+ repos = oss.events.group_by { |e| e.repo }.map { |k,v| { k => v.count } }.sort_by { |r| r.values.first }.reverse[0...10]
36
+ repos.each_with_index do |repo, index|
37
+ puts "#{index + 1}. #{repo.keys.first} - #{repo.values.first} events"
38
+ end
@@ -0,0 +1,102 @@
1
+ require_relative "./open_source_stats/version.rb"
2
+ require_relative "./open_source_stats/user"
3
+ require_relative "./open_source_stats/organization"
4
+ require_relative "./open_source_stats/event"
5
+ require 'active_support'
6
+ require 'active_support/core_ext/numeric/time'
7
+ require 'octokit'
8
+ require 'dotenv'
9
+
10
+ Dotenv.load
11
+
12
+ class OpenSourceStats
13
+
14
+ # Authenticated Octokit client instance
15
+ def self.client
16
+ @client ||= Octokit::Client.new :access_token => ENV["GITHUB_TOKEN"]
17
+ end
18
+
19
+ # Returns a Ruby Time instance coresponding to the earliest possible event timestamp
20
+ def self.start_time
21
+ @start_time ||= Time.now - 24.hours
22
+ end
23
+
24
+ # Helper method to overide the timestamp
25
+ def self.start_time=(time)
26
+ @start_time = time
27
+ end
28
+
29
+ def team
30
+ @team ||= client.team ENV["GITHUB_TEAM_ID"]
31
+ end
32
+
33
+ # Returns an array of Users from the given team
34
+ def users
35
+ @users ||= begin
36
+ users = client.team_members ENV["GITHUB_TEAM_ID"], :per_page => 100
37
+ while client.last_response.rels[:next] && client.rate_limit.remaining > 0
38
+ users.concat client.get client.last_response.rels[:next].href
39
+ end
40
+ users.map { |u| User.new u[:login] }
41
+ end
42
+ end
43
+
44
+ # Returns an array of Events across all Users
45
+ def user_events
46
+ @user_events ||= users.map { |u| u.events }.flatten
47
+ end
48
+
49
+ # Returns an array of Organizations from the specified list of organizations
50
+ def orgs
51
+ @orgs ||= ENV["GITHUB_ORGS"].split(/, ?/).map { |o| Organization.new(o) }
52
+ end
53
+
54
+ # Returns an array of Events across all Organziations
55
+ def org_events
56
+ @org_events ||= orgs.map { |o| o.events }.flatten
57
+ end
58
+
59
+ # Returns an array of unique Events accross all Users and Organizations
60
+ def events
61
+ @events ||= user_events.concat(org_events).uniq
62
+ end
63
+
64
+ # Returns the calculated stats hash
65
+ def stats(events=events)
66
+ {
67
+ :repositories_created => event_count(events, :type =>"CreateEvent"),
68
+ :issue_comments => event_count(events, :type =>"IssueCommentEvent"),
69
+ :issues_opened => event_count(events, :type =>"IssuesEvent", :action => "opened"),
70
+ :issues_closed => event_count(events, :type =>"IssuesEvent", :action => "closed"),
71
+ :repos_open_sourced => event_count(events, :type =>"PublicEvent"),
72
+ :pull_requests_opened => event_count(events, :type =>"PullRequestEvent", :action => "opened"),
73
+ :pull_requests_merged => event_count(events, :type =>"PullRequestEvent", :action => "closed", :merged => true),
74
+ :versions_released => event_count(events, :type =>"ReleaseEvent", :action => "published"),
75
+ :pushes => event_count(events, :type =>"PushEvent"),
76
+ :commits => events.map { |e| e.commits }.flatten.inject{|sum,x| sum + x },
77
+ :total_events => event_count(events)
78
+ }
79
+ end
80
+
81
+ private
82
+
83
+ # Helper method to count events by set conditions
84
+ #
85
+ # Options:
86
+ # :type - the type of event to filter by
87
+ # :action - the payload action to filter by
88
+ # :merged - whether the pull request has been merged
89
+ #
90
+ # Returns an integer reflecting the resulting event count
91
+ def event_count(events, options={})
92
+ events = events.select { |e| e.type == options[:type] } if options[:type]
93
+ events = events.select { |e| e.payload[:action] == options[:action] } if options[:action]
94
+ events = events.select { |e| e.payload[:pull_request][:merged] == options[:merged] } if options[:merged]
95
+ events.count
96
+ end
97
+
98
+ # Helper method to reference the client stored on the class and DRY things up a bit
99
+ def client
100
+ OpenSourceStats.client
101
+ end
102
+ end
@@ -0,0 +1,67 @@
1
+ class OpenSourceStats
2
+ class Event
3
+
4
+ # Hash of event types to count as an event
5
+ #
6
+ # The top-level key should be the event type
7
+ # The top-level value should be a hash used to filter acceptable key/value pairs
8
+ #
9
+ # For the sub-hash, keys are keys to look for in the event,
10
+ # and the value is an array of acceptable values
11
+ #
12
+ # Events that don't match an event type and key/value pair with fail the in_scope? check
13
+ TYPES = {
14
+ "CreateEvent" => { :ref_type => ["repository"] },
15
+ "IssueCommentEvent" => {:action => ["created"] },
16
+ "IssuesEvent" => { :action => ["opened", "closed"] },
17
+ "PublicEvent" => {},
18
+ "PullRequestEvent" => { :action => ["opened", "closed"]},
19
+ "PullRequestReviewCommentEvent" => { :action => ["created"] },
20
+ "PushEvent" => {},
21
+ "ReleaseEvent" => { :action => "published" },
22
+ }
23
+
24
+ attr_reader :type, :repo, :actor, :time, :payload, :id
25
+
26
+ # Takes the raw event object from Octokit
27
+ def initialize(event)
28
+ @type = event[:type]
29
+ @repo = event[:repo][:name]
30
+ @actor = User.new(event[:actor][:login])
31
+ @time = event[:created_at]
32
+ @payload = event[:payload]
33
+ @id = event[:id]
34
+ end
35
+
36
+ # Is this event type within our timeframe AND an acceptable event type?
37
+ def in_scope?
38
+ return false unless time >= OpenSourceStats.start_time
39
+ return false unless TYPES.keys.include?(type)
40
+ TYPES[type].each do |key, values|
41
+ return false unless values.include? payload[key]
42
+ end
43
+ true
44
+ end
45
+
46
+ # Use event IDs to compare uniquness via Array#uniq
47
+ def ==(other_event)
48
+ id == other_event.id
49
+ end
50
+ alias_method :eql?, :==
51
+
52
+ # How many commmits does this event involve?
53
+ #
54
+ # For push events, this is the number of commits pushed
55
+ # For pull request events, this is the number of commits contained in the PR
56
+ def commits
57
+ case type
58
+ when "PushEvent"
59
+ payload[:commits].count
60
+ when "PullRequestEvent"
61
+ payload[:pull_request][:commits]
62
+ else
63
+ 0
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,9 @@
1
+ class OpenSourceStats
2
+ class Organization < OpenSourceStats::User
3
+ attr_accessor :name
4
+
5
+ def initialize(name)
6
+ @name = name
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,41 @@
1
+ class OpenSourceStats
2
+ class User
3
+
4
+ attr_accessor :login
5
+
6
+ def initialize(login)
7
+ @login = login
8
+ end
9
+
10
+ # Returns an array of in-scope Events from the user's public activity feed
11
+ def events
12
+ @events ||= begin
13
+ if self.class == OpenSourceStats::User
14
+ events = client.user_public_events login, :per_page => 100
15
+ else
16
+ events = client.organization_public_events name, :per_page => 100
17
+ end
18
+
19
+ events.concat client.get client.last_response.rels[:next].href while next_page?
20
+ events = events.map { |e| Event.new(e) }
21
+ events.select { |e| e.in_scope? }
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ # Helper method to access the shared Octokit instance
28
+ def client
29
+ OpenSourceStats.client
30
+ end
31
+
32
+ # Helper method to improve readability.
33
+ # Asks is there another page to the results?
34
+ # Looks at both pagination and if we've gone past our allowed timeframe
35
+ def next_page?
36
+ client.last_response.rels[:next] &&
37
+ client.rate_limit.remaining > 0 &&
38
+ client.last_response.data.last[:created_at] >= OpenSourceStats.start_time
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,3 @@
1
+ class OpenSourceStats
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'open_source_stats/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "open_source_stats"
8
+ spec.version = OpenSourceStats::VERSION
9
+ spec.authors = ["Ben Balter"]
10
+ spec.email = ["ben.balter@github.com"]
11
+ spec.summary = %q{A quick script to generate metrics about the contribution your organization makes to the open source community in a 24-hour period}
12
+ spec.homepage = "https://github.com/benbalter/open_source_stats"
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files -z`.split("\x0")
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_dependency "octokit"
21
+ spec.add_dependency "dotenv"
22
+ spec.add_dependency "activesupport"
23
+ spec.add_dependency "terminal-table"
24
+
25
+ spec.add_development_dependency "pry"
26
+ spec.add_development_dependency "bundler", "~> 1.6"
27
+ spec.add_development_dependency "rake", "~> 10.0"
28
+ end
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+
3
+ bundle install
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+
3
+ bundle exec gem build open_source_stats.gemspec
@@ -0,0 +1 @@
1
+ bundle exec pry -r "./lib/open_source_stats.rb"
@@ -0,0 +1,38 @@
1
+ #!/bin/sh
2
+ # Tag and push a release.
3
+
4
+ set -e
5
+
6
+ # Make sure we're in the project root.
7
+
8
+ cd $(dirname "$0")/..
9
+
10
+ # Build a new gem archive.
11
+
12
+ rm -rf open_source_stats-*.gem
13
+ gem build -q open_source_stats.gemspec
14
+
15
+ # Make sure we're on the master branch.
16
+
17
+ (git branch | grep -q '* master') || {
18
+ echo "Only release from the master branch."
19
+ exit 1
20
+ }
21
+
22
+ # Figure out what version we're releasing.
23
+
24
+ tag=v`ls open_source_stats-*.gem | sed 's/^open_source_stats-\(.*\)\.gem$/\1/'`
25
+
26
+ # Make sure we haven't released this version before.
27
+
28
+ git fetch -t origin
29
+
30
+ (git tag -l | grep -q "$tag") && {
31
+ echo "Whoops, there's already a '${tag}' tag."
32
+ exit 1
33
+ }
34
+
35
+ # Tag it and bag it.
36
+
37
+ gem push open_source_stats-*.gem && git tag "$tag" &&
38
+ git push origin master && git push origin "$tag"
metadata ADDED
@@ -0,0 +1,160 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: open_source_stats
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Ben Balter
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-04-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: octokit
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: dotenv
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: activesupport
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: terminal-table
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: pry
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: bundler
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.6'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.6'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '10.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '10.0'
111
+ description:
112
+ email:
113
+ - ben.balter@github.com
114
+ executables:
115
+ - oss
116
+ extensions: []
117
+ extra_rdoc_files: []
118
+ files:
119
+ - ".gitignore"
120
+ - Gemfile
121
+ - LICENSE.md
122
+ - README.md
123
+ - Rakefile
124
+ - bin/oss
125
+ - lib/open_source_stats.rb
126
+ - lib/open_source_stats/event.rb
127
+ - lib/open_source_stats/organization.rb
128
+ - lib/open_source_stats/user.rb
129
+ - lib/open_source_stats/version.rb
130
+ - open_source_stats.gemspec
131
+ - script/bootstrap
132
+ - script/cibuild
133
+ - script/console
134
+ - script/release
135
+ homepage: https://github.com/benbalter/open_source_stats
136
+ licenses:
137
+ - MIT
138
+ metadata: {}
139
+ post_install_message:
140
+ rdoc_options: []
141
+ require_paths:
142
+ - lib
143
+ required_ruby_version: !ruby/object:Gem::Requirement
144
+ requirements:
145
+ - - ">="
146
+ - !ruby/object:Gem::Version
147
+ version: '0'
148
+ required_rubygems_version: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ requirements: []
154
+ rubyforge_project:
155
+ rubygems_version: 2.2.0
156
+ signing_key:
157
+ specification_version: 4
158
+ summary: A quick script to generate metrics about the contribution your organization
159
+ makes to the open source community in a 24-hour period
160
+ test_files: []