gitlab 3.3.0 → 3.4.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -1
  3. data/CHANGELOG.md +157 -0
  4. data/LICENSE.txt +1 -1
  5. data/README.md +16 -10
  6. data/lib/gitlab.rb +1 -1
  7. data/lib/gitlab/api.rb +5 -3
  8. data/lib/gitlab/cli.rb +21 -3
  9. data/lib/gitlab/cli_helpers.rb +46 -79
  10. data/lib/gitlab/client.rb +2 -1
  11. data/lib/gitlab/client/branches.rb +16 -1
  12. data/lib/gitlab/client/groups.rb +1 -0
  13. data/lib/gitlab/client/issues.rb +1 -0
  14. data/lib/gitlab/client/labels.rb +2 -0
  15. data/lib/gitlab/client/merge_requests.rb +12 -10
  16. data/lib/gitlab/client/milestones.rb +15 -0
  17. data/lib/gitlab/client/notes.rb +22 -6
  18. data/lib/gitlab/client/projects.rb +18 -0
  19. data/lib/gitlab/client/repositories.rb +4 -1
  20. data/lib/gitlab/client/repository_files.rb +72 -0
  21. data/lib/gitlab/client/snippets.rb +1 -11
  22. data/lib/gitlab/client/system_hooks.rb +1 -0
  23. data/lib/gitlab/client/users.rb +2 -0
  24. data/lib/gitlab/configuration.rb +3 -1
  25. data/lib/gitlab/error.rb +0 -3
  26. data/lib/gitlab/help.rb +77 -31
  27. data/lib/gitlab/objectified_hash.rb +6 -0
  28. data/lib/gitlab/request.rb +31 -15
  29. data/lib/gitlab/shell.rb +57 -58
  30. data/lib/gitlab/version.rb +1 -1
  31. data/spec/fixtures/branch_delete.json +3 -0
  32. data/spec/fixtures/merge_request_changes.json +1 -0
  33. data/spec/fixtures/milestone_issues.json +1 -0
  34. data/spec/fixtures/project_search.json +1 -0
  35. data/spec/fixtures/repository_file.json +1 -0
  36. data/spec/gitlab/cli_helpers_spec.rb +58 -0
  37. data/spec/gitlab/cli_spec.rb +1 -2
  38. data/spec/gitlab/client/branches_spec.rb +15 -0
  39. data/spec/gitlab/client/merge_requests_spec.rb +20 -12
  40. data/spec/gitlab/client/milestones_spec.rb +16 -0
  41. data/spec/gitlab/client/notes_spec.rb +17 -0
  42. data/spec/gitlab/client/projects_spec.rb +45 -8
  43. data/spec/gitlab/client/repository_files_spec.rb +45 -0
  44. data/spec/gitlab/help_spec.rb +44 -0
  45. data/spec/gitlab/objectified_hash_spec.rb +7 -0
  46. data/spec/gitlab/request_spec.rb +45 -6
  47. data/spec/gitlab/shell_spec.rb +80 -0
  48. data/spec/gitlab_spec.rb +12 -0
  49. metadata +23 -3
@@ -6,14 +6,15 @@ module Gitlab
6
6
  include Branches
7
7
  include Groups
8
8
  include Issues
9
+ include Labels
9
10
  include MergeRequests
10
11
  include Milestones
11
12
  include Notes
12
13
  include Projects
13
14
  include Repositories
15
+ include RepositoryFiles
14
16
  include Snippets
15
17
  include SystemHooks
16
18
  include Users
17
- include Labels
18
19
  end
19
20
  end
@@ -1,5 +1,6 @@
1
1
  class Gitlab::Client
2
2
  # Defines methods related to repositories.
3
+ # @see https://github.com/gitlabhq/gitlabhq/blob/master/doc/api/branches.md
3
4
  module Branches
4
5
  # Gets a list of project repositiory branches.
5
6
  #
@@ -28,7 +29,7 @@ class Gitlab::Client
28
29
  def branch(project, branch)
29
30
  get("/projects/#{project}/repository/branches/#{branch}")
30
31
  end
31
-
32
+
32
33
  alias_method :repo_branch, :branch
33
34
 
34
35
  # Protects a repository branch.
@@ -74,6 +75,20 @@ class Gitlab::Client
74
75
  end
75
76
  alias_method :repo_create_branch, :create_branch
76
77
 
78
+ # Deletes a repository branch. Requires Gitlab >= 6.8.x
79
+ #
80
+ # @example
81
+ # Gitlab.delete_branch(3, 'api')
82
+ # Gitlab.repo_delete_branch(5, 'master')
83
+ #
84
+ # @param [Integer] project The ID of a project.
85
+ # @param [String] branch The name of the branch to delete
86
+ # @return [Gitlab::ObjectifiedHash]
87
+ def delete_branch(project, branch)
88
+ delete("/projects/#{project}/repository/branches/#{branch}")
89
+ end
90
+ alias_method :repo_delete_branch, :delete_branch
91
+
77
92
  end
78
93
  end
79
94
 
@@ -1,5 +1,6 @@
1
1
  class Gitlab::Client
2
2
  # Defines methods related to groups.
3
+ # @see https://github.com/gitlabhq/gitlabhq/blob/master/doc/api/groups.md
3
4
  module Groups
4
5
  # Gets a list of groups.
5
6
  #
@@ -1,5 +1,6 @@
1
1
  class Gitlab::Client
2
2
  # Defines methods related to issues.
3
+ # @see https://github.com/gitlabhq/gitlabhq/blob/master/doc/api/issues.md
3
4
  module Issues
4
5
  # Gets a list of user's issues.
5
6
  # Will return a list of project's issues if project ID passed.
@@ -1,4 +1,6 @@
1
1
  class Gitlab::Client
2
+ # Defines methods related to labels.
3
+ # @see https://github.com/gitlabhq/gitlabhq/blob/master/doc/api/labels.md
2
4
  module Labels
3
5
  # Gets a list of project's labels.
4
6
  #
@@ -1,5 +1,6 @@
1
1
  class Gitlab::Client
2
2
  # Defines methods related to merge requests.
3
+ # @see https://github.com/gitlabhq/gitlabhq/blob/master/doc/api/merge_requests.md
3
4
  module MergeRequests
4
5
  # Gets a list of project merge requests.
5
6
  #
@@ -42,10 +43,9 @@ class Gitlab::Client
42
43
  # @option options [String] :source_branch (required) The source branch name.
43
44
  # @option options [String] :target_branch (required) The target branch name.
44
45
  # @option options [Integer] :assignee_id (optional) The ID of a user to assign merge request.
46
+ # @option options [Integer] :target_project_id (optional) The target project ID.
45
47
  # @return [Gitlab::ObjectifiedHash] Information about created merge request.
46
48
  def create_merge_request(project, title, options={})
47
- check_attributes!(options, [:source_branch, :target_branch])
48
-
49
49
  body = {:title => title}.merge(options)
50
50
  post("/projects/#{project}/merge_requests", :body => body)
51
51
  end
@@ -112,14 +112,16 @@ class Gitlab::Client
112
112
  get("/projects/#{project}/merge_request/#{id}/comments", :query => options)
113
113
  end
114
114
 
115
- private
116
-
117
- def check_attributes!(options, attrs)
118
- attrs.each do |attr|
119
- unless options.has_key?(attr) || options.has_key?(attr.to_s)
120
- raise Gitlab::Error::MissingAttributes.new("Missing '#{attr}' parameter")
121
- end
122
- end
115
+ # Gets the changes of a merge request.
116
+ #
117
+ # @example
118
+ # Gitlab.merge_request_changes(5, 1)
119
+ #
120
+ # @param [Integer] project The ID of a project.
121
+ # @param [Integer] id The ID of a merge request.
122
+ # @return [Gitlab::ObjectifiedHash] The merge request's changes.
123
+ def merge_request_changes(project, id)
124
+ get("/projects/#{project}/merge_request/#{id}/changes")
123
125
  end
124
126
  end
125
127
  end
@@ -1,5 +1,6 @@
1
1
  class Gitlab::Client
2
2
  # Defines methods related to milestones.
3
+ # @see https://github.com/gitlabhq/gitlabhq/blob/master/doc/api/milestones.md
3
4
  module Milestones
4
5
  # Gets a list of project's milestones.
5
6
  #
@@ -27,6 +28,20 @@ class Gitlab::Client
27
28
  get("/projects/#{project}/milestones/#{id}")
28
29
  end
29
30
 
31
+ # Gets the issues of a given milestone.
32
+ #
33
+ # @example
34
+ # Gitlab.milestone_issues(5, 2)
35
+ #
36
+ # @param [Integer, String] project The ID of a project.
37
+ # @param [Integer, String] milestone The ID of a milestone.
38
+ # @option options [Integer] :page The page number.
39
+ # @option options [Integer] :per_page The number of results per page.
40
+ # @return [Array<Gitlab::ObjectifiedHash>]
41
+ def milestone_issues(project, milestone, options={})
42
+ get("/projects/#{project}/milestones/#{milestone}/issues", :query => options)
43
+ end
44
+
30
45
  # Creates a new milestone.
31
46
  #
32
47
  # @param [Integer] project The ID of a project.
@@ -1,5 +1,6 @@
1
1
  class Gitlab::Client
2
2
  # Defines methods related to notes.
3
+ # @see https://github.com/gitlabhq/gitlabhq/blob/master/doc/api/notes.md
3
4
  module Notes
4
5
  # Gets a list of projects notes.
5
6
  #
@@ -7,9 +8,11 @@ class Gitlab::Client
7
8
  # Gitlab.notes(5)
8
9
  #
9
10
  # @param [Integer] project The ID of a project.
11
+ # @option options [Integer] :page The page number.
12
+ # @option options [Integer] :per_page The number of results per page.
10
13
  # @return [Array<Gitlab::ObjectifiedHash>]
11
- def notes(project)
12
- get("/projects/#{project}/notes")
14
+ def notes(project, options={})
15
+ get("/projects/#{project}/notes", :query => options)
13
16
  end
14
17
 
15
18
  # Gets a list of notes for a issue.
@@ -19,9 +22,11 @@ class Gitlab::Client
19
22
  #
20
23
  # @param [Integer] project The ID of a project.
21
24
  # @param [Integer] issue The ID of an issue.
25
+ # @option options [Integer] :page The page number.
26
+ # @option options [Integer] :per_page The number of results per page.
22
27
  # @return [Array<Gitlab::ObjectifiedHash>]
23
- def issue_notes(project, issue)
24
- get("/projects/#{project}/issues/#{issue}/notes")
28
+ def issue_notes(project, issue, options={})
29
+ get("/projects/#{project}/issues/#{issue}/notes", :query => options)
25
30
  end
26
31
 
27
32
  # Gets a list of notes for a snippet.
@@ -31,9 +36,11 @@ class Gitlab::Client
31
36
  #
32
37
  # @param [Integer] project The ID of a project.
33
38
  # @param [Integer] snippet The ID of a snippet.
39
+ # @option options [Integer] :page The page number.
40
+ # @option options [Integer] :per_page The number of results per page.
34
41
  # @return [Array<Gitlab::ObjectifiedHash>]
35
- def snippet_notes(project, snippet)
36
- get("/projects/#{project}/snippets/#{snippet}/notes")
42
+ def snippet_notes(project, snippet, options={})
43
+ get("/projects/#{project}/snippets/#{snippet}/notes", :query => options)
37
44
  end
38
45
 
39
46
  # Gets a single wall note.
@@ -102,5 +109,14 @@ class Gitlab::Client
102
109
  def create_snippet_note(project, snippet, body)
103
110
  post("/projects/#{project}/snippets/#{snippet}/notes", :body => {:body => body})
104
111
  end
112
+
113
+ # Creates a new note for a single merge request.
114
+ #
115
+ # @param [Integer] project The ID of a project.
116
+ # @param [Integer] merge_request The ID of a merge request.
117
+ # @param [String] body The content of a note.
118
+ def create_merge_request_note(project, merge_request, body)
119
+ post("/projects/#{project}/merge_requests/#{merge_request}/notes", :body => {:body => body})
120
+ end
105
121
  end
106
122
  end
@@ -1,5 +1,6 @@
1
1
  class Gitlab::Client
2
2
  # Defines methods related to projects.
3
+ # @see https://github.com/gitlabhq/gitlabhq/blob/master/doc/api/projects.md
3
4
  module Projects
4
5
  # Gets a list of projects owned by the authenticated user.
5
6
  #
@@ -19,6 +20,23 @@ class Gitlab::Client
19
20
  end
20
21
  end
21
22
 
23
+ # Search for projects by name
24
+ #
25
+ # @example
26
+ # Gitlab.project_search('gitlab')
27
+ # Gitlab.project_search('gitlab', :order_by => 'last_activity_at')
28
+ #
29
+ # @param [Hash] options A customizable set of options.
30
+ # @option options [String] :per_page Number of projects to return per page
31
+ # @option options [String] :page The page to retrieve
32
+ # @option options [String] :order_by Return requests ordered by id, name, created_at or last_activity_at fields
33
+ # @option options [String] :sort Return requests sorted in asc or desc order
34
+ # @return [Array<Gitlab::ObjectifiedHash>]
35
+ def project_search(query, options={})
36
+ get("/projects/search/#{query}", :query => options)
37
+ end
38
+
39
+
22
40
  # Gets information about a project.
23
41
  #
24
42
  # @example
@@ -1,5 +1,6 @@
1
1
  class Gitlab::Client
2
2
  # Defines methods related to repositories.
3
+ # @see https://github.com/gitlabhq/gitlabhq/blob/master/doc/api/repositories.md
3
4
  module Repositories
4
5
  # Gets a list of project repository tags.
5
6
  #
@@ -19,7 +20,8 @@ class Gitlab::Client
19
20
  # Creates a new project repository tag.
20
21
  #
21
22
  # @example
22
- # Gitlab.create_tag(42,'new_tag','master')
23
+ # Gitlab.create_tag(42, 'new_tag', 'master')
24
+ # Gitlab.create_tag(42, 'v1.0', 'master', 'Release 1.0')
23
25
  #
24
26
  # @param [Integer] project The ID of a project.
25
27
  # @param [String] tag_name The name of the new tag.
@@ -87,6 +89,7 @@ class Gitlab::Client
87
89
  # @param [String] ref The name of a repository branch or tag or if not given the default branch.
88
90
  # @return [String]
89
91
  def file_contents(project, filepath, ref = 'master')
92
+ ref = URI.encode(ref, /\W/)
90
93
  get "/projects/#{project}/repository/blobs/#{ref}?filepath=#{filepath}",
91
94
  format: nil,
92
95
  headers: { Accept: 'text/plain' },
@@ -0,0 +1,72 @@
1
+ require 'base64'
2
+
3
+ class Gitlab::Client
4
+ # Defines methods related to repository files.
5
+ # @see https://github.com/gitlabhq/gitlabhq/blob/master/doc/api/repository_files.md
6
+ module RepositoryFiles
7
+ # Creates a new repository file.
8
+ #
9
+ # @example
10
+ # Gitlab.create_file(42, "path", "branch", "content", "commit message")
11
+ #
12
+ # @param [Integer] project The ID of a project.
13
+ # @param [String] full path to new file.
14
+ # @param [String] the name of the branch.
15
+ # @param [String] file content.
16
+ # @param [String] commit message.
17
+ # @return [Gitlab::ObjectifiedHash]
18
+ def create_file(project, path, branch, content, commit_message)
19
+ post("/projects/#{project}/repository/files", body: {
20
+ file_path: path,
21
+ branch_name: branch,
22
+ commit_message: commit_message,
23
+ }.merge(encoded_content_attributes(content)))
24
+ end
25
+
26
+ # Edits an existing repository file.
27
+ #
28
+ # @example
29
+ # Gitlab.edit_file(42, "path", "branch", "content", "commit message")
30
+ #
31
+ # @param [Integer] project The ID of a project.
32
+ # @param [String] full path to new file.
33
+ # @param [String] the name of the branch.
34
+ # @param [String] file content.
35
+ # @param [String] commit message.
36
+ # @return [Gitlab::ObjectifiedHash]
37
+ def edit_file(project, path, branch, content, commit_message)
38
+ put("/projects/#{project}/repository/files", body: {
39
+ file_path: path,
40
+ branch_name: branch,
41
+ commit_message: commit_message,
42
+ }.merge(encoded_content_attributes(content)))
43
+ end
44
+
45
+ # Removes an existing repository file.
46
+ #
47
+ # @example
48
+ # Gitlab.remove_file(42, "path", "branch", "commit message")
49
+ #
50
+ # @param [Integer] project The ID of a project.
51
+ # @param [String] full path to new file.
52
+ # @param [String] the name of the branch.
53
+ # @param [String] commit message.
54
+ # @return [Gitlab::ObjectifiedHash]
55
+ def remove_file(project, path, branch, commit_message)
56
+ delete("/projects/#{project}/repository/files", body: {
57
+ file_path: path,
58
+ branch_name: branch,
59
+ commit_message: commit_message,
60
+ })
61
+ end
62
+
63
+ private
64
+
65
+ def encoded_content_attributes(content)
66
+ {
67
+ encoding: 'base64',
68
+ content: Base64.encode64(content),
69
+ }
70
+ end
71
+ end
72
+ end
@@ -1,5 +1,6 @@
1
1
  class Gitlab::Client
2
2
  # Defines methods related to snippets.
3
+ # @see https://github.com/gitlabhq/gitlabhq/blob/master/doc/api/project_snippets.md
3
4
  module Snippets
4
5
  # Gets a list of project's snippets.
5
6
  #
@@ -40,7 +41,6 @@ class Gitlab::Client
40
41
  # @option options [String] :lifetime (optional) The expiration date of a snippet.
41
42
  # @return [Gitlab::ObjectifiedHash] Information about created snippet.
42
43
  def create_snippet(project, options={})
43
- check_attributes!(options, [:title, :file_name, :code])
44
44
  post("/projects/#{project}/snippets", :body => options)
45
45
  end
46
46
 
@@ -72,15 +72,5 @@ class Gitlab::Client
72
72
  def delete_snippet(project, id)
73
73
  delete("/projects/#{project}/snippets/#{id}")
74
74
  end
75
-
76
- private
77
-
78
- def check_attributes!(options, attrs)
79
- attrs.each do |attr|
80
- unless options.has_key?(attr) || options.has_key?(attr.to_s)
81
- raise Gitlab::Error::MissingAttributes.new("Missing '#{attr}' parameter")
82
- end
83
- end
84
- end
85
75
  end
86
76
  end
@@ -1,5 +1,6 @@
1
1
  class Gitlab::Client
2
2
  # Defines methods related to system hooks.
3
+ # @see https://github.com/gitlabhq/gitlabhq/blob/master/doc/api/system_hooks.md
3
4
  module SystemHooks
4
5
  # Gets a list of system hooks.
5
6
  #
@@ -1,5 +1,7 @@
1
1
  class Gitlab::Client
2
2
  # Defines methods related to users.
3
+ # @see https://github.com/gitlabhq/gitlabhq/blob/master/doc/api/users.md
4
+ # @see https://github.com/gitlabhq/gitlabhq/blob/master/doc/api/session.md
3
5
  module Users
4
6
  # Gets a list of users.
5
7
  #
@@ -9,6 +9,8 @@ module Gitlab
9
9
 
10
10
  # @private
11
11
  attr_accessor(*VALID_OPTIONS_KEYS)
12
+ # @private
13
+ alias_method :auth_token=, :private_token=
12
14
 
13
15
  # Sets all configuration options to their default values
14
16
  # when this module is extended.
@@ -31,7 +33,7 @@ module Gitlab
31
33
  # Resets all configuration options to the defaults.
32
34
  def reset
33
35
  self.endpoint = ENV['GITLAB_API_ENDPOINT']
34
- self.private_token = ENV['GITLAB_API_PRIVATE_TOKEN']
36
+ self.private_token = ENV['GITLAB_API_PRIVATE_TOKEN'] || ENV['GITLAB_API_AUTH_TOKEN']
35
37
  self.sudo = nil
36
38
  self.user_agent = DEFAULT_USER_AGENT
37
39
  end
@@ -3,9 +3,6 @@ module Gitlab
3
3
  # Custom error class for rescuing from all Gitlab errors.
4
4
  class Error < StandardError; end
5
5
 
6
- # Raise when attributes are missing.
7
- class MissingAttributes < Error; end
8
-
9
6
  # Raised when API endpoint credentials not configured.
10
7
  class MissingCredentials < Error; end
11
8
 
@@ -4,41 +4,87 @@ require 'gitlab/cli_helpers'
4
4
  module Gitlab::Help
5
5
  extend Gitlab::CLI::Helpers
6
6
 
7
- def self.get_help(methods,cmd=nil)
8
- help = ''
9
-
10
- if cmd.nil? || cmd == 'help'
11
- help = actions_table
12
- else
13
- ri_cmd = `which ri`.chomp
14
-
15
- if $? == 0
16
- namespace = methods.select {|m| m[:name] === cmd }.map {|m| m[:owner]+'.'+m[:name] }.shift
17
-
18
- if namespace
19
- begin
20
- ri_output = `#{ri_cmd} -T #{namespace} 2>&1`.chomp
21
-
22
- if $? == 0
23
- ri_output.gsub!(/#{cmd}\((.*?)\)/m, cmd+' \1')
24
- ri_output.gsub!(/Gitlab\./, 'gitlab> ')
25
- ri_output.gsub!(/Gitlab\..+$/, '')
26
- ri_output.gsub!(/\,[\s]*/, ' ')
27
- help = ri_output
28
- else
29
- help = "Ri docs not found for #{namespace}, please install the docs to use 'help'"
30
- end
31
- rescue => e
32
- puts e.message
33
- end
7
+ class << self
8
+
9
+ # Returns the (modified) help from the 'ri' command or returns an error.
10
+ #
11
+ # @return [String]
12
+ def get_help(cmd)
13
+ cmd_namespace = namespace cmd
14
+
15
+ if cmd_namespace
16
+ ri_output = `#{ri_cmd} -T #{cmd_namespace} 2>&1`.chomp
17
+
18
+ if $? == 0
19
+ change_help_output! cmd, ri_output
20
+ yield ri_output if block_given?
21
+
22
+ ri_output
34
23
  else
35
- help = "Unknown command: #{cmd}"
24
+ "Ri docs not found for #{cmd}, please install the docs to use 'help'."
36
25
  end
37
26
  else
38
- help = "'ri' tool not found in your PATH, please install it to use the help."
27
+ "Unknown command: #{cmd}."
28
+ end
29
+ end
30
+
31
+ # Finds the location of 'ri' on a system.
32
+ #
33
+ # @return [String]
34
+ def ri_cmd
35
+ which_ri = `which ri`.chomp
36
+ if which_ri.empty?
37
+ raise "'ri' tool not found in $PATH. Please install it to use the help."
39
38
  end
39
+
40
+ which_ri
40
41
  end
41
42
 
42
- puts help
43
- end
43
+ # A hash map that contains help topics (Branches, Groups, etc.)
44
+ # and a list of commands that are defined under a topic (create_branch,
45
+ # branches, protect_branch, etc.).
46
+ #
47
+ # @return [Hash<Array>]
48
+ def help_map
49
+ @help_map ||= begin
50
+ actions.each_with_object({}) do |action, hsh|
51
+ key = client.method(action).
52
+ owner.to_s.gsub(/Gitlab::(?:Client::)?/, '')
53
+ hsh[key] ||= []
54
+ hsh[key] << action.to_s
55
+ end
56
+ end
57
+ end
58
+
59
+ # Table with available commands.
60
+ #
61
+ # @return [Terminal::Table]
62
+ def actions_table(topic = nil)
63
+ rows = topic ? help_map[topic] : help_map.keys
64
+ table do |t|
65
+ t.title = topic || "Help Topics"
66
+
67
+ # add_row expects an array and we have strings hence the map.
68
+ rows.sort.map { |r| [r] }.each_with_index do |row, index|
69
+ t.add_row row
70
+ t.add_separator unless rows.size - 1 == index
71
+ end
72
+ end
73
+ end
74
+
75
+ # Returns full namespace of a command (e.g. Gitlab::Client::Branches.cmd)
76
+ def namespace(cmd)
77
+ method_owners.select { |method| method[:name] === cmd }.
78
+ map { |method| method[:owner] + '.' + method[:name] }.
79
+ shift
80
+ end
81
+
82
+ # Massage output from 'ri'.
83
+ def change_help_output!(cmd, output_str)
84
+ output_str.gsub!(/#{cmd}\((.*?)\)/m, cmd+' \1')
85
+ output_str.gsub!(/\,[\s]*/, ' ')
86
+ end
87
+
88
+ end # class << self
44
89
  end
90
+