git-heroes 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ddda5088666b7d915f39ca283765839033d28e0a
4
+ data.tar.gz: 62f5ebea3d678988928f60902acb1711e2e00a45
5
+ SHA512:
6
+ metadata.gz: 7d2303a8aea29cb9b06fe92becffd36a8950ffbac98bda9a5cc11d70cebe55d1f6898912bdf1843d34c88524c89e6984da823bf0ea5311dcbde4b28278f15882
7
+ data.tar.gz: 30a5a723b9877ebf60a9feebad39df26148da913ee4d92f1cd5e1402e8698a71a410fd247e515ea8cd820b810999ef2cda1eafa008bbef306e535e23888f9c5b
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.0.0-p247
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source ENV.fetch('GEM_SOURCE', 'https://rubygems.org')
2
+
3
+ # Specify your gem's dependencies in githeroes.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Julien Letessier
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # Git Heroes
2
+
3
+ Ever wanted to know your your team's top contributors are?
4
+
5
+ `git heroes` will tell you, based on their Github activity (pull requests,
6
+ comments, and merges).
7
+
8
+ **Caveat Emptor**: no hard metric that measures individuals is reliable. Please do
9
+ not use this to estimate someone's productivity. In combination with other
10
+ tools, it can be effectivee to detect trends, though.
11
+
12
+ ## Installation
13
+
14
+ $ gem install git-heroes
15
+
16
+ To preserve your Github API usage limit, the tool requires locally running
17
+ Redis instance for caching.
18
+
19
+ ## Usage
20
+
21
+ $ git heroes -r <your-organization> -t <github-token>
22
+
23
+ Details:
24
+
25
+ Usage: bin/git-heroes [options]
26
+ -r, --organization ORG Progress organization ORG (required)
27
+ -o, --output FILE Save report to FILE
28
+ -t, --token TOKEN A Github authentication token (defaults to GITHUB_TOKEN)
29
+ -w, --weeks WEEKS Report on the last WEEKS weeks (default 12).
30
+ -v, --verbose Run verbosely
31
+ -h, --help Show this message
32
+
33
+ ## Contributing
34
+
35
+ 1. Fork it
36
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
37
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
38
+ 4. Push to the branch (`git push origin my-new-feature`)
39
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require 'bundler/gem_tasks'
data/bin/git-heroes ADDED
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # TODO: 10 points for deploys !
4
+ #
5
+ require 'rubygems'
6
+ require 'bundler/setup'
7
+ require 'dotenv'
8
+ require 'optparse'
9
+
10
+ require 'git_heroes/connection'
11
+ require 'git_heroes/engine'
12
+ require 'git_heroes/logger'
13
+
14
+ Dotenv.load
15
+
16
+ OPTIONS = {
17
+ path: 'report.csv',
18
+ verbose: false,
19
+ token: ENV['GITHUB_TOKEN'],
20
+ weeks: 12
21
+ }
22
+
23
+ OptionParser.new do |opts|
24
+ opts.banner = "Usage: #{$0} [options]"
25
+
26
+ opts.on("-r", "--organization ORG", "Progress organization ORG (required)") do |org|
27
+ OPTIONS[:org] = org
28
+ end
29
+
30
+ opts.on("-o", "--output FILE", "Save report to FILE") do |path|
31
+ OPTIONS[:path] = path
32
+ end
33
+
34
+ opts.on("-t", "--token TOKEN", "A Github authentication token (defaults to GITHUB_TOKEN)") do |token|
35
+ OPTIONS[:token] = token
36
+ end
37
+
38
+ opts.on("-w", "--weeks WEEKS", Integer, "Report on the last WEEKS weeks (default #{OPTIONS[:weeks]}).") do |weeks|
39
+ OPTIONS[:weeks] = weeks
40
+ end
41
+
42
+ opts.on("-v", "--verbose", "Run verbosely") do |v|
43
+ OPTIONS[:verbose] = v
44
+ end
45
+
46
+ opts.on_tail("-h", "--help", "Show this message") do
47
+ $stderr.puts opts
48
+ exit 0
49
+ end
50
+ end.parse!
51
+
52
+
53
+ if OPTIONS[:org].nil?
54
+ $stderr.puts "No organization specified, aborting."
55
+ exit 1
56
+ end
57
+
58
+ if !OPTIONS[:verbose]
59
+ GitHeroes::Logger.instance.level = Logger::WARN
60
+ end
61
+
62
+ client = GitHeroes::Connection.new(token: OPTIONS[:token])
63
+ engine = GitHeroes::Engine.new(client:client, org:OPTIONS[:org], weeks:OPTIONS[:weeks])
64
+ engine.run
65
+ engine.export(OPTIONS[:path])
data/githeroes.gemspec ADDED
@@ -0,0 +1,31 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'git_heroes/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "git-heroes"
8
+ spec.version = GitHeroes::VERSION
9
+ spec.authors = ["Julien Letessier"]
10
+ spec.email = ["julien.letessier@gmail.com"]
11
+ spec.description = %q{Leaderboard of your team's Github activity}
12
+ spec.summary = %q{Leaderboard of your team's Github activity}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "octokit"
22
+ spec.add_dependency "dotenv"
23
+ spec.add_dependency "redis-activesupport"
24
+ spec.add_dependency "faraday-http-cache"
25
+ spec.add_dependency "redis"
26
+
27
+ spec.add_development_dependency "bundler", "~> 1.3"
28
+ spec.add_development_dependency "rake"
29
+ spec.add_development_dependency "pry"
30
+ spec.add_development_dependency "pry-nav"
31
+ end
@@ -0,0 +1,16 @@
1
+ require 'git_heroes'
2
+ require 'faraday'
3
+
4
+ class GitHeroes::Cache < Faraday::Middleware
5
+ def call(env, *args)
6
+ # do something with the request
7
+ force_cache = !! env[:request_headers]['X-Force-Cache']
8
+
9
+ @app.call(env).on_complete do |response|
10
+ # do something with the response
11
+ if force_cache
12
+ response[:response_headers]['cache-control'] = 'public, max-age=31536000'
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,28 @@
1
+ require 'delegate'
2
+ require 'octokit'
3
+ require 'git_heroes'
4
+ require 'git_heroes/cache'
5
+ require 'git_heroes/logger'
6
+ require 'git_heroes/serializer'
7
+ require 'git_heroes/ext/faraday'
8
+
9
+ class GitHeroes::Connection < SimpleDelegator
10
+ def initialize(token:nil)
11
+ stack = Faraday::Builder.new do |builder|
12
+ builder.use(:http_cache,
13
+ store: :redis_store,
14
+ store_options: %w(localhost/0/githeroes),
15
+ serializer: GitHeroes::Serializer,
16
+ logger: GitHeroes::Logger.instance)
17
+ builder.use GitHeroes::Cache
18
+ builder.use Octokit::Response::RaiseError
19
+ builder.adapter Faraday.default_adapter
20
+ end
21
+
22
+ client = Octokit::Client.new(
23
+ access_token: token,
24
+ middleware: stack)
25
+
26
+ super(client)
27
+ end
28
+ end
@@ -0,0 +1,129 @@
1
+ require 'set'
2
+ require 'git_heroes/ext/time'
3
+ require 'git_heroes/ext/octokit'
4
+ require 'active_support/all' # for #beginning_of_week
5
+ require 'csv'
6
+
7
+ class GitHeroes::Engine
8
+ POINTS = { pull: 5, comment: 1, merge: 2 }
9
+
10
+ def initialize(client:nil, org:nil, weeks:nil)
11
+ @client = client
12
+ @organisation = org
13
+ @data = {}
14
+ @users = Set.new
15
+
16
+ @end_time = Time.now.beginning_of_week
17
+ week_count = weeks
18
+ @weeks = (1..week_count).map { |idx| @end_time - idx.weeks }
19
+ @start_time = @weeks.last
20
+ end
21
+
22
+
23
+ def run
24
+ repositories = @client.organization_repositories(@organisation)
25
+ repositories.each do |repository|
26
+ each_pull_request(repository) do |pull_request, get_options|
27
+ process_pull_request(pull_request, get_options)
28
+ end
29
+ end
30
+ end
31
+
32
+
33
+ def export(path)
34
+ CSV.open(path, 'w') do |csv|
35
+ weeks = @weeks.sort
36
+ weeks_keys = weeks.map { |w| w.week_key }
37
+ header = %w(login) + weeks.map { |w| w.strftime('%Y.%W (%b %d)') }
38
+ csv << header
39
+ @users.each do |user|
40
+ row = [user]
41
+ weeks_keys.each do |weeks_keys|
42
+ row << @data[user][weeks_keys]
43
+ end
44
+ csv << row
45
+ end
46
+ end
47
+ end
48
+
49
+
50
+ private
51
+
52
+
53
+ def process_pull_request(pull_request, get_options)
54
+ non_self_comments = 0
55
+ ttl = pull_request.merged_at? ? nil : 1.day
56
+
57
+ each_comment(pull_request, get_options) do |comment|
58
+ next if comment.user.login == pull_request.user.login
59
+ give_points(:comment, comment.user.login, comment.created_at)
60
+ non_self_comments += 1
61
+ end
62
+
63
+ merger = pull_request.merged_by
64
+ self_merge = (merger && merger.login == pull_request.user.login)
65
+
66
+ if (non_self_comments > 0) || !self_merge
67
+ give_points(:pull, pull_request.user.login, pull_request.created_at)
68
+ end
69
+
70
+ if pull_request.merged_by
71
+ give_points(:merge, pull_request.merged_by.login, pull_request.merged_at)
72
+ end
73
+ end
74
+
75
+
76
+ def give_points(kind, user, timestamp)
77
+ return unless @start_time < timestamp && timestamp < @end_time
78
+ week_key = timestamp.week_key
79
+ @users.add user
80
+ @data[user] ||= {}
81
+ @data[user][week_key] ||= 0
82
+ @data[user][week_key] += POINTS[kind]
83
+ end
84
+
85
+
86
+ def each_comment(pull_request, get_options)
87
+ # comments on PR itself
88
+ uri = pull_request.rels[:comments].href
89
+ @client.request(:get, uri, get_options).each do |comment|
90
+ yield comment
91
+ end
92
+
93
+ # comments on diff (unsupported by Octokit)
94
+ uri = "https://api.github.com/repos/#{pull_request.base.repo.full_name}/pulls/#{pull_request.number}/comments"
95
+ @client.request(:get, uri, get_options).each do |comment|
96
+ yield comment
97
+ end
98
+ end
99
+
100
+
101
+ def each_pull_request(repository)
102
+ [:open, :closed].each do |status|
103
+ page = 1
104
+ while true
105
+ pull_requests = @client.pull_requests(repository.full_name, status, per_page: 20, page: page)
106
+ break if pull_requests.empty?
107
+ break if pull_requests.all? { |pr| pr.created_at < @start_time }
108
+ page += 1
109
+
110
+ pull_requests.each do |pull_request|
111
+ # skip unmerged closed PRs
112
+ next if pull_request.closed_at? && !pull_request.merged_at?
113
+ # skip too old PRs
114
+ next if pull_request.created_at < @start_time
115
+
116
+ get_options = {}
117
+ if pull_request.merged_at? || pull_request.closed_at?
118
+ # old, cacheable forever
119
+ get_options[:headers] = { 'X-Force-Cache' => '1' }
120
+ end
121
+
122
+ pull_request = @client.request(:get, pull_request.rels[:self].href, get_options)
123
+
124
+ yield pull_request, get_options
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,13 @@
1
+ require 'faraday-http-cache'
2
+
3
+ # Monkey-patch Faraday cache
4
+ # Forces long caching, even for private resources
5
+ Faraday::HttpCache::CacheControl.class_eval do
6
+ def private?
7
+ false
8
+ end
9
+
10
+ def shared_max_age
11
+ 36000
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ # We need to access #request, since #get is borken
2
+ # (passes headers in the query string)
3
+ class Octokit::Client
4
+ public :request
5
+ end
@@ -0,0 +1,11 @@
1
+ # Provides #Time.wnum
2
+
3
+ class Time
4
+ def wnum
5
+ strftime('%W').to_i
6
+ end
7
+
8
+ def week_key
9
+ strftime('%Y.%W')
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ require 'git_heroes'
2
+ require 'logger'
3
+ require 'delegate'
4
+ require 'singleton'
5
+
6
+ class GitHeroes::Logger < SimpleDelegator
7
+ include Singleton
8
+
9
+ def initialize
10
+ logger = ::Logger.new($stderr)
11
+ super(logger)
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ require 'git_heroes'
2
+
3
+ module GitHeroes::Serializer
4
+ extend self
5
+
6
+ def load(data)
7
+ Marshal.load(Zlib::Inflate.inflate(data))
8
+ end
9
+
10
+ def dump(data)
11
+ Zlib::Deflate.deflate(Marshal.dump(data))
12
+ end
13
+ end
@@ -0,0 +1,3 @@
1
+ module GitHeroes
2
+ VERSION = "0.0.1"
3
+ end
data/lib/git_heroes.rb ADDED
@@ -0,0 +1,5 @@
1
+ require 'git_heroes/version'
2
+
3
+ module GitHeroes
4
+ # Your code goes here...
5
+ end
metadata ADDED
@@ -0,0 +1,189 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: git-heroes
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Julien Letessier
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-11-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: redis-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: faraday-http-cache
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: redis
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
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.3'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ~>
95
+ - !ruby/object:Gem::Version
96
+ version: '1.3'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: pry
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - '>='
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: pry-nav
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - '>='
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - '>='
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ description: Leaderboard of your team's Github activity
140
+ email:
141
+ - julien.letessier@gmail.com
142
+ executables:
143
+ - git-heroes
144
+ extensions: []
145
+ extra_rdoc_files: []
146
+ files:
147
+ - .gitignore
148
+ - .ruby-version
149
+ - Gemfile
150
+ - LICENSE.txt
151
+ - README.md
152
+ - Rakefile
153
+ - bin/git-heroes
154
+ - githeroes.gemspec
155
+ - lib/git_heroes.rb
156
+ - lib/git_heroes/cache.rb
157
+ - lib/git_heroes/connection.rb
158
+ - lib/git_heroes/engine.rb
159
+ - lib/git_heroes/ext/faraday.rb
160
+ - lib/git_heroes/ext/octokit.rb
161
+ - lib/git_heroes/ext/time.rb
162
+ - lib/git_heroes/logger.rb
163
+ - lib/git_heroes/serializer.rb
164
+ - lib/git_heroes/version.rb
165
+ homepage: ''
166
+ licenses:
167
+ - MIT
168
+ metadata: {}
169
+ post_install_message:
170
+ rdoc_options: []
171
+ require_paths:
172
+ - lib
173
+ required_ruby_version: !ruby/object:Gem::Requirement
174
+ requirements:
175
+ - - '>='
176
+ - !ruby/object:Gem::Version
177
+ version: '0'
178
+ required_rubygems_version: !ruby/object:Gem::Requirement
179
+ requirements:
180
+ - - '>='
181
+ - !ruby/object:Gem::Version
182
+ version: '0'
183
+ requirements: []
184
+ rubyforge_project:
185
+ rubygems_version: 2.0.3
186
+ signing_key:
187
+ specification_version: 4
188
+ summary: Leaderboard of your team's Github activity
189
+ test_files: []