gh-akerl 0.15.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +22 -0
- data/lib/gh/cache.rb +65 -0
- data/lib/gh/case.rb +8 -0
- data/lib/gh/custom_limit.rb +27 -0
- data/lib/gh/error.rb +64 -0
- data/lib/gh/instrumentation.rb +36 -0
- data/lib/gh/lazy_loader.rb +26 -0
- data/lib/gh/link_follower.rb +23 -0
- data/lib/gh/merge_commit.rb +86 -0
- data/lib/gh/nested_resources.rb +49 -0
- data/lib/gh/normalizer.rb +111 -0
- data/lib/gh/pagination.rb +57 -0
- data/lib/gh/parallel.rb +59 -0
- data/lib/gh/remote.rb +156 -0
- data/lib/gh/response.rb +93 -0
- data/lib/gh/response_wrapper.rb +11 -0
- data/lib/gh/stack.rb +61 -0
- data/lib/gh/token_check.rb +31 -0
- data/lib/gh/version.rb +4 -0
- data/lib/gh/wrapper.rb +231 -0
- data/lib/gh.rb +74 -0
- metadata +177 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 66c0fe8b3672ab41b6013da9e62d1a04ae026d90402bcaf2843ed08ed3ad1807
|
4
|
+
data.tar.gz: b7514eaaff845c0683a109d4a496221b2f7ca1d2c3125794ef66909334f45779
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: bdac11fcf07dedc0e5ab0212a23d11928ce86bbba6fb639cd0d44d0f31d47fcf5b9d15c55228c5e73c8c9a23fb8bfd00613518a55ba2fdbefde40983cd596c8b
|
7
|
+
data.tar.gz: bae91bdbfbc140dbeee491fe040765d6fca71fe9d7739f4f2b03fbd45ff001a8c7416d6aa458dfc5d4947f13ce2af9d740dcc21270ce66c97029360817c83e1d
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Konstantin Haase
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person
|
4
|
+
obtaining a copy of this software and associated documentation
|
5
|
+
files (the "Software"), to deal in the Software without
|
6
|
+
restriction, including without limitation the rights to use,
|
7
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
copies of the Software, and to permit persons to whom the
|
9
|
+
Software is furnished to do so, subject to the following
|
10
|
+
conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be
|
13
|
+
included in all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
17
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
19
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
20
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
21
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/lib/gh/cache.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'gh'
|
2
|
+
require 'thread'
|
3
|
+
|
4
|
+
module GH
|
5
|
+
# Public: This class caches responses.
|
6
|
+
class Cache < Wrapper
|
7
|
+
# Public: Get/set cache to use. Compatible with Rails/ActiveSupport cache.
|
8
|
+
attr_accessor :cache
|
9
|
+
|
10
|
+
# Internal: Simple in-memory cache basically implementing a copying GC.
|
11
|
+
class SimpleCache
|
12
|
+
# Internal: Initializes a new SimpleCache.
|
13
|
+
#
|
14
|
+
# size - Number of objects to hold in cache.
|
15
|
+
def initialize(size = 2048)
|
16
|
+
@old, @new, @size, @mutex = {}, {}, size/2, Mutex.new
|
17
|
+
end
|
18
|
+
|
19
|
+
# Internal: Tries to fetch a value from the cache and if it doesn't exist, generates it from the
|
20
|
+
# block given.
|
21
|
+
def fetch(key)
|
22
|
+
@mutex.synchronize { @old, @new = @new, {} if @new.size > @size } if @new.size > @size
|
23
|
+
@new[key] ||= @old[key] || yield
|
24
|
+
end
|
25
|
+
|
26
|
+
# Internal: ...
|
27
|
+
def clear
|
28
|
+
@mutex.synchronize { @old, @new = {}, {} }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Internal: Initializes a new Cache instance.
|
33
|
+
def setup(*)
|
34
|
+
#self.cache ||= Rails.cache if defined? Rails.cache and defined? RAILS_CACHE
|
35
|
+
#self.cache ||= ActiveSupport::Cache.lookup_store if defined? ActiveSupport::Cache.lookup_store
|
36
|
+
self.cache ||= SimpleCache.new
|
37
|
+
super
|
38
|
+
end
|
39
|
+
|
40
|
+
# Public: ...
|
41
|
+
def reset
|
42
|
+
super
|
43
|
+
clear_partial or clear_all
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def fetch_resource(key)
|
49
|
+
cache.fetch(prefixed(key)) { super }
|
50
|
+
end
|
51
|
+
|
52
|
+
def clear_partial
|
53
|
+
return false unless cache.respond_to? :delete_matched
|
54
|
+
pattern = "^" << Regexp.escape(prefixed(""))
|
55
|
+
cache.delete_matched Regexp.new(pattern)
|
56
|
+
true
|
57
|
+
rescue NotImplementedError
|
58
|
+
false
|
59
|
+
end
|
60
|
+
|
61
|
+
def clear_all
|
62
|
+
cache.clear
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
data/lib/gh/case.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
module GH
|
2
|
+
# Adds Client info so even unauthenticated requests can use a custom request limit
|
3
|
+
class CustomLimit < Wrapper
|
4
|
+
attr_accessor :client_id, :client_secret
|
5
|
+
|
6
|
+
def setup(backend, options)
|
7
|
+
@client_id = options[:client_id]
|
8
|
+
@client_secret = options[:client_secret]
|
9
|
+
super
|
10
|
+
end
|
11
|
+
|
12
|
+
def full_url(key)
|
13
|
+
return super unless client_id
|
14
|
+
|
15
|
+
url = super
|
16
|
+
params = url.query_values || {}
|
17
|
+
|
18
|
+
unless params.include? 'client_id'
|
19
|
+
params['client_id'] = client_id
|
20
|
+
params['client_secret'] = client_secret
|
21
|
+
end
|
22
|
+
|
23
|
+
url.query_values = params
|
24
|
+
url
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/gh/error.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'gh'
|
2
|
+
|
3
|
+
module GH
|
4
|
+
class Error < StandardError
|
5
|
+
attr_reader :info
|
6
|
+
|
7
|
+
def initialize(error = nil, payload = nil, info = {})
|
8
|
+
info = info.merge(error.info) if error.respond_to? :info and Hash === error.info
|
9
|
+
error = error.error while error.respond_to? :error
|
10
|
+
@info = info.merge(:error => error, :payload => payload)
|
11
|
+
|
12
|
+
if error
|
13
|
+
set_backtrace error.backtrace if error.respond_to? :backtrace
|
14
|
+
if error.respond_to? :response and error.response
|
15
|
+
case response = error.response
|
16
|
+
when Hash
|
17
|
+
@info[:response_status] = response[:status]
|
18
|
+
@info[:response_headers] = response[:headers]
|
19
|
+
@info[:response_body] = response[:body]
|
20
|
+
when Faraday::Response
|
21
|
+
@info[:response_status] = response.status
|
22
|
+
@info[:response_headers] = response.headers
|
23
|
+
@info[:response_body] = response.body
|
24
|
+
else
|
25
|
+
@info[:response] = response
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def payload
|
32
|
+
info[:payload]
|
33
|
+
end
|
34
|
+
|
35
|
+
def error
|
36
|
+
info[:error]
|
37
|
+
end
|
38
|
+
|
39
|
+
def message
|
40
|
+
"GH request failed\n" + info.map { |k,v| entry(k,v) }.join("\n")
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def entry(key, value)
|
46
|
+
value = "#{value.class}: #{value.message}" if Exception === value
|
47
|
+
value = value.inspect unless String === value
|
48
|
+
value.gsub!(/"Basic .+"|(client_(?:id|secret)=)[^&\s]+/, '\1[removed]')
|
49
|
+
(key.to_s + ": ").ljust(20) + value
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class TokenInvalid < Error
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.Error(conditions)
|
57
|
+
Module.new do
|
58
|
+
define_singleton_method(:===) do |exception|
|
59
|
+
return false unless Error === exception and not exception.info.nil?
|
60
|
+
conditions.all? { |k,v| v === exception.info[k]}
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'gh'
|
2
|
+
|
3
|
+
module GH
|
4
|
+
# Public: This class caches responses.
|
5
|
+
class Instrumentation < Wrapper
|
6
|
+
# Public: Get/set instrumenter to use. Compatible with ActiveSupport::Notification and Travis::EventLogger.
|
7
|
+
attr_accessor :instrumenter
|
8
|
+
|
9
|
+
def setup(backend, options)
|
10
|
+
self.instrumenter ||= Travis::EventLogger.method(:notify) if defined? Travis::EventLogger
|
11
|
+
self.instrumenter ||= ActiveSupport::Notifications.method(:instrument) if defined? ActiveSupport::Notifications
|
12
|
+
super
|
13
|
+
end
|
14
|
+
|
15
|
+
def http(verb, url, *)
|
16
|
+
instrument(:http, :verb => verb, :url => url) { super }
|
17
|
+
end
|
18
|
+
|
19
|
+
def load(data)
|
20
|
+
instrument(:load, :data => data) { super }
|
21
|
+
end
|
22
|
+
|
23
|
+
def [](key)
|
24
|
+
instrument(:access, :key => key) { super }
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def instrument(type, payload = {})
|
30
|
+
return yield unless instrumenter
|
31
|
+
result = nil
|
32
|
+
instrumenter.call("#{type}.gh", payload.merge(:gh => frontend)) { result = yield }
|
33
|
+
return result
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'gh'
|
2
|
+
|
3
|
+
module GH
|
4
|
+
# Public: ...
|
5
|
+
class LazyLoader < Wrapper
|
6
|
+
wraps GH::Normalizer
|
7
|
+
double_dispatch
|
8
|
+
|
9
|
+
def modify_hash(hash, loaded = false)
|
10
|
+
hash = super(hash)
|
11
|
+
link = hash['_links']['self'] unless loaded or hash['_links'].nil?
|
12
|
+
setup_lazy_loading(hash, link['href']) if link
|
13
|
+
hash
|
14
|
+
rescue Exception => error
|
15
|
+
raise Error.new(error, hash)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def lazy_load(hash, key, link)
|
21
|
+
modify_hash(backend[link].data, true)
|
22
|
+
rescue Exception => error
|
23
|
+
raise Error.new(error, hash)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module GH
|
2
|
+
class LinkFollower < Wrapper
|
3
|
+
wraps GH::Normalizer
|
4
|
+
double_dispatch
|
5
|
+
|
6
|
+
def modify_hash(hash)
|
7
|
+
hash = super
|
8
|
+
setup_lazy_loading(hash) if hash['_links']
|
9
|
+
hash
|
10
|
+
rescue Exception => error
|
11
|
+
raise Error.new(error, hash)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def lazy_load(hash, key)
|
17
|
+
link = hash['_links'][key]
|
18
|
+
{ key => self[link['href']] } if link
|
19
|
+
rescue Exception => error
|
20
|
+
raise Error.new(error, hash)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'gh'
|
2
|
+
require 'timeout'
|
3
|
+
|
4
|
+
module GH
|
5
|
+
# Public: ...
|
6
|
+
class MergeCommit < Wrapper
|
7
|
+
wraps GH::Normalizer
|
8
|
+
double_dispatch
|
9
|
+
|
10
|
+
def setup(backend, options)
|
11
|
+
@ssl = options[:ssl]
|
12
|
+
super
|
13
|
+
end
|
14
|
+
|
15
|
+
def modify_hash(hash)
|
16
|
+
setup_lazy_loading(super)
|
17
|
+
rescue Exception => error
|
18
|
+
raise Error.new(error, hash)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def lazy_load(hash, key)
|
24
|
+
return unless key =~ /^(merge|head|base)_commit$/ and hash.include? 'mergeable'
|
25
|
+
return unless has_merge_commit?(hash)
|
26
|
+
fields = pull_request_refs(hash)
|
27
|
+
fields['base_commit'] ||= commit_for hash, hash['base']
|
28
|
+
fields['head_commit'] ||= commit_for hash, hash['head']
|
29
|
+
fields
|
30
|
+
rescue Exception => error
|
31
|
+
raise Error.new(error, hash)
|
32
|
+
end
|
33
|
+
|
34
|
+
def commit_for(from, hash)
|
35
|
+
{ 'sha' => hash['sha'], 'ref' => hash['ref'],
|
36
|
+
'_links' => { 'self' => { 'href' => git_url_for(from, hash['sha']) } } }
|
37
|
+
end
|
38
|
+
|
39
|
+
def git_url_for(hash, commitish)
|
40
|
+
hash['_links']['self']['href'].gsub(%r{/pulls/(\d+)$}, "/git/#{commitish}")
|
41
|
+
end
|
42
|
+
|
43
|
+
def pull_request_refs(hash)
|
44
|
+
link = git_url_for(hash, 'refs/pull/\1')
|
45
|
+
commits = self[link].map do |data|
|
46
|
+
ref = data['ref']
|
47
|
+
name = ref.split('/').last + "_commit"
|
48
|
+
object = data['object'].merge 'ref' => ref
|
49
|
+
[name, object]
|
50
|
+
end
|
51
|
+
Hash[commits]
|
52
|
+
end
|
53
|
+
|
54
|
+
def has_merge_commit?(hash)
|
55
|
+
force_merge_commit(hash)
|
56
|
+
hash['mergeable']
|
57
|
+
end
|
58
|
+
|
59
|
+
def github_done_checking?(hash)
|
60
|
+
case hash['mergeable_state']
|
61
|
+
when 'checking' then false
|
62
|
+
when 'unknown' then hash['merged']
|
63
|
+
when 'clean', 'dirty', 'unstable', 'stable', 'blocked', 'behind' then true
|
64
|
+
else fail "unknown mergeable_state #{hash['mergeable_state'].inspect} for #{url(hash)}"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def force_merge_commit(hash)
|
69
|
+
Timeout.timeout(180) do
|
70
|
+
update(hash) until github_done_checking? hash
|
71
|
+
end
|
72
|
+
rescue TimeoutError
|
73
|
+
status = hash['mergeable_state'].inspect
|
74
|
+
raise TimeoutError, "gave up waiting for github to check the merge status (current status is #{status})"
|
75
|
+
end
|
76
|
+
|
77
|
+
def update(hash)
|
78
|
+
hash.merge! backend[url(hash)]
|
79
|
+
sleep 0.5
|
80
|
+
end
|
81
|
+
|
82
|
+
def url(hash)
|
83
|
+
hash['_links']['self']['href']
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'gh'
|
2
|
+
|
3
|
+
module GH
|
4
|
+
# Public: ...
|
5
|
+
class NestedResources < Wrapper
|
6
|
+
wraps GH::Normalizer
|
7
|
+
double_dispatch
|
8
|
+
|
9
|
+
def modify_hash(hash, loaded = false)
|
10
|
+
hash = super(hash)
|
11
|
+
link = hash['_links']['self'] unless loaded or hash['_links'].nil?
|
12
|
+
set_links hash, Addressable::URI.parse(link['href']) if link
|
13
|
+
hash
|
14
|
+
end
|
15
|
+
|
16
|
+
def add(hash, link, name, path = name)
|
17
|
+
hash["_links"][name] ||= { "href" => nested(link, path) }
|
18
|
+
end
|
19
|
+
|
20
|
+
def nested(link, path)
|
21
|
+
new_link = link.dup
|
22
|
+
if path.start_with? '/'
|
23
|
+
new_link.path = path
|
24
|
+
else
|
25
|
+
new_link.path += path
|
26
|
+
end
|
27
|
+
new_link
|
28
|
+
end
|
29
|
+
|
30
|
+
def set_links(hash, link)
|
31
|
+
case link.path
|
32
|
+
when '/gists'
|
33
|
+
add hash, link, 'public'
|
34
|
+
add hash, link, 'starred'
|
35
|
+
when %r{^/repos/[^/]+/[^/]+$}
|
36
|
+
add hash, link, 'commits', 'git/commits'
|
37
|
+
add hash, link, 'refs', 'git/refs'
|
38
|
+
add hash, link, 'tags', 'git/tags'
|
39
|
+
add hash, link, 'issues'
|
40
|
+
when %r{^/repos/[^/]+/[^/]+/issues/\d+$}
|
41
|
+
add hash, link, 'comments'
|
42
|
+
add hash, link, 'events'
|
43
|
+
when '/user'
|
44
|
+
add hash, link, 'gists', '/gists'
|
45
|
+
add hash, link, 'issues', '/issues'
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'gh'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
module GH
|
5
|
+
# Public: A Wrapper class that deals with normalizing Github responses.
|
6
|
+
class Normalizer < Wrapper
|
7
|
+
def generate_response(key, response)
|
8
|
+
result = super
|
9
|
+
links(result)['self'] ||= { 'href' => frontend.full_url(key).to_s } if result.respond_to? :to_hash
|
10
|
+
result
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
double_dispatch
|
16
|
+
|
17
|
+
def links(hash)
|
18
|
+
hash = hash.data if hash.respond_to? :data
|
19
|
+
hash["_links"] ||= {}
|
20
|
+
end
|
21
|
+
|
22
|
+
def set_link(hash, type, href)
|
23
|
+
links(hash)[type] = {"href" => href}
|
24
|
+
end
|
25
|
+
|
26
|
+
def modify_response(response)
|
27
|
+
response = response.dup
|
28
|
+
response.data = modify response.data
|
29
|
+
response
|
30
|
+
end
|
31
|
+
|
32
|
+
def modify_hash(hash)
|
33
|
+
corrected = {}
|
34
|
+
corrected.default_proc = hash.default_proc if hash.default_proc
|
35
|
+
|
36
|
+
hash.each_pair do |key, value|
|
37
|
+
key = modify_key(key, value)
|
38
|
+
next if modify_url(corrected, key, value)
|
39
|
+
next if modify_time(corrected, key, value)
|
40
|
+
corrected[key] = modify(value)
|
41
|
+
end
|
42
|
+
|
43
|
+
modify_user(corrected)
|
44
|
+
corrected
|
45
|
+
end
|
46
|
+
|
47
|
+
TIME_KEYS = %w[date timestamp committed_at created_at merged_at closed_at datetime time]
|
48
|
+
TIME_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\S*$/
|
49
|
+
|
50
|
+
def modify_time(hash, key, value)
|
51
|
+
return unless TIME_KEYS.include? key or TIME_PATTERN === value
|
52
|
+
should_be = key == 'timestamp' ? 'date' : key
|
53
|
+
raise ArgumentError if RUBY_VERSION < "1.9" and value == "" # TODO: remove this line. duh.
|
54
|
+
time = Time.at(value) rescue Time.parse(value.to_s)
|
55
|
+
hash[should_be] = time.utc.xmlschema if time
|
56
|
+
rescue ArgumentError, TypeError
|
57
|
+
hash[should_be] = value
|
58
|
+
end
|
59
|
+
|
60
|
+
def modify_user(hash)
|
61
|
+
hash['owner'] ||= hash.delete('user') if hash['created_at'] and hash['user']
|
62
|
+
hash['author'] ||= hash.delete('user') if hash['committed_at'] and hash['user']
|
63
|
+
|
64
|
+
hash['committer'] ||= hash['author'] if hash['author']
|
65
|
+
hash['author'] ||= hash['committer'] if hash['committer']
|
66
|
+
|
67
|
+
modify_user_fields hash['owner']
|
68
|
+
modify_user_fields hash['user']
|
69
|
+
end
|
70
|
+
|
71
|
+
def modify_user_fields(hash)
|
72
|
+
return unless Hash === hash
|
73
|
+
hash['login'] = hash.delete('name') if hash['name']
|
74
|
+
set_link hash, 'self', "users/#{hash['login']}" unless links(hash).include? 'self'
|
75
|
+
end
|
76
|
+
|
77
|
+
def modify_url(hash, key, value)
|
78
|
+
case key
|
79
|
+
when "blog"
|
80
|
+
set_link(hash, key, value)
|
81
|
+
when "url"
|
82
|
+
type = value.to_s.start_with?(api_host.to_s) ? "self" : "html"
|
83
|
+
set_link(hash, type, value)
|
84
|
+
when /^(.+)_url$/
|
85
|
+
set_link(hash, $1, value)
|
86
|
+
when "config"
|
87
|
+
hash[key] = value
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def modify_key(key, value = nil)
|
92
|
+
case key
|
93
|
+
when 'gravatar_url' then 'avatar_url'
|
94
|
+
when 'org' then 'organization'
|
95
|
+
when 'orgs' then 'organizations'
|
96
|
+
when 'username' then 'login'
|
97
|
+
when 'repo' then 'repository'
|
98
|
+
when 'repos' then modify_key('repositories', value)
|
99
|
+
when /^repos?_(.*)$/ then "repository_#{$1}"
|
100
|
+
when /^(.*)_repo$/ then "#{$1}_repository"
|
101
|
+
when /^(.*)_repos$/ then "#{$1}_repositories"
|
102
|
+
when 'commit', 'commit_id', 'id' then value =~ /^\w{40}$/ ? 'sha' : key
|
103
|
+
when 'comments' then Numeric === value ? 'comment_count' : key
|
104
|
+
when 'forks' then Numeric === value ? 'fork_count' : key
|
105
|
+
when 'repositories' then Numeric === value ? 'repository_count' : key
|
106
|
+
when /^(.*)s_count$/ then "#{$1}_count"
|
107
|
+
else key
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module GH
|
2
|
+
class Pagination < Wrapper
|
3
|
+
class Paginated
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
def initialize(page, url, gh)
|
7
|
+
@page, @next_url, @gh = page, url, gh
|
8
|
+
end
|
9
|
+
|
10
|
+
def each(&block)
|
11
|
+
return enum_for(:each) unless block
|
12
|
+
@page.each(&block)
|
13
|
+
next_page.each(&block)
|
14
|
+
end
|
15
|
+
|
16
|
+
def inspect
|
17
|
+
"[#{first.inspect}, ...]"
|
18
|
+
end
|
19
|
+
|
20
|
+
def [](value)
|
21
|
+
raise TypeError, "index has to be an Integer, got #{value.class}" unless value.is_a? Integer
|
22
|
+
return @page[value] if value < @page.size
|
23
|
+
next_page[value - @page.size]
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_ary
|
27
|
+
to_a # replace with better implementation (use in_parallel)
|
28
|
+
end
|
29
|
+
|
30
|
+
def headers
|
31
|
+
@page.headers
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def next_page
|
37
|
+
@next_page ||= @gh[@next_url]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
wraps GH::Normalizer
|
42
|
+
double_dispatch
|
43
|
+
|
44
|
+
def fetch_resource(key)
|
45
|
+
url = frontend.full_url(key)
|
46
|
+
params = url.query_values || {}
|
47
|
+
params['per_page'] ||= 100
|
48
|
+
url.query_values = params
|
49
|
+
super url.request_uri
|
50
|
+
end
|
51
|
+
|
52
|
+
def modify_response(response)
|
53
|
+
return response unless response.respond_to? :to_ary and response.headers['link'] =~ /<([^>]+)>;\s*rel=\"next\"/
|
54
|
+
Paginated.new(response, $1, self)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/lib/gh/parallel.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'gh'
|
2
|
+
require 'thread'
|
3
|
+
require 'backports/basic_object' unless defined? BasicObject
|
4
|
+
|
5
|
+
module GH
|
6
|
+
# Public: ...
|
7
|
+
class Parallel < Wrapper
|
8
|
+
attr_accessor :parallelize
|
9
|
+
|
10
|
+
class Dummy < BasicObject
|
11
|
+
attr_accessor :__delegate__
|
12
|
+
def method_missing(*args)
|
13
|
+
::Kernel.raise ::RuntimeError, "response not yet loaded" if __delegate__.nil?
|
14
|
+
__delegate__.__send__(*args)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def setup(*)
|
19
|
+
@parallelize = true if @parallelize.nil?
|
20
|
+
@in_parallel = false
|
21
|
+
@mutex = Mutex.new
|
22
|
+
@queue = []
|
23
|
+
super
|
24
|
+
end
|
25
|
+
|
26
|
+
def generate_response(key, response)
|
27
|
+
return super unless in_parallel?
|
28
|
+
dummy = Dummy.new
|
29
|
+
@mutex.synchronize { @queue << [dummy, key, response] }
|
30
|
+
dummy
|
31
|
+
end
|
32
|
+
|
33
|
+
def in_parallel
|
34
|
+
return yield if in_parallel? or not @parallelize
|
35
|
+
was, @in_parallel = @in_parallel, true
|
36
|
+
result = nil
|
37
|
+
connection.in_parallel { result = yield }
|
38
|
+
@mutex.synchronize do
|
39
|
+
@queue.each { |dummy, key, response| dummy.__delegate__ = backend.generate_response(key, response) }
|
40
|
+
@queue.clear
|
41
|
+
end
|
42
|
+
result
|
43
|
+
ensure
|
44
|
+
@in_parallel = was unless was.nil?
|
45
|
+
end
|
46
|
+
|
47
|
+
def in_parallel?
|
48
|
+
@in_parallel
|
49
|
+
end
|
50
|
+
|
51
|
+
def connection
|
52
|
+
@connection ||= begin
|
53
|
+
layer = backend
|
54
|
+
layer = layer.backend until layer.respond_to? :connection
|
55
|
+
layer.connection
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|