travis-gh 0.21.0

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
+ 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