travis-gh 0.21.0

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
+ SHA256:
3
+ metadata.gz: 194688122c14fd85510b9a099c5857c7e6638a5525a6414e1303d4422606b050
4
+ data.tar.gz: 43c35e35441b76db6c188f348ef4a522b67b0db766401d1231256c48f9a8abb4
5
+ SHA512:
6
+ metadata.gz: c036a243cd1c7b6dad6f66e9b47d5a2e0bbfbb206ff52fe3b66e3131255e54a576d91afb47c977def57c3d15edac5fc1800779168fd3d177b4e503bbe614eae2
7
+ data.tar.gz: b58de545c86a16fc389d221049d757c23c0aa39d76c0b2e9faf645a276824c5a13fd25c2d3d175be58b9e607740adfd337342206238e85a2e161ef469e7ff8c0
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,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'gh'
4
+
5
+ module GH
6
+ # Public: This class caches responses.
7
+ class Cache < Wrapper
8
+ # Public: Get/set cache to use. Compatible with Rails/ActiveSupport cache.
9
+ attr_accessor :cache
10
+
11
+ # Internal: Simple in-memory cache basically implementing a copying GC.
12
+ class SimpleCache
13
+ # Internal: Initializes a new SimpleCache.
14
+ #
15
+ # size - Number of objects to hold in cache.
16
+ def initialize(size = 2048)
17
+ @old = {}
18
+ @new = {}
19
+ @size = size / 2
20
+ @mutex = Mutex.new
21
+ end
22
+
23
+ # Internal: Tries to fetch a value from the cache and if it doesn't exist, generates it from the
24
+ # block given.
25
+ def fetch(key)
26
+ if @new.size > @size
27
+ @mutex.synchronize do
28
+ if @new.size > @size
29
+ @old = @new
30
+ @new = {}
31
+ end
32
+ end
33
+ end
34
+ @new[key] ||= @old[key] || yield
35
+ end
36
+
37
+ # Internal: ...
38
+ def clear
39
+ @mutex.synchronize do
40
+ @old = {}
41
+ @new = {}
42
+ end
43
+ end
44
+ end
45
+
46
+ # Internal: Initializes a new Cache instance.
47
+ def setup(*)
48
+ # self.cache ||= Rails.cache if defined? Rails.cache and defined? RAILS_CACHE
49
+ # self.cache ||= ActiveSupport::Cache.lookup_store if defined? ActiveSupport::Cache.lookup_store
50
+ self.cache ||= SimpleCache.new
51
+ super
52
+ end
53
+
54
+ # Public: ...
55
+ def reset
56
+ super
57
+ clear_partial or clear_all
58
+ end
59
+
60
+ private
61
+
62
+ def fetch_resource(key)
63
+ cache.fetch(prefixed(key)) { super }
64
+ end
65
+
66
+ def clear_partial
67
+ return false unless cache.respond_to? :delete_matched
68
+
69
+ pattern = '^' << Regexp.escape(prefixed(''))
70
+ cache.delete_matched Regexp.new(pattern)
71
+ true
72
+ rescue NotImplementedError
73
+ false
74
+ end
75
+
76
+ def clear_all
77
+ cache.clear
78
+ end
79
+ end
80
+ end
data/lib/gh/case.rb ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'gh'
4
+
5
+ module GH
6
+ module Case
7
+ def respond_to(method)
8
+ proc { |o| o.respond_to? method }
9
+ end
10
+
11
+ private :respond_to
12
+ end
13
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GH
4
+ # Adds Client info so even unauthenticated requests can use a custom request limit
5
+ class CustomLimit < Wrapper
6
+ attr_accessor :client_id, :client_secret
7
+
8
+ def setup(backend, options)
9
+ @client_id = options[:client_id]
10
+ @client_secret = options[:client_secret]
11
+ super
12
+ end
13
+
14
+ def full_url(key)
15
+ return super unless client_id
16
+
17
+ url = super
18
+ params = url.query_values || {}
19
+
20
+ unless params.include? 'client_id'
21
+ params['client_id'] = client_id
22
+ params['client_secret'] = client_secret
23
+ end
24
+
25
+ url.query_values = params
26
+ url
27
+ end
28
+ end
29
+ end
data/lib/gh/error.rb ADDED
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'gh'
4
+
5
+ module GH
6
+ class Error < StandardError
7
+ attr_reader :info
8
+
9
+ def initialize(error = nil, payload = nil, info = {})
10
+ super(error)
11
+
12
+ info = info.merge(error.info) if error.respond_to?(:info) && error.info.is_a?(Hash)
13
+ error = error.error while error.respond_to? :error
14
+ @info = info.merge(error:, payload:)
15
+
16
+ return unless error
17
+
18
+ set_backtrace error.backtrace if error.respond_to? :backtrace
19
+ return unless error.respond_to?(:response) && error.response
20
+
21
+ case response = error.response
22
+ when Hash
23
+ @info[:response_status] = response[:status]
24
+ @info[:response_headers] = response[:headers]
25
+ @info[:response_body] = response[:body]
26
+ when Faraday::Response
27
+ @info[:response_status] = response.status
28
+ @info[:response_headers] = response.headers
29
+ @info[:response_body] = response.body
30
+ else
31
+ @info[:response] = response
32
+ end
33
+ end
34
+
35
+ def payload
36
+ info[:payload]
37
+ end
38
+
39
+ def error
40
+ info[:error]
41
+ end
42
+
43
+ def message
44
+ (['GH request failed'] + info.map { |k, v| entry(k, v) }).join("\n")
45
+ end
46
+
47
+ private
48
+
49
+ def entry(key, value)
50
+ value = "#{value.class}: #{value.message}" if value.is_a?(Exception)
51
+ value = value.inspect unless value.is_a?(String)
52
+ value.gsub!(/"Basic .+"|(client_(?:id|secret)=)[^&\s]+/, '\1[removed]')
53
+ "#{key}: ".ljust(20) + value
54
+ end
55
+ end
56
+
57
+ class TokenInvalid < Error
58
+ end
59
+
60
+ def self.Error(conditions)
61
+ Module.new do
62
+ define_singleton_method(:===) do |exception|
63
+ return false unless exception.is_a?(Error) && !exception.info.nil?
64
+
65
+ # rubocop:disable Style/CaseEquality
66
+ conditions.all? { |k, v| v === exception.info[k] }
67
+ # rubocop:enable Style/CaseEquality
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'gh'
4
+
5
+ module GH
6
+ # Public: This class caches responses.
7
+ class Instrumentation < Wrapper
8
+ # Public: Get/set instrumenter to use. Compatible with ActiveSupport::Notification and Travis::EventLogger.
9
+ attr_accessor :instrumenter
10
+
11
+ def setup(backend, options)
12
+ self.instrumenter ||= Travis::EventLogger.method(:notify) if defined? Travis::EventLogger
13
+ self.instrumenter ||= ActiveSupport::Notifications.method(:instrument) if defined? ActiveSupport::Notifications
14
+ super
15
+ end
16
+
17
+ def http(verb, url, *)
18
+ instrument(:http, verb:, url:) { super }
19
+ end
20
+
21
+ def load(data)
22
+ instrument(:load, data:) { super }
23
+ end
24
+
25
+ def [](key)
26
+ instrument(:access, key:) { super }
27
+ end
28
+
29
+ private
30
+
31
+ def instrument(type, payload = {})
32
+ return yield unless instrumenter
33
+
34
+ result = nil
35
+ instrumenter.call("#{type}.gh", payload.merge(gh: frontend)) { result = yield }
36
+ result
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'gh'
4
+
5
+ module GH
6
+ # Public: ...
7
+ class LazyLoader < Wrapper
8
+ wraps GH::Normalizer
9
+ double_dispatch
10
+
11
+ def modify_hash(hash, loaded = false) # rubocop:disable Style/OptionalBooleanParameter
12
+ hash = super(hash)
13
+ link = hash['_links']['self'] unless loaded || hash['_links'].nil?
14
+ setup_lazy_loading(hash, link['href']) if link
15
+ hash
16
+ rescue StandardError => e
17
+ raise Error.new(e, hash)
18
+ end
19
+
20
+ private
21
+
22
+ def lazy_load(hash, _key, link)
23
+ modify_hash(backend[link].data, true)
24
+ rescue StandardError => e
25
+ raise Error.new(e, hash)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GH
4
+ class LinkFollower < Wrapper
5
+ wraps GH::Normalizer
6
+ double_dispatch
7
+
8
+ def modify_hash(hash)
9
+ hash = super(hash)
10
+ setup_lazy_loading(hash) if hash['_links']
11
+ hash
12
+ rescue StandardError => e
13
+ raise Error.new(e, hash)
14
+ end
15
+
16
+ private
17
+
18
+ def lazy_load(hash, key)
19
+ link = hash['_links'][key]
20
+ { key => self[link['href']] } if link
21
+ rescue StandardError => e
22
+ raise Error.new(e, hash)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'gh'
4
+ require 'timeout'
5
+
6
+ module GH
7
+ # Public: ...
8
+ class MergeCommit < Wrapper
9
+ wraps GH::Normalizer
10
+ double_dispatch
11
+
12
+ def setup(backend, options)
13
+ @ssl = options[:ssl]
14
+ super
15
+ end
16
+
17
+ def modify_hash(hash)
18
+ setup_lazy_loading(super)
19
+ rescue StandardError => e
20
+ raise Error.new(e, hash)
21
+ end
22
+
23
+ private
24
+
25
+ def lazy_load(hash, key)
26
+ return unless key =~ (/^(merge|head|base)_commit$/) && hash.include?('mergeable')
27
+ return unless merge_commit?(hash)
28
+
29
+ fields = pull_request_refs(hash)
30
+ fields['base_commit'] ||= commit_for hash, hash['base']
31
+ fields['head_commit'] ||= commit_for hash, hash['head']
32
+ fields
33
+ rescue StandardError => e
34
+ raise Error.new(e, hash)
35
+ end
36
+
37
+ def commit_for(from, hash)
38
+ { 'sha' => hash['sha'], 'ref' => hash['ref'],
39
+ '_links' => { 'self' => { 'href' => git_url_for(from, hash['sha']) } } }
40
+ end
41
+
42
+ def git_url_for(hash, commitish)
43
+ hash['_links']['self']['href'].gsub(%r{/pulls/(\d+)$}, "/git/#{commitish}")
44
+ end
45
+
46
+ def pull_request_refs(hash)
47
+ link = git_url_for(hash, 'refs/pull/\1')
48
+ commits = self[link].map do |data|
49
+ ref = data['ref']
50
+ name = "#{ref.split('/').last}_commit"
51
+ object = data['object'].merge 'ref' => ref
52
+ [name, object]
53
+ end
54
+ commits.to_h
55
+ end
56
+
57
+ def merge_commit?(hash)
58
+ force_merge_commit(hash)
59
+ hash['mergeable']
60
+ end
61
+
62
+ def github_done_checking?(hash)
63
+ case hash['mergeable_state']
64
+ when 'checking' then false
65
+ when 'unknown' then hash['merged']
66
+ when 'clean', 'dirty', 'unstable', 'stable', 'blocked', 'behind', 'has_hooks', 'draft' then true
67
+ else raise "unknown mergeable_state #{hash['mergeable_state'].inspect} for #{url(hash)}"
68
+ end
69
+ end
70
+
71
+ def force_merge_commit(hash)
72
+ Timeout.timeout(180) do
73
+ update(hash) until github_done_checking? hash
74
+ end
75
+ rescue TimeoutError
76
+ status = hash['mergeable_state'].inspect
77
+ raise TimeoutError, "gave up waiting for github to check the merge status (current status is #{status})"
78
+ end
79
+
80
+ def update(hash)
81
+ hash.merge! backend[url(hash)]
82
+ sleep 0.5
83
+ end
84
+
85
+ def url(hash)
86
+ hash['_links']['self']['href']
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'gh'
4
+
5
+ module GH
6
+ # Public: ...
7
+ class NestedResources < Wrapper
8
+ wraps GH::Normalizer
9
+ double_dispatch
10
+
11
+ def modify_hash(hash, loaded = false) # rubocop:disable Style/OptionalBooleanParameter
12
+ hash = super(hash)
13
+ link = hash['_links']['self'] unless loaded || hash['_links'].nil?
14
+ set_links hash, Addressable::URI.parse(link['href']) if link
15
+ hash
16
+ end
17
+
18
+ def add(hash, link, name, path = name)
19
+ hash['_links'][name] ||= { 'href' => nested(link, path) }
20
+ end
21
+
22
+ def nested(link, path)
23
+ new_link = link.dup
24
+ if path.start_with? '/'
25
+ new_link.path = path
26
+ else
27
+ new_link.path += path
28
+ end
29
+ new_link
30
+ end
31
+
32
+ def set_links(hash, link)
33
+ case link.path
34
+ when '/gists'
35
+ add hash, link, 'public'
36
+ add hash, link, 'starred'
37
+ when %r{^/repos/[^/]+/[^/]+$}
38
+ add hash, link, 'commits', 'git/commits'
39
+ add hash, link, 'refs', 'git/refs'
40
+ add hash, link, 'tags', 'git/tags'
41
+ add hash, link, 'issues'
42
+ when %r{^/repos/[^/]+/[^/]+/issues/\d+$}
43
+ add hash, link, 'comments'
44
+ add hash, link, 'events'
45
+ when '/user'
46
+ add hash, link, 'gists', '/gists'
47
+ add hash, link, 'issues', '/issues'
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'gh'
4
+ require 'time'
5
+
6
+ module GH
7
+ # Public: A Wrapper class that deals with normalizing Github responses.
8
+ class Normalizer < Wrapper
9
+ def generate_response(key, response)
10
+ result = super
11
+ links(result)['self'] ||= { 'href' => frontend.full_url(key).to_s } if result.respond_to? :to_hash
12
+ result
13
+ end
14
+
15
+ private
16
+
17
+ double_dispatch
18
+
19
+ def links(hash)
20
+ hash = hash.data if hash.respond_to? :data
21
+ hash['_links'] ||= {}
22
+ end
23
+
24
+ def set_link(hash, type, href)
25
+ links(hash)[type] = { 'href' => href }
26
+ end
27
+
28
+ def modify_response(response)
29
+ response = response.dup
30
+ response.data = modify response.data
31
+ response
32
+ end
33
+
34
+ def modify_hash(hash)
35
+ corrected = {}
36
+ corrected.default_proc = hash.default_proc if hash.default_proc
37
+
38
+ hash.each_pair do |key, value|
39
+ key = modify_key(key, value)
40
+ next if modify_url(corrected, key, value)
41
+ next if modify_time(corrected, key, value)
42
+
43
+ corrected[key] = modify(value)
44
+ end
45
+
46
+ modify_user(corrected)
47
+ corrected
48
+ end
49
+
50
+ TIME_KEYS = %w[date timestamp committed_at created_at merged_at closed_at datetime time].freeze
51
+ TIME_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\S*$/
52
+
53
+ def modify_time(hash, key, value)
54
+ return unless TIME_KEYS.include?(key) || (TIME_PATTERN === value)
55
+
56
+ should_be = key == 'timestamp' ? 'date' : key
57
+ raise ArgumentError if (RUBY_VERSION < '1.9') && (value == '') # TODO: remove this line. duh.
58
+
59
+ time = begin
60
+ Time.at(value)
61
+ rescue StandardError
62
+ Time.parse(value.to_s)
63
+ end
64
+ hash[should_be] = time.utc.xmlschema if time
65
+ rescue ArgumentError, TypeError
66
+ hash[should_be] = value
67
+ end
68
+
69
+ def modify_user(hash)
70
+ hash['owner'] ||= hash.delete('user') if hash['created_at'] && hash['user']
71
+ hash['author'] ||= hash.delete('user') if hash['committed_at'] && hash['user']
72
+
73
+ hash['committer'] ||= hash['author'] if hash['author']
74
+ hash['author'] ||= hash['committer'] if hash['committer']
75
+
76
+ modify_user_fields hash['owner']
77
+ modify_user_fields hash['user']
78
+ end
79
+
80
+ def modify_user_fields(hash)
81
+ return unless hash.is_a?(Hash)
82
+
83
+ hash['login'] = hash.delete('name') if hash['name']
84
+ set_link hash, 'self', "users/#{hash['login']}" unless links(hash).include? 'self'
85
+ end
86
+
87
+ def modify_url(hash, key, value)
88
+ case key
89
+ when 'blog'
90
+ set_link(hash, key, value)
91
+ when 'url'
92
+ type = value.to_s.start_with?(api_host.to_s) ? 'self' : 'html'
93
+ set_link(hash, type, value)
94
+ when /^(.+)_url$/
95
+ set_link(hash, ::Regexp.last_match(1), value)
96
+ when 'config'
97
+ hash[key] = value
98
+ end
99
+ end
100
+
101
+ def modify_key(key, value = nil)
102
+ case key
103
+ when 'gravatar_url' then 'avatar_url'
104
+ when 'org' then 'organization'
105
+ when 'orgs' then 'organizations'
106
+ when 'username' then 'login'
107
+ when 'repo' then 'repository'
108
+ when 'repos' then modify_key('repositories', value)
109
+ when /^repos?_(.*)$/ then "repository_#{::Regexp.last_match(1)}"
110
+ when /^(.*)_repo$/ then "#{::Regexp.last_match(1)}_repository"
111
+ when /^(.*)_repos$/ then "#{::Regexp.last_match(1)}_repositories"
112
+ when 'commit', 'commit_id', 'id' then value.to_s =~ /^\w{40}$/ ? 'sha' : key
113
+ when 'comments' then value.is_a?(Numeric) ? 'comment_count' : key
114
+ when 'forks' then value.is_a?(Numeric) ? 'fork_count' : key
115
+ when 'repositories' then value.is_a?(Numeric) ? 'repository_count' : key
116
+ when /^(.*)s_count$/ then "#{::Regexp.last_match(1)}_count"
117
+ else key
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GH
4
+ class Pagination < Wrapper
5
+ class Paginated
6
+ include Enumerable
7
+
8
+ def initialize(page, url, gh)
9
+ @page = page
10
+ @next_url = url
11
+ @gh = gh
12
+ end
13
+
14
+ def each(&block)
15
+ return enum_for(:each) unless block
16
+
17
+ @page.each(&block)
18
+ next_page.each(&block)
19
+ end
20
+
21
+ def inspect
22
+ "[#{first.inspect}, ...]"
23
+ end
24
+
25
+ def [](value)
26
+ raise TypeError, "index has to be an Integer, got #{value.class}" unless value.is_a? Integer
27
+ return @page[value] if value < @page.size
28
+
29
+ next_page[value - @page.size]
30
+ end
31
+
32
+ def to_ary
33
+ to_a # replace with better implementation (use in_parallel)
34
+ end
35
+
36
+ def headers
37
+ @page.headers
38
+ end
39
+
40
+ private
41
+
42
+ def next_page
43
+ @next_page ||= @gh[@next_url]
44
+ end
45
+ end
46
+
47
+ wraps GH::Normalizer
48
+ double_dispatch
49
+
50
+ def fetch_resource(key)
51
+ url = frontend.full_url(key)
52
+ params = url.query_values || {}
53
+ params['per_page'] ||= 100
54
+ url.query_values = params
55
+ super url.request_uri
56
+ end
57
+
58
+ def modify_response(response)
59
+ return response unless response.respond_to?(:to_ary) && response.headers['link'] =~ (/<([^>]+)>;\s*rel="next"/)
60
+
61
+ Paginated.new(response, ::Regexp.last_match(1), self)
62
+ end
63
+ end
64
+ end