gitlab 4.5.0 → 5.1.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.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +0 -267
  3. data/LICENSE.txt +1 -1
  4. data/README.md +40 -30
  5. data/exe/gitlab +5 -1
  6. data/lib/gitlab/api.rb +7 -3
  7. data/lib/gitlab/cli.rb +13 -9
  8. data/lib/gitlab/cli_helpers.rb +49 -45
  9. data/lib/gitlab/client/access_requests.rb +10 -1
  10. data/lib/gitlab/client/application_settings.rb +172 -0
  11. data/lib/gitlab/client/avatar.rb +21 -0
  12. data/lib/gitlab/client/award_emojis.rb +5 -3
  13. data/lib/gitlab/client/boards.rb +62 -4
  14. data/lib/gitlab/client/branches.rb +47 -8
  15. data/lib/gitlab/client/broadcast_messages.rb +75 -0
  16. data/lib/gitlab/client/build_variables.rb +19 -12
  17. data/lib/gitlab/client/builds.rb +13 -11
  18. data/lib/gitlab/client/commits.rb +73 -21
  19. data/lib/gitlab/client/container_registry.rb +85 -0
  20. data/lib/gitlab/client/deployments.rb +3 -1
  21. data/lib/gitlab/client/environments.rb +5 -3
  22. data/lib/gitlab/client/epic_issues.rb +23 -0
  23. data/lib/gitlab/client/epics.rb +73 -0
  24. data/lib/gitlab/client/events.rb +6 -4
  25. data/lib/gitlab/client/features.rb +48 -0
  26. data/lib/gitlab/client/group_badges.rb +88 -0
  27. data/lib/gitlab/client/group_boards.rb +141 -0
  28. data/lib/gitlab/client/group_labels.rb +88 -0
  29. data/lib/gitlab/client/group_milestones.rb +7 -6
  30. data/lib/gitlab/client/groups.rb +326 -12
  31. data/lib/gitlab/client/issue_links.rb +48 -0
  32. data/lib/gitlab/client/issues.rb +47 -13
  33. data/lib/gitlab/client/jobs.rb +96 -8
  34. data/lib/gitlab/client/keys.rb +13 -0
  35. data/lib/gitlab/client/labels.rb +6 -4
  36. data/lib/gitlab/client/lint.rb +19 -0
  37. data/lib/gitlab/client/markdown.rb +23 -0
  38. data/lib/gitlab/client/merge_request_approvals.rb +164 -9
  39. data/lib/gitlab/client/merge_requests.rb +148 -11
  40. data/lib/gitlab/client/merge_trains.rb +55 -0
  41. data/lib/gitlab/client/milestones.rb +19 -5
  42. data/lib/gitlab/client/namespaces.rb +4 -2
  43. data/lib/gitlab/client/notes.rb +38 -9
  44. data/lib/gitlab/client/packages.rb +95 -0
  45. data/lib/gitlab/client/pipeline_schedules.rb +36 -10
  46. data/lib/gitlab/client/pipeline_triggers.rb +10 -8
  47. data/lib/gitlab/client/pipelines.rb +65 -3
  48. data/lib/gitlab/client/project_badges.rb +85 -0
  49. data/lib/gitlab/client/project_clusters.rb +83 -0
  50. data/lib/gitlab/client/project_exports.rb +54 -0
  51. data/lib/gitlab/client/project_release_links.rb +76 -0
  52. data/lib/gitlab/client/project_releases.rb +90 -0
  53. data/lib/gitlab/client/projects.rb +307 -26
  54. data/lib/gitlab/client/protected_tags.rb +59 -0
  55. data/lib/gitlab/client/remote_mirrors.rb +51 -0
  56. data/lib/gitlab/client/repositories.rb +77 -6
  57. data/lib/gitlab/client/repository_files.rb +21 -3
  58. data/lib/gitlab/client/repository_submodules.rb +27 -0
  59. data/lib/gitlab/client/resource_label_events.rb +82 -0
  60. data/lib/gitlab/client/resource_state_events.rb +57 -0
  61. data/lib/gitlab/client/runners.rb +170 -18
  62. data/lib/gitlab/client/search.rb +66 -0
  63. data/lib/gitlab/client/services.rb +4 -1
  64. data/lib/gitlab/client/sidekiq.rb +2 -0
  65. data/lib/gitlab/client/snippets.rb +5 -3
  66. data/lib/gitlab/client/system_hooks.rb +9 -7
  67. data/lib/gitlab/client/tags.rb +10 -9
  68. data/lib/gitlab/client/templates.rb +100 -0
  69. data/lib/gitlab/client/todos.rb +7 -5
  70. data/lib/gitlab/client/user_snippets.rb +114 -0
  71. data/lib/gitlab/client/users.rb +302 -31
  72. data/lib/gitlab/client/versions.rb +18 -0
  73. data/lib/gitlab/client/wikis.rb +79 -0
  74. data/lib/gitlab/client.rb +48 -9
  75. data/lib/gitlab/configuration.rb +9 -6
  76. data/lib/gitlab/error.rb +73 -3
  77. data/lib/gitlab/file_response.rb +4 -2
  78. data/lib/gitlab/headers/page_links.rb +37 -0
  79. data/lib/gitlab/headers/total.rb +29 -0
  80. data/lib/gitlab/help.rb +16 -16
  81. data/lib/gitlab/objectified_hash.rb +27 -10
  82. data/lib/gitlab/paginated_response.rb +43 -25
  83. data/lib/gitlab/request.rb +51 -37
  84. data/lib/gitlab/shell.rb +6 -4
  85. data/lib/gitlab/shell_history.rb +11 -13
  86. data/lib/gitlab/version.rb +3 -1
  87. data/lib/gitlab.rb +23 -9
  88. metadata +59 -45
  89. data/.gitignore +0 -22
  90. data/CONTRIBUTING.md +0 -195
  91. data/Gemfile +0 -4
  92. data/Rakefile +0 -17
  93. data/bin/console +0 -10
  94. data/bin/setup +0 -6
  95. data/gitlab.gemspec +0 -33
  96. data/lib/gitlab/page_links.rb +0 -33
@@ -1,17 +1,19 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'gitlab/cli_helpers'
2
4
  module Gitlab
3
5
  # Defines constants and methods related to configuration.
4
6
  module Configuration
5
7
  # An array of valid keys in the options hash when configuring a Gitlab::API.
6
- VALID_OPTIONS_KEYS = %i(endpoint private_token user_agent sudo httparty).freeze
8
+ VALID_OPTIONS_KEYS = %i[endpoint private_token user_agent sudo httparty pat_prefix].freeze
7
9
 
8
10
  # The user agent that will be sent to the API endpoint if none is set.
9
- DEFAULT_USER_AGENT = "Gitlab Ruby Gem #{Gitlab::VERSION}".freeze
11
+ DEFAULT_USER_AGENT = "Gitlab Ruby Gem #{Gitlab::VERSION}"
10
12
 
11
13
  # @private
12
14
  attr_accessor(*VALID_OPTIONS_KEYS)
13
15
  # @private
14
- alias_method :auth_token=, :private_token=
16
+ alias auth_token= private_token=
15
17
 
16
18
  # Sets all configuration options to their default values
17
19
  # when this module is extended.
@@ -33,8 +35,9 @@ module Gitlab
33
35
 
34
36
  # Resets all configuration options to the defaults.
35
37
  def reset
36
- self.endpoint = ENV['GITLAB_API_ENDPOINT']
38
+ self.endpoint = ENV['GITLAB_API_ENDPOINT'] || ENV['CI_API_V4_URL']
37
39
  self.private_token = ENV['GITLAB_API_PRIVATE_TOKEN'] || ENV['GITLAB_API_AUTH_TOKEN']
40
+ self.pat_prefix = nil
38
41
  self.httparty = get_httparty_config(ENV['GITLAB_API_HTTPARTY_OPTIONS'])
39
42
  self.sudo = nil
40
43
  self.user_agent = DEFAULT_USER_AGENT
@@ -44,11 +47,11 @@ module Gitlab
44
47
 
45
48
  # Allows HTTParty config to be specified in ENV using YAML hash.
46
49
  def get_httparty_config(options)
47
- return options if options.nil?
50
+ return if options.nil?
48
51
 
49
52
  httparty = Gitlab::CLI::Helpers.yaml_load(options)
50
-
51
53
  raise ArgumentError, 'HTTParty config should be a Hash.' unless httparty.is_a? Hash
54
+
52
55
  Gitlab::CLI::Helpers.symbolize_keys httparty
53
56
  end
54
57
  end
data/lib/gitlab/error.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Gitlab
2
4
  module Error
3
5
  # Custom error class for rescuing from all Gitlab errors.
@@ -11,7 +13,7 @@ module Gitlab
11
13
 
12
14
  # Custom error class for rescuing from HTTP response errors.
13
15
  class ResponseError < Error
14
- POSSIBLE_MESSAGE_KEYS = %i(message error_description error)
16
+ POSSIBLE_MESSAGE_KEYS = %i[message error_description error].freeze
15
17
 
16
18
  def initialize(response)
17
19
  @response = response
@@ -32,13 +34,24 @@ module Gitlab
32
34
  @response.parsed_response.message
33
35
  end
34
36
 
37
+ # Additional error context returned by some API endpoints
38
+ #
39
+ # @return [String]
40
+ def error_code
41
+ if @response.respond_to?(:error_code)
42
+ @response.error_code
43
+ else
44
+ ''
45
+ end
46
+ end
47
+
35
48
  private
36
49
 
37
50
  # Human friendly message.
38
51
  #
39
52
  # @return [String]
40
53
  def build_error_message
41
- parsed_response = @response.parsed_response
54
+ parsed_response = classified_response
42
55
  message = check_error_keys(parsed_response)
43
56
  "Server responded with code #{@response.code}, message: " \
44
57
  "#{handle_message(message)}. " \
@@ -52,12 +65,31 @@ module Gitlab
52
65
  key ? resp.send(key) : resp
53
66
  end
54
67
 
68
+ # Parse the body based on the classification of the body content type
69
+ #
70
+ # @return parsed response
71
+ def classified_response
72
+ if @response.respond_to?('headers')
73
+ @response.headers['content-type'] == 'text/plain' ? { message: @response.to_s } : @response.parsed_response
74
+ else
75
+ @response.parsed_response
76
+ end
77
+ rescue Gitlab::Error::Parsing
78
+ # Return stringified response when receiving a
79
+ # parsing error to avoid obfuscation of the
80
+ # api error.
81
+ #
82
+ # note: The Gitlab API does not always return valid
83
+ # JSON when there are errors.
84
+ @response.to_s
85
+ end
86
+
55
87
  # Handle error response message in case of nested hashes
56
88
  def handle_message(message)
57
89
  case message
58
90
  when Gitlab::ObjectifiedHash
59
91
  message.to_h.sort.map do |key, val|
60
- "'#{key}' #{(val.is_a?(Hash) ? val.sort.map { |k, v| "(#{k}: #{v.join(' ')})" } : val).join(' ')}"
92
+ "'#{key}' #{(val.is_a?(Hash) ? val.sort.map { |k, v| "(#{k}: #{v.join(' ')})" } : [val].flatten).join(' ')}"
61
93
  end.join(', ')
62
94
  when Array
63
95
  message.join(' ')
@@ -82,12 +114,18 @@ module Gitlab
82
114
  # Raised when API endpoint returns the HTTP status code 405.
83
115
  class MethodNotAllowed < ResponseError; end
84
116
 
117
+ # Raised when API endpoint returns the HTTP status code 406.
118
+ class NotAcceptable < ResponseError; end
119
+
85
120
  # Raised when API endpoint returns the HTTP status code 409.
86
121
  class Conflict < ResponseError; end
87
122
 
88
123
  # Raised when API endpoint returns the HTTP status code 422.
89
124
  class Unprocessable < ResponseError; end
90
125
 
126
+ # Raised when API endpoint returns the HTTP status code 429.
127
+ class TooManyRequests < ResponseError; end
128
+
91
129
  # Raised when API endpoint returns the HTTP status code 500.
92
130
  class InternalServerError < ResponseError; end
93
131
 
@@ -96,5 +134,37 @@ module Gitlab
96
134
 
97
135
  # Raised when API endpoint returns the HTTP status code 503.
98
136
  class ServiceUnavailable < ResponseError; end
137
+
138
+ # Raised when API endpoint returns the HTTP status code 522.
139
+ class ConnectionTimedOut < ResponseError; end
140
+
141
+ # HTTP status codes mapped to error classes.
142
+ STATUS_MAPPINGS = {
143
+ 400 => BadRequest,
144
+ 401 => Unauthorized,
145
+ 403 => Forbidden,
146
+ 404 => NotFound,
147
+ 405 => MethodNotAllowed,
148
+ 406 => NotAcceptable,
149
+ 409 => Conflict,
150
+ 422 => Unprocessable,
151
+ 429 => TooManyRequests,
152
+ 500 => InternalServerError,
153
+ 502 => BadGateway,
154
+ 503 => ServiceUnavailable,
155
+ 522 => ConnectionTimedOut
156
+ }.freeze
157
+
158
+ # Returns error class that should be raised for this response. Returns nil
159
+ # if the response status code is not 4xx or 5xx.
160
+ #
161
+ # @param [HTTParty::Response] response The response object.
162
+ # @return [Class<Error::ResponseError>, nil]
163
+ def self.klass(response)
164
+ error_klass = STATUS_MAPPINGS[response.code]
165
+ return error_klass if error_klass
166
+
167
+ ResponseError if response.server_error? || response.client_error?
168
+ end
99
169
  end
100
170
  end
@@ -1,7 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Gitlab
2
4
  # Wrapper class of file response.
3
5
  class FileResponse
4
- HEADER_CONTENT_DISPOSITION = 'Content-Disposition'.freeze
6
+ HEADER_CONTENT_DISPOSITION = 'Content-Disposition'
5
7
 
6
8
  attr_reader :filename
7
9
 
@@ -18,7 +20,7 @@ module Gitlab
18
20
  def to_hash
19
21
  { filename: @filename, data: @file }
20
22
  end
21
- alias_method :to_h, :to_hash
23
+ alias to_h to_hash
22
24
 
23
25
  # @return [String] Formatted string with the class name, object id and filename.
24
26
  def inspect
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gitlab
4
+ module Headers
5
+ # Parses link header.
6
+ #
7
+ # @private
8
+ class PageLinks
9
+ HEADER_LINK = 'Link'
10
+ DELIM_LINKS = ','
11
+ LINK_REGEX = /<([^>]+)>; rel="([^"]+)"/
12
+ METAS = %w[last next first prev].freeze
13
+
14
+ attr_accessor(*METAS)
15
+
16
+ def initialize(headers)
17
+ link_header = headers[HEADER_LINK]
18
+
19
+ extract_links(link_header) if link_header && link_header =~ /(next|first|last|prev)/
20
+ end
21
+
22
+ private
23
+
24
+ def extract_links(header)
25
+ header.split(DELIM_LINKS).each do |link|
26
+ LINK_REGEX.match(link.strip) do |match|
27
+ url = match[1]
28
+ meta = match[2]
29
+ next if !url || !meta || METAS.index(meta).nil?
30
+
31
+ send("#{meta}=", url)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gitlab
4
+ module Headers
5
+ # Parses total header.
6
+ #
7
+ # @private
8
+ class Total
9
+ HEADER_TOTAL = 'x-total'
10
+ TOTAL_REGEX = /^\d+$/
11
+
12
+ attr_accessor :total
13
+
14
+ def initialize(headers)
15
+ header_total = headers[HEADER_TOTAL]
16
+
17
+ extract_total(header_total) if header_total
18
+ end
19
+
20
+ private
21
+
22
+ def extract_total(header_total)
23
+ TOTAL_REGEX.match(header_total.strip) do |match|
24
+ @total = match[0]
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
data/lib/gitlab/help.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'gitlab'
2
4
  require 'gitlab/cli_helpers'
3
5
 
@@ -32,9 +34,7 @@ module Gitlab::Help
32
34
  # @return [String]
33
35
  def ri_cmd
34
36
  which_ri = `which ri`.chomp
35
- if which_ri.empty?
36
- raise "'ri' tool not found in $PATH. Please install it to use the help."
37
- end
37
+ raise "'ri' tool not found in $PATH. Please install it to use the help." if which_ri.empty?
38
38
 
39
39
  which_ri
40
40
  end
@@ -45,20 +45,19 @@ module Gitlab::Help
45
45
  #
46
46
  # @return [Hash<Array>]
47
47
  def help_map
48
- @help_map ||= begin
48
+ @help_map ||=
49
49
  actions.each_with_object({}) do |action, hsh|
50
- key = client.method(action).
51
- owner.to_s.gsub(/Gitlab::(?:Client::)?/, '')
50
+ key = client.method(action)
51
+ .owner.to_s.gsub(/Gitlab::(?:Client::)?/, '')
52
52
  hsh[key] ||= []
53
53
  hsh[key] << action.to_s
54
54
  end
55
- end
56
55
  end
57
56
 
58
57
  # Table with available commands.
59
58
  #
60
59
  # @return [Terminal::Table]
61
- def actions_table(topic=nil)
60
+ def actions_table(topic = nil)
62
61
  rows = topic ? help_map[topic] : help_map.keys
63
62
  table do |t|
64
63
  t.title = topic || 'Help Topics'
@@ -73,22 +72,23 @@ module Gitlab::Help
73
72
 
74
73
  # Returns full namespace of a command (e.g. Gitlab::Client::Branches.cmd)
75
74
  def namespace(cmd)
76
- method_owners.select { |method| method[:name] == cmd }.
77
- map { |method| method[:owner] + '.' + method[:name] }.
78
- shift
75
+ method_owners.select { |method| method[:name] == cmd }
76
+ .map { |method| "#{method[:owner]}.#{method[:name]}" }
77
+ .shift
79
78
  end
80
79
 
81
80
  # Massage output from 'ri'.
82
81
  def change_help_output!(cmd, output_str)
83
- output_str.gsub!(/#{cmd}\((.*?)\)/m, cmd + ' \1')
84
- output_str.gsub!(/\,[\s]*/, ' ')
82
+ output_str = +output_str
83
+ output_str.gsub!(/#{cmd}(\(.*?\))/m, "#{cmd}\\1")
84
+ output_str.gsub!(/,\s*/, ', ')
85
85
 
86
86
  # Ensure @option descriptions are on a single line
87
87
  output_str.gsub!(/\n\[/, " \[")
88
88
  output_str.gsub!(/\s(@)/, "\n@")
89
- output_str.gsub!(/(\])\n(\:)/, '\1 \2')
90
- output_str.gsub!(/(\:.*)(\n)(.*\.)/, '\1 \3')
91
- output_str.gsub!(/\{(.+)\}/, '"{\1}"')
89
+ output_str.gsub!(/(\])\n(:)/, '\\1 \\2')
90
+ output_str.gsub!(/(:.*)(\n)(.*\.)/, '\\1 \\3')
91
+ output_str.gsub!(/\{(.+)\}/, '"{\\1}"')
92
92
  end
93
93
  end
94
94
  end
@@ -1,34 +1,51 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Gitlab
2
4
  # Converts hashes to the objects.
3
5
  class ObjectifiedHash
4
6
  # Creates a new ObjectifiedHash object.
5
7
  def initialize(hash)
6
8
  @hash = hash
7
- @data = hash.inject({}) do |data, (key, value)|
8
- value = ObjectifiedHash.new(value) if value.is_a? Hash
9
+ @data = hash.each_with_object({}) do |(key, value), data|
10
+ value = self.class.new(value) if value.is_a? Hash
11
+ value = value.map { |v| v.is_a?(Hash) ? self.class.new(v) : v } if value.is_a? Array
9
12
  data[key.to_s] = value
10
- data
11
13
  end
12
14
  end
13
15
 
14
16
  # @return [Hash] The original hash.
15
17
  def to_hash
16
- @hash
18
+ hash
17
19
  end
18
- alias_method :to_h, :to_hash
20
+ alias to_h to_hash
19
21
 
20
22
  # @return [String] Formatted string with the class name, object id and original hash.
21
23
  def inspect
22
- "#<#{self.class}:#{object_id} {hash: #{@hash.inspect}}"
24
+ "#<#{self.class}:#{object_id} {hash: #{hash.inspect}}"
25
+ end
26
+
27
+ def [](key)
28
+ data[key]
23
29
  end
24
30
 
25
- # Delegate to ObjectifiedHash.
26
- def method_missing(key)
27
- @data.key?(key.to_s) ? @data[key.to_s] : nil
31
+ private
32
+
33
+ attr_reader :hash, :data
34
+
35
+ # Respond to messages for which `self.data` has a key
36
+ def method_missing(method_name, *args, &block)
37
+ if data.key?(method_name.to_s)
38
+ data[method_name.to_s]
39
+ elsif data.respond_to?(method_name)
40
+ warn 'WARNING: Please convert ObjectifiedHash object to hash before calling Hash methods on it.'
41
+ data.send(method_name, *args, &block)
42
+ else
43
+ super
44
+ end
28
45
  end
29
46
 
30
47
  def respond_to_missing?(method_name, include_private = false)
31
- @hash.keys.map(&:to_sym).include?(method_name.to_sym) || super
48
+ hash.keys.map(&:to_sym).include?(method_name.to_sym) || super
32
49
  end
33
50
  end
34
51
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Gitlab
2
4
  # Wrapper class of paginated response.
3
5
  class PaginatedResponse
@@ -28,7 +30,8 @@ module Gitlab
28
30
  end
29
31
 
30
32
  def parse_headers!(headers)
31
- @links = PageLinks.new headers
33
+ @links = Headers::PageLinks.new headers
34
+ @total = Headers::Total.new headers
32
35
  end
33
36
 
34
37
  def each_page
@@ -40,58 +43,73 @@ module Gitlab
40
43
  end
41
44
  end
42
45
 
43
- def auto_paginate
44
- response = block_given? ? nil : []
45
- each_page do |page|
46
- if block_given?
47
- page.each do |item|
48
- yield item
49
- end
50
- else
51
- response += page
52
- end
53
- end
54
- response
46
+ def lazy_paginate
47
+ to_enum(:each_page).lazy.flat_map(&:to_ary)
48
+ end
49
+
50
+ def auto_paginate(&block)
51
+ return lazy_paginate.to_a unless block
52
+
53
+ lazy_paginate.each(&block)
54
+ end
55
+
56
+ def paginate_with_limit(limit, &block)
57
+ return lazy_paginate.take(limit).to_a unless block
58
+
59
+ lazy_paginate.take(limit).each(&block)
55
60
  end
56
61
 
57
- def has_last_page?
62
+ def total
63
+ @total.total
64
+ end
65
+
66
+ def last_page?
58
67
  !(@links.nil? || @links.last.nil?)
59
68
  end
69
+ alias has_last_page? last_page?
60
70
 
61
71
  def last_page
62
72
  return nil if @client.nil? || !has_last_page?
63
- path = @links.last.sub(/#{@client.endpoint}/, '')
64
- @client.get(path)
73
+
74
+ @client.get(client_relative_path(@links.last))
65
75
  end
66
76
 
67
- def has_first_page?
77
+ def first_page?
68
78
  !(@links.nil? || @links.first.nil?)
69
79
  end
80
+ alias has_first_page? first_page?
70
81
 
71
82
  def first_page
72
83
  return nil if @client.nil? || !has_first_page?
73
- path = @links.first.sub(/#{@client.endpoint}/, '')
74
- @client.get(path)
84
+
85
+ @client.get(client_relative_path(@links.first))
75
86
  end
76
87
 
77
- def has_next_page?
88
+ def next_page?
78
89
  !(@links.nil? || @links.next.nil?)
79
90
  end
91
+ alias has_next_page? next_page?
80
92
 
81
93
  def next_page
82
94
  return nil if @client.nil? || !has_next_page?
83
- path = @links.next.sub(/#{@client.endpoint}/, '')
84
- @client.get(path)
95
+
96
+ @client.get(client_relative_path(@links.next))
85
97
  end
86
98
 
87
- def has_prev_page?
99
+ def prev_page?
88
100
  !(@links.nil? || @links.prev.nil?)
89
101
  end
102
+ alias has_prev_page? prev_page?
90
103
 
91
104
  def prev_page
92
105
  return nil if @client.nil? || !has_prev_page?
93
- path = @links.prev.sub(/#{@client.endpoint}/, '')
94
- @client.get(path)
106
+
107
+ @client.get(client_relative_path(@links.prev))
108
+ end
109
+
110
+ def client_relative_path(link)
111
+ client_endpoint_path = URI.parse(@client.endpoint).request_uri # api/v4
112
+ URI.parse(link).request_uri.sub(client_endpoint_path, '')
95
113
  end
96
114
  end
97
115
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'httparty'
2
4
  require 'json'
3
5
 
@@ -6,10 +8,11 @@ module Gitlab
6
8
  class Request
7
9
  include HTTParty
8
10
  format :json
11
+ maintain_method_across_redirects true
9
12
  headers 'Accept' => 'application/json', 'Content-Type' => 'application/x-www-form-urlencoded'
10
- parser proc { |body, _| parse(body) }
13
+ parser(proc { |body, _| parse(body) })
11
14
 
12
- attr_accessor :private_token, :endpoint
15
+ attr_accessor :private_token, :endpoint, :pat_prefix
13
16
 
14
17
  # Converts the response body to an ObjectifiedHash.
15
18
  def self.parse(body)
@@ -23,8 +26,6 @@ module Gitlab
23
26
  true
24
27
  elsif !body
25
28
  false
26
- elsif body.nil?
27
- false
28
29
  else
29
30
  raise Error::Parsing, "Couldn't parse a response body"
30
31
  end
@@ -32,35 +33,41 @@ module Gitlab
32
33
 
33
34
  # Decodes a JSON response into Ruby object.
34
35
  def self.decode(response)
35
- return response ? JSON.load(response) : {}
36
+ response ? JSON.load(response) : {}
36
37
  rescue JSON::ParserError
37
38
  raise Error::Parsing, 'The response is not a valid JSON'
38
39
  end
39
40
 
40
- %w(get post put delete).each do |method|
41
- define_method method do |path, options={}|
42
- httparty_config(options)
43
- authorization_header(options)
44
- validate self.class.send(method, @endpoint + path, options)
41
+ %w[get post put patch delete].each do |method|
42
+ define_method method do |path, options = {}|
43
+ params = options.dup
44
+
45
+ httparty_config(params)
46
+
47
+ unless params[:unauthenticated]
48
+ params[:headers] ||= {}
49
+ params[:headers].merge!(authorization_header)
50
+ end
51
+
52
+ retries_left = params[:ratelimit_retries] || 3
53
+ begin
54
+ response = self.class.send(method, endpoint + path, params)
55
+ validate response
56
+ rescue Gitlab::Error::TooManyRequests => e
57
+ retries_left -= 1
58
+ raise e if retries_left.zero?
59
+
60
+ wait_time = response.headers['Retry-After'] || 2
61
+ sleep(wait_time.to_i)
62
+ retry
63
+ end
45
64
  end
46
65
  end
47
66
 
48
67
  # Checks the response code for common errors.
49
68
  # Returns parsed response for successful requests.
50
69
  def validate(response)
51
- error_klass = case response.code
52
- when 400 then Error::BadRequest
53
- when 401 then Error::Unauthorized
54
- when 403 then Error::Forbidden
55
- when 404 then Error::NotFound
56
- when 405 then Error::MethodNotAllowed
57
- when 409 then Error::Conflict
58
- when 422 then Error::Unprocessable
59
- when 500 then Error::InternalServerError
60
- when 502 then Error::BadGateway
61
- when 503 then Error::ServiceUnavailable
62
- end
63
-
70
+ error_klass = Error.klass(response)
64
71
  raise error_klass, response if error_klass
65
72
 
66
73
  parsed = response.parsed_response
@@ -71,28 +78,35 @@ module Gitlab
71
78
 
72
79
  # Sets a base_uri and default_params for requests.
73
80
  # @raise [Error::MissingCredentials] if endpoint not set.
74
- def request_defaults(sudo=nil)
81
+ def request_defaults(sudo = nil)
82
+ raise Error::MissingCredentials, 'Please set an endpoint to API' unless endpoint
83
+
75
84
  self.class.default_params sudo: sudo
76
- raise Error::MissingCredentials, 'Please set an endpoint to API' unless @endpoint
77
85
  self.class.default_params.delete(:sudo) if sudo.nil?
78
86
  end
79
87
 
80
88
  private
81
89
 
82
- # Sets a PRIVATE-TOKEN or Authorization header for requests.
90
+ # Returns an Authorization header hash
83
91
  #
84
- # @param [Hash] options A customizable set of options.
85
- # @option options [Boolean] :unauthenticated true if the API call does not require user authentication.
86
92
  # @raise [Error::MissingCredentials] if private_token and auth_token are not set.
87
- def authorization_header(options)
88
- return if options[:unauthenticated]
89
- raise Error::MissingCredentials, 'Please provide a private_token or auth_token for user' unless @private_token
90
-
91
- options[:headers] = if @private_token.size < 21
92
- { 'PRIVATE-TOKEN' => @private_token }
93
- else
94
- { 'Authorization' => "Bearer #{@private_token}" }
95
- end
93
+ def authorization_header
94
+ raise Error::MissingCredentials, 'Please provide a private_token or auth_token for user' unless private_token
95
+
96
+ # The Personal Access Token prefix can be at most 20 characters, and the
97
+ # generated part is of length 20 characters. Personal Access Tokens, thus
98
+ # can have a maximum size of 40 characters. GitLab uses
99
+ # `Doorkeeper::OAuth::Helpers::UniqueToken.generate` for generating
100
+ # OAuth2 tokens, and specified `hex` as token generator method. Thus, the
101
+ # OAuth2 tokens are of length more than 64. If the token length is below
102
+ # that, it is probably a Personal Access Token or CI_JOB_TOKEN.
103
+ if private_token.size >= 64
104
+ { 'Authorization' => "Bearer #{private_token}" }
105
+ elsif private_token.start_with?(pat_prefix.to_s)
106
+ { 'PRIVATE-TOKEN' => private_token }
107
+ else
108
+ { 'JOB-TOKEN' => private_token }
109
+ end
96
110
  end
97
111
 
98
112
  # Set HTTParty configuration