git-heroes 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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: []