From e3c12332ba2434f3e6ddae6d5cc77913623ef8cf Mon Sep 17 00:00:00 2001 From: ernstvn Date: Tue, 12 Apr 2016 14:55:13 -0700 Subject: [PATCH 001/153] remove need for schedules --- .../corporate_contributor_license_agreement.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/doc/legal/corporate_contributor_license_agreement.md b/doc/legal/corporate_contributor_license_agreement.md index 7b94506c29785b..001829b4f18f14 100644 --- a/doc/legal/corporate_contributor_license_agreement.md +++ b/doc/legal/corporate_contributor_license_agreement.md @@ -6,13 +6,17 @@ You accept and agree to the following terms and conditions for Your present and "You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with GitLab B.V.. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - "Contribution" shall mean the code, documentation or other original works of authorship expressly identified in Schedule B, as well as any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to GitLab B.V. for inclusion in, or documentation of, any of the products owned or managed by GitLab B.V. (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to GitLab B.V. or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, GitLab B.V. for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution." + "Contribution" shall mean the code, documentation or other original works of authorship, as well as any original work of authorship, including any modifications or additions to an existing work, that is submitted by You to GitLab B.V. for inclusion in, or documentation of, any of the products owned or managed by GitLab B.V. (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to GitLab B.V. or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, GitLab B.V. for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution." -2. Grant of Copyright License. Subject to the terms and conditions of this Agreement, You hereby grant to GitLab B.V. and to recipients of software distributed by GitLab B.V. a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works. +2. Grant of Copyright License. -3. Grant of Patent License. Subject to the terms and conditions of this Agreement, You hereby grant to GitLab B.V. and to recipients of software distributed by GitLab B.V. a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed. +Subject to the terms and conditions of this Agreement, You hereby grant to GitLab B.V. and to recipients of software distributed by GitLab B.V. a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works. -4. You represent that You are legally entitled to grant the above license. You represent further that each employee of the Corporation designated on Schedule A below (or in a subsequent written modification to that Schedule) is authorized to submit Contributions on behalf of the Corporation. +3. Grant of Patent License. + +Subject to the terms and conditions of this Agreement, You hereby grant to GitLab B.V. and to recipients of software distributed by GitLab B.V. a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed. + +4. You represent that You are legally entitled to grant the above license. You represent further that each employee of the Corporation is authorized to submit Contributions on behalf of the Corporation, but excluding employees that are designated in writing by You as "Not authorized to submit Contributions on behalf of [name of corporation here]." 5. You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others). @@ -20,6 +24,6 @@ You accept and agree to the following terms and conditions for Your present and 7. Should You wish to submit work that is not Your original creation, You may submit it to GitLab B.V. separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [named here]". -8. It is your responsibility to notify GitLab B.V. when any change is required to the list of designated employees authorized to submit Contributions on behalf of the Corporation, or to the Corporation's Point of Contact with GitLab B.V.. +8. It is your responsibility to notify GitLab B.V. when any change is required to the designation of employees not authorized to submit Contributions on behalf of the Corporation, or to the Corporation's Point of Contact with GitLab B.V.. This text is licensed under the [Creative Commons Attribution 3.0 License](https://creativecommons.org/licenses/by/3.0/) and the original source is the Google Open Source Programs Office. -- GitLab From 416be9bf8513ce6b94911ff3e1bf10167b9be644 Mon Sep 17 00:00:00 2001 From: Ernst van Nierop Date: Wed, 13 Apr 2016 21:44:37 +0000 Subject: [PATCH 002/153] edit --- doc/legal/corporate_contributor_license_agreement.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/legal/corporate_contributor_license_agreement.md b/doc/legal/corporate_contributor_license_agreement.md index 001829b4f18f14..edd6c59138fd89 100644 --- a/doc/legal/corporate_contributor_license_agreement.md +++ b/doc/legal/corporate_contributor_license_agreement.md @@ -6,7 +6,7 @@ You accept and agree to the following terms and conditions for Your present and "You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with GitLab B.V.. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - "Contribution" shall mean the code, documentation or other original works of authorship, as well as any original work of authorship, including any modifications or additions to an existing work, that is submitted by You to GitLab B.V. for inclusion in, or documentation of, any of the products owned or managed by GitLab B.V. (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to GitLab B.V. or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, GitLab B.V. for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution." + "Contribution" shall mean the code, documentation or other original works of authorship, including any modifications or additions to an existing work, that is submitted by You to GitLab B.V. for inclusion in, or documentation of, any of the products owned or managed by GitLab B.V. (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to GitLab B.V. or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, GitLab B.V. for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution." 2. Grant of Copyright License. -- GitLab From ae2d4d2de03f2ff25a79266128ef75d5e301e298 Mon Sep 17 00:00:00 2001 From: Eric K Idema Date: Thu, 12 May 2016 10:48:34 -0400 Subject: [PATCH 003/153] Fix escaped angle bracket's in integration documentation. There are several escaped angle brackets in our integration docs. While these render fine within GitLab, they are broken rendered on doc.gitlab.com because pandoc does not escape them correctly. --- doc/integration/bitbucket.md | 2 +- doc/integration/github.md | 2 +- doc/integration/gitlab.md | 2 +- doc/integration/twitter.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md index 63432b044323a1..2eb6266ebe7952 100644 --- a/doc/integration/bitbucket.md +++ b/doc/integration/bitbucket.md @@ -14,7 +14,7 @@ Bitbucket will generate an application ID and secret key for you to use. 1. Select "Add consumer". 1. Provide the required details. - - Name: This can be anything. Consider something like "\'s GitLab" or "\'s GitLab" or something else descriptive. + - Name: This can be anything. Consider something like `'s GitLab` or `'s GitLab` or something else descriptive. - Application description: Fill this in if you wish. - URL: The URL to your GitLab installation. 'https://gitlab.company.com' 1. Select "Save". diff --git a/doc/integration/github.md b/doc/integration/github.md index e7497e475c9a33..8e865cc656474b 100644 --- a/doc/integration/github.md +++ b/doc/integration/github.md @@ -16,7 +16,7 @@ GitHub will generate an application ID and secret key for you to use. 1. Select "Register new application". 1. Provide the required details. - - Application name: This can be anything. Consider something like "\'s GitLab" or "\'s GitLab" or something else descriptive. + - Application name: This can be anything. Consider something like `'s GitLab` or `'s GitLab` or something else descriptive. - Homepage URL: The URL to your GitLab installation. 'https://gitlab.company.com' - Application description: Fill this in if you wish. - Default authorization callback URL is '${YOUR_DOMAIN}/import/github/callback' diff --git a/doc/integration/gitlab.md b/doc/integration/gitlab.md index b215cc7c609a7c..6d8f3912ede101 100644 --- a/doc/integration/gitlab.md +++ b/doc/integration/gitlab.md @@ -14,7 +14,7 @@ GitLab.com will generate an application ID and secret key for you to use. 1. Select "New application". 1. Provide the required details. - - Name: This can be anything. Consider something like "\'s GitLab" or "\'s GitLab" or something else descriptive. + - Name: This can be anything. Consider something like `'s GitLab` or `'s GitLab` or something else descriptive. - Redirect URI: ``` diff --git a/doc/integration/twitter.md b/doc/integration/twitter.md index 4769f26b259ff5..abbea09f22fcc8 100644 --- a/doc/integration/twitter.md +++ b/doc/integration/twitter.md @@ -7,7 +7,7 @@ To enable the Twitter OmniAuth provider you must register your application with 1. Select "Create new app" 1. Fill in the application details. - - Name: This can be anything. Consider something like "\'s GitLab" or "\'s GitLab" or + - Name: This can be anything. Consider something like `'s GitLab` or `'s GitLab` or something else descriptive. - Description: Create a description. - Website: The URL to your GitLab installation. 'https://gitlab.example.com' -- GitLab From 28ed7a1a0ac796699110c829abfae3bf8ace0f77 Mon Sep 17 00:00:00 2001 From: David Warburton Date: Wed, 13 Jul 2016 16:57:39 +0000 Subject: [PATCH 004/153] Update container_registry.md --- doc/administration/container_registry.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md index d5d433034546fe..3f4d44052d2f19 100644 --- a/doc/administration/container_registry.md +++ b/doc/administration/container_registry.md @@ -122,6 +122,8 @@ Registry is exposed to the outside world is `4567`, here is what you need to set in `gitlab.rb` or `gitlab.yml` if you are using Omnibus GitLab or installed GitLab from source respectively. +N.B. Do not choose port 5000, it will conflict with the Docker registry service. + --- **Omnibus GitLab installations** -- GitLab From 0d9752446d8e2b3b4fdb37eb8ec75c5e5f996f1c Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 20 Jul 2016 18:41:26 +0200 Subject: [PATCH 005/153] Add LFS controllers --- .../projects/git_http_client_controller.rb | 110 ++++++++++++++++++ .../projects/git_http_controller.rb | 107 +---------------- .../projects/lfs_api_controller.rb | 94 +++++++++++++++ .../projects/lfs_storage_controller.rb | 106 +++++++++++++++++ app/helpers/lfs_helper.rb | 66 +++++++++++ config/routes.rb | 20 +++- spec/requests/lfs_http_spec.rb | 33 ++++-- 7 files changed, 420 insertions(+), 116 deletions(-) create mode 100644 app/controllers/projects/git_http_client_controller.rb create mode 100644 app/controllers/projects/lfs_api_controller.rb create mode 100644 app/controllers/projects/lfs_storage_controller.rb create mode 100644 app/helpers/lfs_helper.rb diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb new file mode 100644 index 00000000000000..7c21bd181dc04d --- /dev/null +++ b/app/controllers/projects/git_http_client_controller.rb @@ -0,0 +1,110 @@ +# This file should be identical in GitLab Community Edition and Enterprise Edition + +class Projects::GitHttpClientController < Projects::ApplicationController + include ActionController::HttpAuthentication::Basic + include KerberosSpnegoHelper + + attr_reader :user + + # Git clients will not know what authenticity token to send along + skip_before_action :verify_authenticity_token + skip_before_action :repository + before_action :authenticate_user + before_action :ensure_project_found! + + private + + def authenticate_user + if project && project.public? && download_request? + return # Allow access + end + + if allow_basic_auth? && basic_auth_provided? + login, password = user_name_and_password(request) + auth_result = Gitlab::Auth.find_for_git_client(login, password, project: project, ip: request.ip) + + if auth_result.type == :ci && download_request? + @ci = true + elsif auth_result.type == :oauth && !download_request? + # Not allowed + else + @user = auth_result.user + end + + if ci? || user + return # Allow access + end + elsif allow_kerberos_spnego_auth? && spnego_provided? + @user = find_kerberos_user + + if user + send_final_spnego_response + return # Allow access + end + end + + send_challenges + render plain: "HTTP Basic: Access denied\n", status: 401 + end + + def basic_auth_provided? + has_basic_credentials?(request) + end + + def send_challenges + challenges = [] + challenges << 'Basic realm="GitLab"' if allow_basic_auth? + challenges << spnego_challenge if allow_kerberos_spnego_auth? + headers['Www-Authenticate'] = challenges.join("\n") if challenges.any? + end + + def ensure_project_found! + render_not_found if project.blank? + end + + def project + return @project if defined?(@project) + + project_id, _ = project_id_with_suffix + if project_id.blank? + @project = nil + else + @project = Project.find_with_namespace("#{params[:namespace_id]}/#{project_id}") + end + end + + # This method returns two values so that we can parse + # params[:project_id] (untrusted input!) in exactly one place. + def project_id_with_suffix + id = params[:project_id] || '' + + %w[.wiki.git .git].each do |suffix| + if id.end_with?(suffix) + # Be careful to only remove the suffix from the end of 'id'. + # Accidentally removing it from the middle is how security + # vulnerabilities happen! + return [id.slice(0, id.length - suffix.length), suffix] + end + end + + # Something is wrong with params[:project_id]; do not pass it on. + [nil, nil] + end + + def repository + _, suffix = project_id_with_suffix + if suffix == '.wiki.git' + project.wiki.repository + else + project.repository + end + end + + def render_not_found + render plain: 'Not Found', status: :not_found + end + + def ci? + @ci.present? + end +end diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb index 40a8b7940d9eb1..be73a4c0d2cc92 100644 --- a/app/controllers/projects/git_http_controller.rb +++ b/app/controllers/projects/git_http_controller.rb @@ -1,17 +1,6 @@ # This file should be identical in GitLab Community Edition and Enterprise Edition -class Projects::GitHttpController < Projects::ApplicationController - include ActionController::HttpAuthentication::Basic - include KerberosSpnegoHelper - - attr_reader :user - - # Git clients will not know what authenticity token to send along - skip_before_action :verify_authenticity_token - skip_before_action :repository - before_action :authenticate_user - before_action :ensure_project_found! - +class Projects::GitHttpController < Projects::GitHttpClientController # GET /foo/bar.git/info/refs?service=git-upload-pack (git pull) # GET /foo/bar.git/info/refs?service=git-receive-pack (git push) def info_refs @@ -46,81 +35,8 @@ def git_receive_pack private - def authenticate_user - if project && project.public? && upload_pack? - return # Allow access - end - - if allow_basic_auth? && basic_auth_provided? - login, password = user_name_and_password(request) - auth_result = Gitlab::Auth.find_for_git_client(login, password, project: project, ip: request.ip) - - if auth_result.type == :ci && upload_pack? - @ci = true - elsif auth_result.type == :oauth && !upload_pack? - # Not allowed - else - @user = auth_result.user - end - - if ci? || user - return # Allow access - end - elsif allow_kerberos_spnego_auth? && spnego_provided? - @user = find_kerberos_user - - if user - send_final_spnego_response - return # Allow access - end - end - - send_challenges - render plain: "HTTP Basic: Access denied\n", status: 401 - end - - def basic_auth_provided? - has_basic_credentials?(request) - end - - def send_challenges - challenges = [] - challenges << 'Basic realm="GitLab"' if allow_basic_auth? - challenges << spnego_challenge if allow_kerberos_spnego_auth? - headers['Www-Authenticate'] = challenges.join("\n") if challenges.any? - end - - def ensure_project_found! - render_not_found if project.blank? - end - - def project - return @project if defined?(@project) - - project_id, _ = project_id_with_suffix - if project_id.blank? - @project = nil - else - @project = Project.find_with_namespace("#{params[:namespace_id]}/#{project_id}") - end - end - - # This method returns two values so that we can parse - # params[:project_id] (untrusted input!) in exactly one place. - def project_id_with_suffix - id = params[:project_id] || '' - - %w[.wiki.git .git].each do |suffix| - if id.end_with?(suffix) - # Be careful to only remove the suffix from the end of 'id'. - # Accidentally removing it from the middle is how security - # vulnerabilities happen! - return [id.slice(0, id.length - suffix.length), suffix] - end - end - - # Something is wrong with params[:project_id]; do not pass it on. - [nil, nil] + def download_request? + upload_pack? end def upload_pack? @@ -143,27 +59,10 @@ def render_ok render json: Gitlab::Workhorse.git_http_ok(repository, user) end - def repository - _, suffix = project_id_with_suffix - if suffix == '.wiki.git' - project.wiki.repository - else - project.repository - end - end - - def render_not_found - render plain: 'Not Found', status: :not_found - end - def render_not_allowed render plain: download_access.message, status: :forbidden end - def ci? - @ci.present? - end - def upload_pack_allowed? return false unless Gitlab.config.gitlab_shell.upload_pack diff --git a/app/controllers/projects/lfs_api_controller.rb b/app/controllers/projects/lfs_api_controller.rb new file mode 100644 index 00000000000000..694ce5a0aa149f --- /dev/null +++ b/app/controllers/projects/lfs_api_controller.rb @@ -0,0 +1,94 @@ +class Projects::LfsApiController < Projects::GitHttpClientController + include LfsHelper + + before_action :lfs_enabled! + before_action :lfs_check_access!, except: [:deprecated] + + def batch + unless objects.present? + render_lfs_not_found + return + end + + if download_request? + render json: { objects: download_objects! } + elsif upload_request? + render json: { objects: upload_objects! } + else + raise "Never reached" + end + end + + def deprecated + render( + json: { + message: 'Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.', + documentation_url: "#{Gitlab.config.gitlab.url}/help", + }, + status: 501 + ) + end + + private + + def objects + (params[:objects] || []).to_a + end + + def existing_oids + @existing_oids ||= begin + storage_project.lfs_objects.where(oid: objects.map { |o| o['oid'].to_s }).pluck(:oid) + end + end + + def download_objects! + objects.each do |object| + if existing_oids.include?(object[:oid]) + object[:actions] = download_actions(object) + else + object[:error] = { + code: 404, + message: "Object does not exist on the server or you don't have permissions to access it", + } + end + end + objects + end + + def upload_objects! + objects.each do |object| + object[:actions] = upload_actions(object) unless existing_oids.include?(object[:oid]) + end + objects + end + + def download_actions(object) + { + download: { + href: "#{project.http_url_to_repo}/gitlab-lfs/objects/#{object[:oid]}", + header: { + Authorization: request.headers['Authorization'] + }.compact + } + } + end + + def upload_actions(object) + { + upload: { + href: "#{project.http_url_to_repo}/gitlab-lfs/objects/#{object[:oid]}/#{object[:size]}", + header: { + Authorization: request.headers['Authorization'] + }.compact + } + } + end + + def download_request? + params[:operation] == 'download' + end + + def upload_request? + params[:operation] == 'upload' + end +end diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb new file mode 100644 index 00000000000000..3aa08bcd4aed81 --- /dev/null +++ b/app/controllers/projects/lfs_storage_controller.rb @@ -0,0 +1,106 @@ +class Projects::LfsStorageController < Projects::GitHttpClientController + include LfsHelper + + before_action :lfs_enabled! + before_action :lfs_check_access! + + def download + lfs_object = LfsObject.find_by_oid(oid) + unless lfs_object && lfs_object.file.exists? + render_lfs_not_found + return + end + + send_file lfs_object.file.path, content_type: "application/octet-stream" + end + + def upload_authorize + render( + json: { + StoreLFSPath: "#{Gitlab.config.lfs.storage_path}/tmp/upload", + LfsOid: oid, + LfsSize: size, + }, + content_type: 'application/json; charset=utf-8' + ) + end + + def upload_finalize + unless tmp_filename + render_lfs_forbidden + return + end + + if store_file(oid, size, tmp_filename) + head 200 + else + render plain: 'Unprocessable entity', status: 422 + end + end + + private + + def download_request? + action_name == 'download' + end + + def upload_request? + %w[upload_authorize upload_finalize].include? action_name + end + + def oid + params[:oid].to_s + end + + def size + params[:size].to_i + end + + def tmp_filename + name = request.headers['X-Gitlab-Lfs-Tmp'] + if name.present? + name.gsub!(/^.*(\\|\/)/, '') + name = name.match(/[0-9a-f]{73}/) + name[0] if name + else + nil + end + end + + def store_file(oid, size, tmp_file) + tmp_file_path = File.join("#{Gitlab.config.lfs.storage_path}/tmp/upload", tmp_file) + + object = LfsObject.find_or_create_by(oid: oid, size: size) + if object.file.exists? + success = true + else + success = move_tmp_file_to_storage(object, tmp_file_path) + end + + if success + success = link_to_project(object) + end + + success + ensure + # Ensure that the tmp file is removed + FileUtils.rm_f(tmp_file_path) + end + + def move_tmp_file_to_storage(object, path) + File.open(path) do |f| + object.file = f + end + + object.file.store! + object.save + end + + def link_to_project(object) + if object && !object.projects.exists?(storage_project.id) + object.projects << storage_project + object.save + end + end +end + diff --git a/app/helpers/lfs_helper.rb b/app/helpers/lfs_helper.rb new file mode 100644 index 00000000000000..6146a2ad849871 --- /dev/null +++ b/app/helpers/lfs_helper.rb @@ -0,0 +1,66 @@ +module LfsHelper + def lfs_enabled! + return if Gitlab.config.lfs.enabled + + render( + json: { + message: 'Git LFS is not enabled on this GitLab server, contact your admin.', + documentation_url: "#{Gitlab.config.gitlab.url}/help", + }, + status: 501 + ) + end + + def lfs_check_access! + return if download_request? && lfs_download_access? + return if upload_request? && lfs_upload_access? + + if project.public? || (user && user.can?(:read_project, project)) + render_lfs_forbidden + else + render_lfs_not_found + end + end + + def lfs_download_access? + project.public? || ci? || (user && user.can?(:download_code, project)) + end + + def lfs_upload_access? + user && user.can?(:push_code, project) + end + + def render_lfs_forbidden + render( + json: { + message: 'Access forbidden. Check your access level.', + documentation_url: "#{Gitlab.config.gitlab.url}/help", + }, + content_type: "application/vnd.git-lfs+json", + status: 403 + ) + end + + def render_lfs_not_found + render( + json: { + message: 'Not found.', + documentation_url: "#{Gitlab.config.gitlab.url}/help", + }, + content_type: "application/vnd.git-lfs+json", + status: 404 + ) + end + + def storage_project + @storage_project ||= begin + result = project + + while result.forked? do + result = result.forked_from_project + end + + result + end + end +end diff --git a/config/routes.rb b/config/routes.rb index 21f3585bacdc6c..47599441cade66 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -85,9 +85,6 @@ # Health check get 'health_check(/:checks)' => 'health_check#index', as: :health_check - # Enable Grack support (for LFS only) - mount Grack::AuthSpawner, at: '/', constraints: lambda { |request| /[-\/\w\.]+\.git\/(info\/lfs|gitlab-lfs)/.match(request.path_info) }, via: [:get, :post, :put] - # Help get 'help' => 'help#index' get 'help/shortcuts' => 'help#shortcuts' @@ -483,11 +480,26 @@ end scope module: :projects do - # Git HTTP clients ('git clone' etc.) scope constraints: { id: /.+\.git/, format: nil } do + # Git HTTP clients ('git clone' etc.) get '/info/refs', to: 'git_http#info_refs' post '/git-upload-pack', to: 'git_http#git_upload_pack' post '/git-receive-pack', to: 'git_http#git_receive_pack' + + # Git LFS API (metadata) + post '/info/lfs/objects/batch', to: 'lfs_api#batch' + post '/info/lfs/objects', to: 'lfs_api#deprecated' + get '/info/lfs/objects/*oid', to: 'lfs_api#deprecated' + + # GitLab LFS object storage + scope constraints: { oid: /[a-f0-9]{64}/ } do + get '/gitlab-lfs/objects/*oid', to: 'lfs_storage#download' + + scope constraints: { size: /[0-9]+/ } do + put '/gitlab-lfs/objects/*oid/*size/authorize', to: 'lfs_storage#upload_authorize' + put '/gitlab-lfs/objects/*oid/*size', to: 'lfs_storage#upload_finalize' + end + end end # Allow /info/refs, /info/refs?service=git-upload-pack, and diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb index 93d2bc160cc8fc..d6fe4935635673 100644 --- a/spec/requests/lfs_http_spec.rb +++ b/spec/requests/lfs_http_spec.rb @@ -31,6 +31,7 @@ 'operation' => 'upload' } end + let(:authorization) { authorize_user } before do allow(Gitlab.config.lfs).to receive(:enabled).and_return(false) @@ -71,6 +72,7 @@ end context 'when handling lfs request using deprecated API' do + let(:authorization) { authorize_user } before do post_json "#{project.http_url_to_repo}/info/lfs/objects", nil, headers end @@ -118,8 +120,8 @@ project.lfs_objects << lfs_object end - it 'responds with status 403' do - expect(response).to have_http_status(403) + it 'responds with status 404' do + expect(response).to have_http_status(404) end end @@ -147,8 +149,8 @@ context 'without required headers' do let(:authorization) { authorize_user } - it 'responds with status 403' do - expect(response).to have_http_status(403) + it 'responds with status 404' do + expect(response).to have_http_status(404) end end end @@ -304,10 +306,10 @@ end context 'when user does is not member of the project' do - let(:role) { :guest } + let(:update_user_permissions) { nil } - it 'responds with 403' do - expect(response).to have_http_status(403) + it 'responds with 404' do + expect(response).to have_http_status(404) end end @@ -510,6 +512,7 @@ describe 'unsupported' do let(:project) { create(:empty_project) } + let(:authorization) { authorize_user } let(:body) do { 'operation' => 'other', 'objects' => [ @@ -557,7 +560,7 @@ end it 'does not recognize it as a valid lfs command' do - expect(response).to have_http_status(403) + expect(response).to have_http_status(401) end end end @@ -582,6 +585,16 @@ expect(response).to have_http_status(403) end end + + context 'and request is sent with a malformed headers' do + before do + put_finalize('cat /etc/passwd') + end + + it 'does not recognize it as a valid lfs command' do + expect(response).to have_http_status(403) + end + end end describe 'to one project' do @@ -627,6 +640,10 @@ end describe 'and user does not have push access' do + before do + project.team << [user, :reporter] + end + it_behaves_like 'forbidden' end end -- GitLab From df3ca41e62efb7ccbc1ea82d5676527e8eeca530 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 22 Jul 2016 15:45:38 +0200 Subject: [PATCH 006/153] Remove obsolete code --- config/initializers/5_backend.rb | 3 - lib/gitlab/backend/grack_auth.rb | 163 --------------- lib/gitlab/lfs/response.rb | 329 ------------------------------- lib/gitlab/lfs/router.rb | 98 --------- spec/requests/lfs_http_spec.rb | 2 +- 5 files changed, 1 insertion(+), 594 deletions(-) delete mode 100644 lib/gitlab/backend/grack_auth.rb delete mode 100644 lib/gitlab/lfs/response.rb delete mode 100644 lib/gitlab/lfs/router.rb diff --git a/config/initializers/5_backend.rb b/config/initializers/5_backend.rb index e026151a03270c..ed88c8ee1b827d 100644 --- a/config/initializers/5_backend.rb +++ b/config/initializers/5_backend.rb @@ -1,6 +1,3 @@ -# GIT over HTTP -require_dependency Rails.root.join('lib/gitlab/backend/grack_auth') - # GIT over SSH require_dependency Rails.root.join('lib/gitlab/backend/shell') diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb deleted file mode 100644 index ab94abeda77193..00000000000000 --- a/lib/gitlab/backend/grack_auth.rb +++ /dev/null @@ -1,163 +0,0 @@ -module Grack - class AuthSpawner - def self.call(env) - # Avoid issues with instance variables in Grack::Auth persisting across - # requests by creating a new instance for each request. - Auth.new({}).call(env) - end - end - - class Auth < Rack::Auth::Basic - attr_accessor :user, :project, :env - - def call(env) - @env = env - @request = Rack::Request.new(env) - @auth = Request.new(env) - - @ci = false - - # Need this patch due to the rails mount - # Need this if under RELATIVE_URL_ROOT - unless Gitlab.config.gitlab.relative_url_root.empty? - # If website is mounted using relative_url_root need to remove it first - @env['PATH_INFO'] = @request.path.sub(Gitlab.config.gitlab.relative_url_root, '') - else - @env['PATH_INFO'] = @request.path - end - - @env['SCRIPT_NAME'] = "" - - auth! - - lfs_response = Gitlab::Lfs::Router.new(project, @user, @ci, @request).try_call - return lfs_response unless lfs_response.nil? - - if @user.nil? && !@ci - unauthorized - else - render_not_found - end - end - - private - - def auth! - return unless @auth.provided? - - return bad_request unless @auth.basic? - - # Authentication with username and password - login, password = @auth.credentials - - # Allow authentication for GitLab CI service - # if valid token passed - if ci_request?(login, password) - @ci = true - return - end - - @user = authenticate_user(login, password) - end - - def ci_request?(login, password) - matched_login = /(?^[a-zA-Z]*-ci)-token$/.match(login) - - if project && matched_login.present? - underscored_service = matched_login['s'].underscore - - if underscored_service == 'gitlab_ci' - return project && project.valid_build_token?(password) - elsif Service.available_services_names.include?(underscored_service) - service_method = "#{underscored_service}_service" - service = project.send(service_method) - - return service && service.activated? && service.valid_token?(password) - end - end - - false - end - - def oauth_access_token_check(login, password) - if login == "oauth2" && git_cmd == 'git-upload-pack' && password.present? - token = Doorkeeper::AccessToken.by_token(password) - token && token.accessible? && User.find_by(id: token.resource_owner_id) - end - end - - def authenticate_user(login, password) - user = Gitlab::Auth.find_with_user_password(login, password) - - unless user - user = oauth_access_token_check(login, password) - end - - # If the user authenticated successfully, we reset the auth failure count - # from Rack::Attack for that IP. A client may attempt to authenticate - # with a username and blank password first, and only after it receives - # a 401 error does it present a password. Resetting the count prevents - # false positives from occurring. - # - # Otherwise, we let Rack::Attack know there was a failed authentication - # attempt from this IP. This information is stored in the Rails cache - # (Redis) and will be used by the Rack::Attack middleware to decide - # whether to block requests from this IP. - config = Gitlab.config.rack_attack.git_basic_auth - - if config.enabled - if user - # A successful login will reset the auth failure count from this IP - Rack::Attack::Allow2Ban.reset(@request.ip, config) - else - banned = Rack::Attack::Allow2Ban.filter(@request.ip, config) do - # Unless the IP is whitelisted, return true so that Allow2Ban - # increments the counter (stored in Rails.cache) for the IP - if config.ip_whitelist.include?(@request.ip) - false - else - true - end - end - - if banned - Rails.logger.info "IP #{@request.ip} failed to login " \ - "as #{login} but has been temporarily banned from Git auth" - end - end - end - - user - end - - def git_cmd - if @request.get? - @request.params['service'] - elsif @request.post? - File.basename(@request.path) - else - nil - end - end - - def project - return @project if defined?(@project) - - @project = project_by_path(@request.path_info) - end - - def project_by_path(path) - if m = /^([\w\.\/-]+)\.git/.match(path).to_a - path_with_namespace = m.last - path_with_namespace.gsub!(/\.wiki$/, '') - - path_with_namespace[0] = '' if path_with_namespace.start_with?('/') - Project.find_with_namespace(path_with_namespace) - end - end - - def render_not_found - [404, { "Content-Type" => "text/plain" }, ["Not Found"]] - end - end -end diff --git a/lib/gitlab/lfs/response.rb b/lib/gitlab/lfs/response.rb deleted file mode 100644 index a1ee1aa81ff090..00000000000000 --- a/lib/gitlab/lfs/response.rb +++ /dev/null @@ -1,329 +0,0 @@ -module Gitlab - module Lfs - class Response - def initialize(project, user, ci, request) - @origin_project = project - @project = storage_project(project) - @user = user - @ci = ci - @env = request.env - @request = request - end - - def render_download_object_response(oid) - render_response_to_download do - if check_download_sendfile_header? - render_lfs_sendfile(oid) - else - render_not_found - end - end - end - - def render_batch_operation_response - request_body = JSON.parse(@request.body.read) - case request_body["operation"] - when "download" - render_batch_download(request_body) - when "upload" - render_batch_upload(request_body) - else - render_not_found - end - end - - def render_storage_upload_authorize_response(oid, size) - render_response_to_push do - [ - 200, - { "Content-Type" => "application/json; charset=utf-8" }, - [JSON.dump({ - 'StoreLFSPath' => "#{Gitlab.config.lfs.storage_path}/tmp/upload", - 'LfsOid' => oid, - 'LfsSize' => size - })] - ] - end - end - - def render_storage_upload_store_response(oid, size, tmp_file_name) - return render_forbidden unless tmp_file_name - - render_response_to_push do - render_lfs_upload_ok(oid, size, tmp_file_name) - end - end - - def render_unsupported_deprecated_api - [ - 501, - { "Content-Type" => "application/json; charset=utf-8" }, - [JSON.dump({ - 'message' => 'Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.', - 'documentation_url' => "#{Gitlab.config.gitlab.url}/help", - })] - ] - end - - private - - def render_not_enabled - [ - 501, - { - "Content-Type" => "application/json; charset=utf-8", - }, - [JSON.dump({ - 'message' => 'Git LFS is not enabled on this GitLab server, contact your admin.', - 'documentation_url' => "#{Gitlab.config.gitlab.url}/help", - })] - ] - end - - def render_unauthorized - [ - 401, - { - 'Content-Type' => 'text/plain' - }, - ['Unauthorized'] - ] - end - - def render_not_found - [ - 404, - { - "Content-Type" => "application/vnd.git-lfs+json" - }, - [JSON.dump({ - 'message' => 'Not found.', - 'documentation_url' => "#{Gitlab.config.gitlab.url}/help", - })] - ] - end - - def render_forbidden - [ - 403, - { - "Content-Type" => "application/vnd.git-lfs+json" - }, - [JSON.dump({ - 'message' => 'Access forbidden. Check your access level.', - 'documentation_url' => "#{Gitlab.config.gitlab.url}/help", - })] - ] - end - - def render_lfs_sendfile(oid) - return render_not_found unless oid.present? - - lfs_object = object_for_download(oid) - - if lfs_object && lfs_object.file.exists? - [ - 200, - { - # GitLab-workhorse will forward Content-Type header - "Content-Type" => "application/octet-stream", - "X-Sendfile" => lfs_object.file.path - }, - [] - ] - else - render_not_found - end - end - - def render_batch_upload(body) - return render_not_found if body.empty? || body['objects'].nil? - - render_response_to_push do - response = build_upload_batch_response(body['objects']) - [ - 200, - { - "Content-Type" => "application/json; charset=utf-8", - "Cache-Control" => "private", - }, - [JSON.dump(response)] - ] - end - end - - def render_batch_download(body) - return render_not_found if body.empty? || body['objects'].nil? - - render_response_to_download do - response = build_download_batch_response(body['objects']) - [ - 200, - { - "Content-Type" => "application/json; charset=utf-8", - "Cache-Control" => "private", - }, - [JSON.dump(response)] - ] - end - end - - def render_lfs_upload_ok(oid, size, tmp_file) - if store_file(oid, size, tmp_file) - [ - 200, - { - 'Content-Type' => 'text/plain', - 'Content-Length' => 0 - }, - [] - ] - else - [ - 422, - { 'Content-Type' => 'text/plain' }, - ["Unprocessable entity"] - ] - end - end - - def render_response_to_download - return render_not_enabled unless Gitlab.config.lfs.enabled - - unless @project.public? - return render_unauthorized unless @user || @ci - return render_forbidden unless user_can_fetch? - end - - yield - end - - def render_response_to_push - return render_not_enabled unless Gitlab.config.lfs.enabled - return render_unauthorized unless @user - return render_forbidden unless user_can_push? - - yield - end - - def check_download_sendfile_header? - @env['HTTP_X_SENDFILE_TYPE'].to_s == "X-Sendfile" - end - - def user_can_fetch? - # Check user access against the project they used to initiate the pull - @ci || @user.can?(:download_code, @origin_project) - end - - def user_can_push? - # Check user access against the project they used to initiate the push - @user.can?(:push_code, @origin_project) - end - - def storage_project(project) - if project.forked? - storage_project(project.forked_from_project) - else - project - end - end - - def store_file(oid, size, tmp_file) - tmp_file_path = File.join("#{Gitlab.config.lfs.storage_path}/tmp/upload", tmp_file) - - object = LfsObject.find_or_create_by(oid: oid, size: size) - if object.file.exists? - success = true - else - success = move_tmp_file_to_storage(object, tmp_file_path) - end - - if success - success = link_to_project(object) - end - - success - ensure - # Ensure that the tmp file is removed - FileUtils.rm_f(tmp_file_path) - end - - def object_for_download(oid) - @project.lfs_objects.find_by(oid: oid) - end - - def move_tmp_file_to_storage(object, path) - File.open(path) do |f| - object.file = f - end - - object.file.store! - object.save - end - - def link_to_project(object) - if object && !object.projects.exists?(@project.id) - object.projects << @project - object.save - end - end - - def select_existing_objects(objects) - objects_oids = objects.map { |o| o['oid'] } - @project.lfs_objects.where(oid: objects_oids).pluck(:oid).to_set - end - - def build_upload_batch_response(objects) - selected_objects = select_existing_objects(objects) - - upload_hypermedia_links(objects, selected_objects) - end - - def build_download_batch_response(objects) - selected_objects = select_existing_objects(objects) - - download_hypermedia_links(objects, selected_objects) - end - - def download_hypermedia_links(all_objects, existing_objects) - all_objects.each do |object| - if existing_objects.include?(object['oid']) - object['actions'] = { - 'download' => { - 'href' => "#{@origin_project.http_url_to_repo}/gitlab-lfs/objects/#{object['oid']}", - 'header' => { - 'Authorization' => @env['HTTP_AUTHORIZATION'] - }.compact - } - } - else - object['error'] = { - 'code' => 404, - 'message' => "Object does not exist on the server or you don't have permissions to access it", - } - end - end - - { 'objects' => all_objects } - end - - def upload_hypermedia_links(all_objects, existing_objects) - all_objects.each do |object| - # generate actions only for non-existing objects - next if existing_objects.include?(object['oid']) - - object['actions'] = { - 'upload' => { - 'href' => "#{@origin_project.http_url_to_repo}/gitlab-lfs/objects/#{object['oid']}/#{object['size']}", - 'header' => { - 'Authorization' => @env['HTTP_AUTHORIZATION'] - }.compact - } - } - end - - { 'objects' => all_objects } - end - end - end -end diff --git a/lib/gitlab/lfs/router.rb b/lib/gitlab/lfs/router.rb deleted file mode 100644 index f2a76a56b8f245..00000000000000 --- a/lib/gitlab/lfs/router.rb +++ /dev/null @@ -1,98 +0,0 @@ -module Gitlab - module Lfs - class Router - attr_reader :project, :user, :ci, :request - - def initialize(project, user, ci, request) - @project = project - @user = user - @ci = ci - @env = request.env - @request = request - end - - def try_call - return unless @request && @request.path.present? - - case @request.request_method - when 'GET' - get_response - when 'POST' - post_response - when 'PUT' - put_response - else - nil - end - end - - private - - def get_response - path_match = @request.path.match(/\/(info\/lfs|gitlab-lfs)\/objects\/([0-9a-f]{64})$/) - return nil unless path_match - - oid = path_match[2] - return nil unless oid - - case path_match[1] - when "info/lfs" - lfs.render_unsupported_deprecated_api - when "gitlab-lfs" - lfs.render_download_object_response(oid) - else - nil - end - end - - def post_response - post_path = @request.path.match(/\/info\/lfs\/objects(\/batch)?$/) - return nil unless post_path - - # Check for Batch API - if post_path[0].ends_with?("/info/lfs/objects/batch") - lfs.render_batch_operation_response - elsif post_path[0].ends_with?("/info/lfs/objects") - lfs.render_unsupported_deprecated_api - else - nil - end - end - - def put_response - object_match = @request.path.match(/\/gitlab-lfs\/objects\/([0-9a-f]{64})\/([0-9]+)(|\/authorize){1}$/) - return nil if object_match.nil? - - oid = object_match[1] - size = object_match[2].try(:to_i) - return nil if oid.nil? || size.nil? - - # GitLab-workhorse requests - # 1. Try to authorize the request - # 2. send a request with a header containing the name of the temporary file - if object_match[3] && object_match[3] == '/authorize' - lfs.render_storage_upload_authorize_response(oid, size) - else - tmp_file_name = sanitize_tmp_filename(@request.env['HTTP_X_GITLAB_LFS_TMP']) - lfs.render_storage_upload_store_response(oid, size, tmp_file_name) - end - end - - def lfs - return unless @project - - Gitlab::Lfs::Response.new(@project, @user, @ci, @request) - end - - def sanitize_tmp_filename(name) - if name.present? - name.gsub!(/^.*(\\|\/)/, '') - name = name.match(/[0-9a-f]{73}/) - name[0] if name - else - nil - end - end - end - end -end diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb index d6fe4935635673..7692f986d27ecf 100644 --- a/spec/requests/lfs_http_spec.rb +++ b/spec/requests/lfs_http_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Lfs::Router do +describe 'Git LFS API and storage' do let(:user) { create(:user) } let!(:lfs_object) { create(:lfs_object, :with_file) } -- GitLab From d199b3cdd7a43d46d88a6386b95b48c0b40b8315 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 22 Jul 2016 15:56:10 +0200 Subject: [PATCH 007/153] Better cache when modifying in-place --- app/controllers/projects/lfs_api_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/projects/lfs_api_controller.rb b/app/controllers/projects/lfs_api_controller.rb index 694ce5a0aa149f..1fa9cd8f47f83a 100644 --- a/app/controllers/projects/lfs_api_controller.rb +++ b/app/controllers/projects/lfs_api_controller.rb @@ -32,7 +32,7 @@ def deprecated private def objects - (params[:objects] || []).to_a + @objects ||= (params[:objects] || []).to_a end def existing_oids -- GitLab From 23425401d1b574dd87babfffda4d59b9f91d1538 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 22 Jul 2016 16:40:22 +0200 Subject: [PATCH 008/153] Rubocop --- app/controllers/projects/lfs_storage_controller.rb | 13 ++++++------- app/helpers/lfs_helper.rb | 3 ++- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb index 3aa08bcd4aed81..d0895e1324ce57 100644 --- a/app/controllers/projects/lfs_storage_controller.rb +++ b/app/controllers/projects/lfs_storage_controller.rb @@ -16,12 +16,12 @@ def download def upload_authorize render( - json: { - StoreLFSPath: "#{Gitlab.config.lfs.storage_path}/tmp/upload", - LfsOid: oid, - LfsSize: size, - }, - content_type: 'application/json; charset=utf-8' + json: { + StoreLFSPath: "#{Gitlab.config.lfs.storage_path}/tmp/upload", + LfsOid: oid, + LfsSize: size, + }, + content_type: 'application/json; charset=utf-8' ) end @@ -103,4 +103,3 @@ def link_to_project(object) end end end - diff --git a/app/helpers/lfs_helper.rb b/app/helpers/lfs_helper.rb index 6146a2ad849871..ae230ee18782d0 100644 --- a/app/helpers/lfs_helper.rb +++ b/app/helpers/lfs_helper.rb @@ -56,7 +56,8 @@ def storage_project @storage_project ||= begin result = project - while result.forked? do + loop do + break unless result.forked? result = result.forked_from_project end -- GitLab From 71952d057d5edad0697d7da76f5da034689e0f4a Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 22 Jul 2016 17:23:35 +0200 Subject: [PATCH 009/153] Handle custom Git LFS content type --- config/initializers/mime_types.rb | 7 +++++++ spec/requests/lfs_http_spec.rb | 10 +++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb index 3e55312020593e..f498732feca212 100644 --- a/config/initializers/mime_types.rb +++ b/config/initializers/mime_types.rb @@ -12,3 +12,10 @@ Mime::Type.register "video/mp4", :mp4, [], [:m4v, :mov] Mime::Type.register "video/webm", :webm Mime::Type.register "video/ogg", :ogv + +middlewares = Gitlab::Application.config.middleware +middlewares.swap(ActionDispatch::ParamsParser, ActionDispatch::ParamsParser, { + Mime::Type.lookup('application/vnd.git-lfs+json') => lambda do |body| + ActiveSupport::JSON.decode(body) + end +}) diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb index 7692f986d27ecf..26572699bf8a06 100644 --- a/spec/requests/lfs_http_spec.rb +++ b/spec/requests/lfs_http_spec.rb @@ -35,7 +35,7 @@ before do allow(Gitlab.config.lfs).to receive(:enabled).and_return(false) - post_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers + post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers end it 'responds with 501' do @@ -74,7 +74,7 @@ context 'when handling lfs request using deprecated API' do let(:authorization) { authorize_user } before do - post_json "#{project.http_url_to_repo}/info/lfs/objects", nil, headers + post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects", nil, headers end it_behaves_like 'a deprecated' @@ -164,7 +164,7 @@ enable_lfs update_lfs_permissions update_user_permissions - post_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers + post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers end describe 'download' do @@ -775,8 +775,8 @@ def fork_project(project, user, object = nil) Projects::ForkService.new(project, user, {}).execute end - def post_json(url, body = nil, headers = nil) - post(url, body.try(:to_json), (headers || {}).merge('Content-Type' => 'application/json')) + def post_lfs_json(url, body = nil, headers = nil) + post(url, body.try(:to_json), (headers || {}).merge('Content-Type' => 'application/vnd.git-lfs+json')) end def json_response -- GitLab From 1799b4a1ac4d5f27e417982c06e8ed3d27ff39d1 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Mon, 25 Jul 2016 15:15:44 -0500 Subject: [PATCH 010/153] Align visibility icons on group page --- app/views/shared/projects/_project.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index b8b66d08db8585..92803838d02ea8 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -24,7 +24,7 @@ = icon('star') = project.star_count %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project)} - = visibility_level_icon(project.visibility_level, fw: false) + = visibility_level_icon(project.visibility_level, fw: true) .title = link_to project_path(project), class: dom_class(project) do -- GitLab From 2e6085d8af37d77e550331595ca8a1fc8f2af49f Mon Sep 17 00:00:00 2001 From: Elliot Wiltshire Date: Mon, 25 Jul 2016 14:46:40 -0700 Subject: [PATCH 011/153] Fixing scope issue in GitAccess. --- lib/gitlab/git_access.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index 8e8f39d9cb2543..d321a1292921c7 100644 --- a/lib/gitlab/git_access.rb +++ b/lib/gitlab/git_access.rb @@ -134,7 +134,7 @@ def user end def build_status_object(status, message = '') - GitAccessStatus.new(status, message) + Gitlab::GitAccessStatus.new(status, message) end end end -- GitLab From df0a77a7bed7c860793e027421a3fafeb12646df Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Tue, 26 Jul 2016 16:59:55 -0600 Subject: [PATCH 012/153] Removes two simple instances of inline JavaScript. --- app/assets/javascripts/dispatcher.js | 4 ++++ app/views/admin/labels/_form.html.haml | 3 --- app/views/projects/_home_panel.html.haml | 3 --- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index d212d66da1b595..308885dcfe903e 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -181,6 +181,9 @@ break; case 'projects': new NamespaceSelects(); + break; + case 'labels': + new Labels(); } break; case 'dashboard': @@ -206,6 +209,7 @@ new ProjectNew(); break; case 'show': + new Star(); new ProjectNew(); new ProjectShow(); new NotificationsDropdown(); diff --git a/app/views/admin/labels/_form.html.haml b/app/views/admin/labels/_form.html.haml index 448aa95354818b..602cfa9b6fc8e7 100644 --- a/app/views/admin/labels/_form.html.haml +++ b/app/views/admin/labels/_form.html.haml @@ -28,6 +28,3 @@ .form-actions = f.submit 'Save', class: 'btn btn-save js-save-button' = link_to "Cancel", admin_labels_path, class: 'btn btn-cancel' - -:javascript - new Labels(); diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 51f74f3b7ce6dd..8ef31ca3bda74b 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -24,6 +24,3 @@ .project-clone-holder = render "shared/clone_panel" - -:javascript - new Star(); -- GitLab From 8f3e3f6b8070d6887f0d267fcaea89579d909287 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 27 Jul 2016 03:24:11 -0500 Subject: [PATCH 013/153] Make sure Labels is instantiated on edit page --- app/assets/javascripts/dispatcher.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 308885dcfe903e..82435c51a0bdc5 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -183,7 +183,10 @@ new NamespaceSelects(); break; case 'labels': - new Labels(); + switch (path[2]) { + case 'edit': + new Labels(); + } } break; case 'dashboard': -- GitLab From 33e14285d3dc0c2340ca34ede7235644a418cea6 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 27 Jul 2016 13:08:43 +0200 Subject: [PATCH 014/153] Add generic SVG badge template --- app/views/projects/badges/badge.svg.erb | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 app/views/projects/badges/badge.svg.erb diff --git a/app/views/projects/badges/badge.svg.erb b/app/views/projects/badges/badge.svg.erb new file mode 100644 index 00000000000000..72f341176e33d9 --- /dev/null +++ b/app/views/projects/badges/badge.svg.erb @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + build + build + success + success + + + -- GitLab From 42c035ee38a6629090da07aea818f4c9e3733075 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 28 Jul 2016 13:50:50 +0200 Subject: [PATCH 015/153] Add badge template to generic badge view template --- app/views/projects/badges/badge.svg.erb | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/views/projects/badges/badge.svg.erb b/app/views/projects/badges/badge.svg.erb index 72f341176e33d9..5a71419d3af91f 100644 --- a/app/views/projects/badges/badge.svg.erb +++ b/app/views/projects/badges/badge.svg.erb @@ -1,25 +1,25 @@ - + - + - - - + + + - build - build - success - success + <%= badge.key_text %> + <%= badge.key_text %> + <%= badge.value_text %> + <%= badge.value_text %> -- GitLab From 9ae1ecf876e40ce9dd64c72e025f32e38c882fd6 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 28 Jul 2016 14:35:02 +0200 Subject: [PATCH 016/153] Extract build status badge metadata to separate class --- .../projects/pipelines_settings_controller.rb | 2 +- lib/gitlab/badge/build.rb | 25 ++----------- lib/gitlab/badge/build/metadata.rb | 36 ++++++++++++++++++ spec/lib/gitlab/badge/build/metadata_spec.rb | 37 +++++++++++++++++++ spec/lib/gitlab/badge/build_spec.rb | 31 ++-------------- 5 files changed, 82 insertions(+), 49 deletions(-) create mode 100644 lib/gitlab/badge/build/metadata.rb create mode 100644 spec/lib/gitlab/badge/build/metadata_spec.rb diff --git a/app/controllers/projects/pipelines_settings_controller.rb b/app/controllers/projects/pipelines_settings_controller.rb index 85ba706e5cd050..75dd3648e45280 100644 --- a/app/controllers/projects/pipelines_settings_controller.rb +++ b/app/controllers/projects/pipelines_settings_controller.rb @@ -3,7 +3,7 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController def show @ref = params[:ref] || @project.default_branch || 'master' - @build_badge = Gitlab::Badge::Build.new(@project, @ref) + @build_badge = Gitlab::Badge::Build.new(@project, @ref).metadata end def update diff --git a/lib/gitlab/badge/build.rb b/lib/gitlab/badge/build.rb index e5e9fab3f5c8c8..21d60854bf50cb 100644 --- a/lib/gitlab/badge/build.rb +++ b/lib/gitlab/badge/build.rb @@ -4,15 +4,15 @@ module Badge # Build badge # class Build - include Gitlab::Application.routes.url_helpers - include ActionView::Helpers::AssetTagHelper - include ActionView::Helpers::UrlHelper - def initialize(project, ref) @project, @ref = project, ref @image = ::Ci::ImageForBuildService.new.execute(project, ref: ref) end + def metadata + Build::Metadata.new(@project, @ref) + end + def type 'image/svg+xml' end @@ -24,23 +24,6 @@ def data def to_s @image[:name].sub(/\.svg$/, '') end - - def to_html - link_to(image_tag(image_url, alt: 'build status'), link_url) - end - - def to_markdown - "[![build status](#{image_url})](#{link_url})" - end - - def image_url - build_namespace_project_badges_url(@project.namespace, - @project, @ref, format: :svg) - end - - def link_url - namespace_project_commits_url(@project.namespace, @project, id: @ref) - end end end end diff --git a/lib/gitlab/badge/build/metadata.rb b/lib/gitlab/badge/build/metadata.rb new file mode 100644 index 00000000000000..553ef8d7b16169 --- /dev/null +++ b/lib/gitlab/badge/build/metadata.rb @@ -0,0 +1,36 @@ +module Gitlab + module Badge + class Build + ## + # Class that describes build badge metadata + # + class Metadata + include Gitlab::Application.routes.url_helpers + include ActionView::Helpers::AssetTagHelper + include ActionView::Helpers::UrlHelper + + def initialize(project, ref) + @project = project + @ref = ref + end + + def to_html + link_to(image_tag(image_url, alt: 'build status'), link_url) + end + + def to_markdown + "[![build status](#{image_url})](#{link_url})" + end + + def image_url + build_namespace_project_badges_url(@project.namespace, + @project, @ref, format: :svg) + end + + def link_url + namespace_project_commits_url(@project.namespace, @project, id: @ref) + end + end + end + end +end diff --git a/spec/lib/gitlab/badge/build/metadata_spec.rb b/spec/lib/gitlab/badge/build/metadata_spec.rb new file mode 100644 index 00000000000000..ad5388215c27cf --- /dev/null +++ b/spec/lib/gitlab/badge/build/metadata_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe Gitlab::Badge::Build::Metadata do + let(:project) { create(:project) } + let(:branch) { 'master' } + let(:badge) { described_class.new(project, branch) } + + describe '#to_html' do + let(:html) { Nokogiri::HTML.parse(badge.to_html) } + let(:a_href) { html.at('a') } + + it 'points to link' do + expect(a_href[:href]).to eq badge.link_url + end + + it 'contains clickable image' do + expect(a_href.children.first.name).to eq 'img' + end + end + + describe '#to_markdown' do + subject { badge.to_markdown } + + it { is_expected.to include badge.image_url } + it { is_expected.to include badge.link_url } + end + + describe '#image_url' do + subject { badge.image_url } + it { is_expected.to include "badges/#{branch}/build.svg" } + end + + describe '#link_url' do + subject { badge.link_url } + it { is_expected.to include "commits/#{branch}" } + end +end diff --git a/spec/lib/gitlab/badge/build_spec.rb b/spec/lib/gitlab/badge/build_spec.rb index f3b522a02f52d1..b7a83dc8af03b8 100644 --- a/spec/lib/gitlab/badge/build_spec.rb +++ b/spec/lib/gitlab/badge/build_spec.rb @@ -11,36 +11,13 @@ it { is_expected.to eq 'image/svg+xml' } end - describe '#to_html' do - let(:html) { Nokogiri::HTML.parse(badge.to_html) } - let(:a_href) { html.at('a') } - - it 'points to link' do - expect(a_href[:href]).to eq badge.link_url - end - - it 'contains clickable image' do - expect(a_href.children.first.name).to eq 'img' + describe '#metadata' do + it 'returns badge metadata' do + expect(badge.metadata.image_url) + .to include 'badges/master/build.svg' end end - describe '#to_markdown' do - subject { badge.to_markdown } - - it { is_expected.to include badge.image_url } - it { is_expected.to include badge.link_url } - end - - describe '#image_url' do - subject { badge.image_url } - it { is_expected.to include "badges/#{branch}/build.svg" } - end - - describe '#link_url' do - subject { badge.link_url } - it { is_expected.to include "commits/#{branch}" } - end - context 'build exists' do let!(:build) { create_build(project, sha, branch) } -- GitLab From 0c4fa8619ca477c0a78c825df8dd38cd2a109644 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 28 Jul 2016 14:58:53 +0200 Subject: [PATCH 017/153] Calculate build status only in build badge class --- lib/gitlab/badge/build.rb | 20 +++++++++++++------- spec/lib/gitlab/badge/build_spec.rb | 23 +++++++++++++---------- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/lib/gitlab/badge/build.rb b/lib/gitlab/badge/build.rb index 21d60854bf50cb..7bc6f285ce1b07 100644 --- a/lib/gitlab/badge/build.rb +++ b/lib/gitlab/badge/build.rb @@ -5,8 +5,16 @@ module Badge # class Build def initialize(project, ref) - @project, @ref = project, ref - @image = ::Ci::ImageForBuildService.new.execute(project, ref: ref) + @project = project + @ref = ref + end + + def status + sha = @project.commit(@ref).try(:sha) + + @project.pipelines + .where(sha: sha, ref: @ref) + .status || 'unknown' end def metadata @@ -18,11 +26,9 @@ def type end def data - File.read(@image[:path]) - end - - def to_s - @image[:name].sub(/\.svg$/, '') + File.read( + Rails.root.join('public/ci', 'build-' + status + '.svg') + ) end end end diff --git a/spec/lib/gitlab/badge/build_spec.rb b/spec/lib/gitlab/badge/build_spec.rb index b7a83dc8af03b8..1b36c0a36eaeaa 100644 --- a/spec/lib/gitlab/badge/build_spec.rb +++ b/spec/lib/gitlab/badge/build_spec.rb @@ -24,9 +24,10 @@ context 'build success' do before { build.success! } - describe '#to_s' do - subject { badge.to_s } - it { is_expected.to eq 'build-success' } + describe '#status' do + it 'is successful' do + expect(badge.status).to eq 'success' + end end describe '#data' do @@ -41,9 +42,10 @@ context 'build failed' do before { build.drop! } - describe '#to_s' do - subject { badge.to_s } - it { is_expected.to eq 'build-failed' } + describe '#status' do + it 'failed' do + expect(badge.status).to eq 'failed' + end end describe '#data' do @@ -57,9 +59,10 @@ end context 'build does not exist' do - describe '#to_s' do - subject { badge.to_s } - it { is_expected.to eq 'build-unknown' } + describe '#status' do + it 'is unknown' do + expect(badge.status).to eq 'unknown' + end end describe '#data' do @@ -81,7 +84,7 @@ end it 'does not take outdated pipeline into account' do - expect(badge.to_s).to eq 'build-success' + expect(badge.status).to eq 'success' end end -- GitLab From 503c44ee2a66e3e160a9ca9c02aba14a8aa7e310 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 28 Jul 2016 15:30:05 +0200 Subject: [PATCH 018/153] Add badge template class to use with SVG ERB template --- lib/gitlab/badge/build/template.rb | 63 ++++++++++++++++ spec/lib/gitlab/badge/build/template_spec.rb | 76 ++++++++++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 lib/gitlab/badge/build/template.rb create mode 100644 spec/lib/gitlab/badge/build/template_spec.rb diff --git a/lib/gitlab/badge/build/template.rb b/lib/gitlab/badge/build/template.rb new file mode 100644 index 00000000000000..a7c2e17693569d --- /dev/null +++ b/lib/gitlab/badge/build/template.rb @@ -0,0 +1,63 @@ +module Gitlab + module Badge + class Build + ## + # Abstract class for build badge template. + # + # Template object will be passed to badge.svg.erb template. + # + class Template + STATUS_COLOR = { + success: '#4c1', + failed: '#e05d44', + running: '#dfb317', + pending: '#dfb317', + canceled: '#9f9f9f', + skipped: '#9f9f9f', + unknown: '#9f9f9f' + } + + def initialize(status) + @status = status + end + + def key_text + 'build' + end + + def value_text + @status + end + + def key_width + 38 + end + + def value_width + 54 + end + + def key_color + '#555' + end + + def value_color + STATUS_COLOR[@status.to_sym] || + STATUS_COLOR[:unknown] + end + + def key_text_anchor + key_width / 2 + end + + def value_text_anchor + key_width + (value_width / 2) + end + + def width + key_width + value_width + end + end + end + end +end diff --git a/spec/lib/gitlab/badge/build/template_spec.rb b/spec/lib/gitlab/badge/build/template_spec.rb new file mode 100644 index 00000000000000..86dead3c54e1c2 --- /dev/null +++ b/spec/lib/gitlab/badge/build/template_spec.rb @@ -0,0 +1,76 @@ +require 'spec_helper' + +describe Gitlab::Badge::Build::Template do + let(:status) { 'success' } + let(:template) { described_class.new(status) } + + describe '#key_text' do + it 'is always says build' do + expect(template.key_text).to eq 'build' + end + end + + describe '#value_text' do + it 'is status value' do + expect(template.value_text).to eq 'success' + end + end + + describe 'widths and text anchors' do + it 'has fixed width and text anchors' do + expect(template.width).to eq 92 + expect(template.key_width).to eq 38 + expect(template.value_width).to eq 54 + expect(template.key_text_anchor).to eq 19 + expect(template.value_text_anchor).to eq 65 + end + end + + describe '#key_color' do + it 'is always the same' do + expect(template.key_color).to eq '#555' + end + end + + describe '#value_color' do + context 'when status is success' do + let(:status) { 'success' } + + it 'has expected color' do + expect(template.value_color).to eq '#4c1' + end + end + + context 'when status is failed' do + let(:status) { 'failed' } + + it 'has expected color' do + expect(template.value_color).to eq '#e05d44' + end + end + + context 'when status is running' do + let(:status) { 'running' } + + it 'has expected color' do + expect(template.value_color).to eq '#dfb317' + end + end + + context 'when status is unknown' do + let(:status) { 'unknown' } + + it 'has expected color' do + expect(template.value_color).to eq '#9f9f9f' + end + end + + context 'when status does not match any known statuses' do + let(:status) { 'invalid status' } + + it 'has expected color' do + expect(template.value_color).to eq '#9f9f9f' + end + end + end +end -- GitLab From 9f6ceed554319592c8bb47421c4a2654178e3453 Mon Sep 17 00:00:00 2001 From: David Warburton Date: Mon, 1 Aug 2016 23:34:55 +0000 Subject: [PATCH 019/153] Update container_registry.md --- doc/administration/container_registry.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md index 3f4d44052d2f19..44ed8c88a39b49 100644 --- a/doc/administration/container_registry.md +++ b/doc/administration/container_registry.md @@ -122,7 +122,9 @@ Registry is exposed to the outside world is `4567`, here is what you need to set in `gitlab.rb` or `gitlab.yml` if you are using Omnibus GitLab or installed GitLab from source respectively. -N.B. Do not choose port 5000, it will conflict with the Docker registry service. +>**Note:** +Be careful to choose a port different than the one that Registry listens to (`5000` by default), +otherwise you will run into conflicts . --- -- GitLab From 4dd1f95516731465caa523be402400f56a53a4e9 Mon Sep 17 00:00:00 2001 From: winniehell Date: Sun, 24 Jul 2016 06:43:21 +0200 Subject: [PATCH 020/153] Add failing test for #20026 --- .../filter/relative_link_filter_spec.rb | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb index 224baca803019c..4b8d14ec9027a6 100644 --- a/spec/lib/banzai/filter/relative_link_filter_spec.rb +++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb @@ -3,7 +3,7 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do def filter(doc, contexts = {}) contexts.reverse_merge!({ - commit: project.commit, + commit: commit, project: project, project_wiki: project_wiki, ref: ref, @@ -28,6 +28,7 @@ def link(path) let(:project) { create(:project) } let(:project_path) { project.path_with_namespace } let(:ref) { 'markdown' } + let(:commit) { project.commit(ref) } let(:project_wiki) { nil } let(:requested_path) { '/' } @@ -77,7 +78,13 @@ def link(path) expect { filter(act) }.not_to raise_error end - context 'with a valid repository' do + it 'ignores ref if commit is passed' do + doc = filter(link('non/existent.file'), commit: project.commit('empty-branch') ) + expect(doc.at_css('a')['href']). + to eq "/#{project_path}/#{ref}/non/existent.file" # non-existent files have no leading blob/raw/tree + end + + shared_examples :valid_repository do it 'rebuilds absolute URL for a file in the repo' do doc = filter(link('/doc/api/README.md')) expect(doc.at_css('a')['href']). @@ -184,4 +191,13 @@ def link(path) include_examples :relative_to_requested end end + + context 'with a valid commit' do + include_examples :valid_repository + end + + context 'with a valid ref' do + let(:commit) { nil } # force filter to use ref instead of commit + include_examples :valid_repository + end end -- GitLab From e63eccf9744de0965d4727326a4b30f1fe8165e8 Mon Sep 17 00:00:00 2001 From: winniehell Date: Mon, 1 Aug 2016 02:43:29 +0200 Subject: [PATCH 021/153] Do not look up commit again when it is passed to RelativeLinkFilter (!5455) --- lib/banzai/filter/relative_link_filter.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/banzai/filter/relative_link_filter.rb b/lib/banzai/filter/relative_link_filter.rb index 5b73fc8fceec1b..7d172ccdb8f19f 100644 --- a/lib/banzai/filter/relative_link_filter.rb +++ b/lib/banzai/filter/relative_link_filter.rb @@ -51,7 +51,7 @@ def rebuild_relative_uri(uri) relative_url_root, context[:project].path_with_namespace, uri_type(file_path), - ref || context[:project].default_branch, # if no ref exists, point to the default branch + ref, file_path ].compact.join('/').squeeze('/').chomp('/') @@ -115,7 +115,7 @@ def uri_type(path) end def current_commit - @current_commit ||= context[:commit] || ref ? repository.commit(ref) : repository.head_commit + @current_commit ||= context[:commit] || repository.commit(ref) end def relative_url_root @@ -123,7 +123,7 @@ def relative_url_root end def ref - context[:ref] + context[:ref] || context[:project].default_branch end def repository -- GitLab From c7b31e4029978adcde4e7f47622a6364d9ed121a Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 3 Aug 2016 12:01:20 +0200 Subject: [PATCH 022/153] Enable some Rubocop cops related to new lines --- .rubocop.yml | 4 ++-- .../features/admin/admin_disables_git_access_protocol_spec.rb | 1 - spec/lib/banzai/filter/video_link_filter_spec.rb | 1 - spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 1 - spec/lib/gitlab/git/hook_spec.rb | 1 - spec/lib/gitlab/import_export/project_tree_restorer_spec.rb | 1 - spec/lib/gitlab/user_access_spec.rb | 1 - 7 files changed, 2 insertions(+), 8 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 556a5d11a394a7..d90c47022ecd3f 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -149,7 +149,7 @@ Style/EmptyLinesAroundAccessModifier: # Keeps track of empty lines around block bodies. Style/EmptyLinesAroundBlockBody: - Enabled: false + Enabled: true # Keeps track of empty lines around class bodies. Style/EmptyLinesAroundClassBody: @@ -161,7 +161,7 @@ Style/EmptyLinesAroundModuleBody: # Keeps track of empty lines around method bodies. Style/EmptyLinesAroundMethodBody: - Enabled: false + Enabled: true # Avoid the use of END blocks. Style/EndBlock: diff --git a/spec/features/admin/admin_disables_git_access_protocol_spec.rb b/spec/features/admin/admin_disables_git_access_protocol_spec.rb index 5b1c0460274087..66044b444952de 100644 --- a/spec/features/admin/admin_disables_git_access_protocol_spec.rb +++ b/spec/features/admin/admin_disables_git_access_protocol_spec.rb @@ -45,7 +45,6 @@ expect(page).to have_content("git clone #{project.ssh_url_to_repo}") expect(page).to have_selector('#clone-dropdown') end - end def visit_project diff --git a/spec/lib/banzai/filter/video_link_filter_spec.rb b/spec/lib/banzai/filter/video_link_filter_spec.rb index cc4349f80ba5aa..6ab1be9ccb70e8 100644 --- a/spec/lib/banzai/filter/video_link_filter_spec.rb +++ b/spec/lib/banzai/filter/video_link_filter_spec.rb @@ -47,5 +47,4 @@ def link_to_image(path) expect(element['src']).to eq '/path/my_image.jpg' end end - end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index 61490555ff5a40..a7d1d6b0a8bfda 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -534,7 +534,6 @@ module Ci end context 'when also global variables are defined' do - end context 'when syntax is correct' do diff --git a/spec/lib/gitlab/git/hook_spec.rb b/spec/lib/gitlab/git/hook_spec.rb index a15aa173fbd314..d1f947b6850094 100644 --- a/spec/lib/gitlab/git/hook_spec.rb +++ b/spec/lib/gitlab/git/hook_spec.rb @@ -25,7 +25,6 @@ def create_failing_hook(name) end ['pre-receive', 'post-receive', 'update'].each do |hook_name| - context "when triggering a #{hook_name} hook" do context "when the hook is successful" do it "returns success with no errors" do diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index 32c0d6462f1491..5fdd4d5f25f15e 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -2,7 +2,6 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do describe 'restore project tree' do - let(:user) { create(:user) } let(:namespace) { create(:namespace, owner: user) } let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path') } diff --git a/spec/lib/gitlab/user_access_spec.rb b/spec/lib/gitlab/user_access_spec.rb index 5bb095366fa1d7..b67e5efb244dd8 100644 --- a/spec/lib/gitlab/user_access_spec.rb +++ b/spec/lib/gitlab/user_access_spec.rb @@ -83,6 +83,5 @@ expect(access.can_merge_to_branch?(@branch.name)).to be_falsey end end - end end -- GitLab From b8f754dd0abdf437669e17a820a8e6c230afa73e Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 3 Aug 2016 14:54:12 +0200 Subject: [PATCH 023/153] Stop 'git push' over HTTP early Before this change we always let users push Git data over HTTP before deciding whether to accept to push. This was different from pushing over SSH where we terminate a 'git push' early if we already know the user is not allowed to push. This change let Git over HTTP follow the same behavior as Git over SSH. We also distinguish between HTTP 404 and 403 responses when denying Git requests, depending on whether the user is allowed to know the project exists. --- .../projects/git_http_controller.rb | 39 +++++++++++-------- lib/gitlab/git_access.rb | 2 +- spec/requests/git_http_spec.rb | 10 ++--- 3 files changed, 28 insertions(+), 23 deletions(-) diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb index 40a8b7940d9eb1..e2f93e239bdeab 100644 --- a/app/controllers/projects/git_http_controller.rb +++ b/app/controllers/projects/git_http_controller.rb @@ -20,9 +20,9 @@ def info_refs elsif receive_pack? && receive_pack_allowed? render_ok elsif http_blocked? - render_not_allowed + render_http_not_allowed else - render_not_found + render_denied end end @@ -31,7 +31,7 @@ def git_upload_pack if upload_pack? && upload_pack_allowed? render_ok else - render_not_found + render_denied end end @@ -40,7 +40,7 @@ def git_receive_pack if receive_pack? && receive_pack_allowed? render_ok else - render_not_found + render_denied end end @@ -156,8 +156,17 @@ def render_not_found render plain: 'Not Found', status: :not_found end - def render_not_allowed - render plain: download_access.message, status: :forbidden + def render_http_not_allowed + render plain: access_check.message, status: :forbidden + end + + def render_denied + if user && user.can?(:read_project, project) + render plain: 'Access denied', status: :forbidden + else + # Do not leak information about project existence + render_not_found + end end def ci? @@ -168,22 +177,20 @@ def upload_pack_allowed? return false unless Gitlab.config.gitlab_shell.upload_pack if user - download_access.allowed? + access_check.allowed? else ci? || project.public? end end def access - return @access if defined?(@access) - - @access = Gitlab::GitAccess.new(user, project, 'http') + @access ||= Gitlab::GitAccess.new(user, project, 'http') end - def download_access - return @download_access if defined?(@download_access) - - @download_access = access.check('git-upload-pack') + def access_check + # Use the magic string '_any' to indicate we do not know what the + # changes are. This is also what gitlab-shell does. + @access_check ||= access.check(git_command, '_any') end def http_blocked? @@ -193,8 +200,6 @@ def http_blocked? def receive_pack_allowed? return false unless Gitlab.config.gitlab_shell.receive_pack - # Skip user authorization on upload request. - # It will be done by the pre-receive hook in the repository. - user.present? + access_check.allowed? end end diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index 8e8f39d9cb2543..69943e223534f2 100644 --- a/lib/gitlab/git_access.rb +++ b/lib/gitlab/git_access.rb @@ -14,7 +14,7 @@ def initialize(actor, project, protocol) @user_access = UserAccess.new(user, project: project) end - def check(cmd, changes = nil) + def check(cmd, changes) return build_status_object(false, "Git access over #{protocol.upcase} is not allowed") unless protocol_allowed? unless actor diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index 82ab582beace73..febfdf48c7ec78 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -75,9 +75,9 @@ context "with correct credentials" do let(:env) { { user: user.username, password: user.password } } - it "uploads get status 200 (because Git hooks do the real check)" do + it "uploads get status 403" do upload(path, env) do |response| - expect(response).to have_http_status(200) + expect(response).to have_http_status(403) end end @@ -86,7 +86,7 @@ allow(Gitlab.config.gitlab_shell).to receive(:receive_pack).and_return(false) upload(path, env) do |response| - expect(response).to have_http_status(404) + expect(response).to have_http_status(403) end end end @@ -236,9 +236,9 @@ def attempt_login(include_password) end end - it "uploads get status 200 (because Git hooks do the real check)" do + it "uploads get status 404" do upload(path, user: user.username, password: user.password) do |response| - expect(response).to have_http_status(200) + expect(response).to have_http_status(404) end end end -- GitLab From da3d3ba89c19364ca626eb57380e1e33bd344902 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Wed, 3 Aug 2016 14:45:32 +0200 Subject: [PATCH 024/153] Endpoints to enable and disable deploy keys Resolves #20123 --- CHANGELOG | 1 + doc/api/deploy_keys.md | 49 ++++++++++++++++ lib/api/deploy_keys.rb | 31 ++++++++++ spec/requests/api/deploy_keys.rb | 38 ------------ spec/requests/api/deploy_keys_spec.rb | 84 +++++++++++++++++++++++++++ 5 files changed, 165 insertions(+), 38 deletions(-) delete mode 100644 spec/requests/api/deploy_keys.rb create mode 100644 spec/requests/api/deploy_keys_spec.rb diff --git a/CHANGELOG b/CHANGELOG index db2617dcbd7db6..e28897d456b74b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,6 +4,7 @@ v 8.11.0 (unreleased) - Fix the title of the toggle dropdown button. !5515 (herminiotorres) - Improve diff performance by eliminating redundant checks for text blobs - Convert switch icon into icon font (ClemMakesApps) + - API: Endpoints for enabling and disabling deploy keys - Remove magic comments (`# encoding: UTF-8`) from Ruby files. !5456 (winniehell) - Add support for relative links starting with ./ or / to RelativeLinkFilter (winniehell) - Fix CI status icon link underline (ClemMakesApps) diff --git a/doc/api/deploy_keys.md b/doc/api/deploy_keys.md index 4e620ccc81a596..a0340fd4d37c80 100644 --- a/doc/api/deploy_keys.md +++ b/doc/api/deploy_keys.md @@ -159,3 +159,52 @@ Example response: "id" : 13 } ``` + +## Enable a deploy key + +Enables a deploy key for a project so this can be used. Returns the enabled key, with a status code 201 when successful. + +``` +curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/deploy_keys/13/enable +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of the project | +| `key_id` | integer | yes | The ID of the deploy key | + +Example response: + +```json +```json +{ + "key" : "ssh-rsa AAAA...", + "id" : 12, + "title" : "My deploy key", + "created_at" : "2015-08-29T12:44:31.550Z" +} +``` + +## Disable a deploy key + +Disable a deploy key for a project. Returns the disabled key. + +``` +curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/deploy_keys/13/disable +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of the project | +| `key_id` | integer | yes | The ID of the deploy key | + +Example response: + +```json +{ + "key" : "ssh-rsa AAAA...", + "id" : 12, + "title" : "My deploy key", + "created_at" : "2015-08-29T12:44:31.550Z" +} +``` diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb index 5c570b5e5ca375..ab4868eca2db3b 100644 --- a/lib/api/deploy_keys.rb +++ b/lib/api/deploy_keys.rb @@ -74,6 +74,37 @@ class DeployKeys < Grape::API end end + desc 'Enable a deploy key for a project' do + detail 'This feature was added in GitLab 8.11' + success Entities::SSHKey + end + params do + requires :key_id, type: Integer, desc: 'The ID of the deploy key' + end + post ":id/#{path}/:key_id/enable" do + key = DeployKey.find(params[:key_id]) + + if user_project.deploy_keys << key + present key, with: Entities::SSHKey + else + render_validation_error!(key) + end + end + + desc 'Disable a deploy key for a project' do + detail 'This feature was added in GitLab 8.11' + success Entities::SSHKey + end + params do + requires :key_id, type: Integer, desc: 'The ID of the deploy key' + end + delete ":id/#{path}/:key_id/disable" do + key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id]) + key.destroy + + present key.deploy_key, with: Entities::SSHKey + end + # Delete existing deploy key of currently authenticated user # # Example Request: diff --git a/spec/requests/api/deploy_keys.rb b/spec/requests/api/deploy_keys.rb deleted file mode 100644 index ac42288bc3493d..00000000000000 --- a/spec/requests/api/deploy_keys.rb +++ /dev/null @@ -1,38 +0,0 @@ -require 'spec_helper' - -describe API::API, api: true do - include ApiHelpers - - let(:user) { create(:user) } - let(:project) { create(:project, creator_id: user.id) } - let!(:deploy_keys_project) { create(:deploy_keys_project, project: project) } - let(:admin) { create(:admin) } - - describe 'GET /deploy_keys' do - before { admin } - - context 'when unauthenticated' do - it 'should return authentication error' do - get api('/deploy_keys') - expect(response.status).to eq(401) - end - end - - context 'when authenticated as non-admin user' do - it 'should return a 403 error' do - get api('/deploy_keys', user) - expect(response.status).to eq(403) - end - end - - context 'when authenticated as admin' do - it 'should return all deploy keys' do - get api('/deploy_keys', admin) - expect(response.status).to eq(200) - - expect(json_response).to be_an Array - expect(json_response.first['id']).to eq(deploy_keys_project.deploy_key.id) - end - end - end -end diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb new file mode 100644 index 00000000000000..315b2f08e877a9 --- /dev/null +++ b/spec/requests/api/deploy_keys_spec.rb @@ -0,0 +1,84 @@ +require 'spec_helper' + +describe API::API, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:admin) { create(:admin) } + let(:project) { create(:project, creator_id: user.id) } + let!(:deploy_keys_project) { create(:deploy_keys_project, project: project) } + let(:deploy_key) { deploy_keys_project.deploy_key } + + describe 'GET /deploy_keys' do + context 'when unauthenticated' do + it 'should return authentication error' do + get api('/deploy_keys') + + expect(response.status).to eq(401) + end + end + + context 'when authenticated as non-admin user' do + it 'should return a 403 error' do + get api('/deploy_keys', user) + + expect(response.status).to eq(403) + end + end + + context 'when authenticated as admin' do + + it 'should return all deploy keys' do + get api('/deploy_keys', admin) + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['id']).to eq(deploy_keys_project.deploy_key.id) + end + end + end + + describe 'POST /projects/:id/deploy_keys/:key_id/enable' do + let(:project2) { create(:empty_project) } + + context 'when the user can admin the project' do + it 'enables the key' do + expect do + post api("/projects/#{project2.id}/deploy_keys/#{deploy_key.id}/enable", admin) + end.to change { project2.deploy_keys.count }.from(0).to(1) + + expect(response).to have_http_status(201) + expect(json_response['id']).to eq(deploy_key.id) + end + end + + context 'when authenticated as non-admin user' do + it 'should return a 404 error' do + post api("/projects/#{project2.id}/deploy_keys/#{deploy_key.id}/enable", user) + + expect(response).to have_http_status(404) + end + end + end + + describe 'DELETE /projects/:id/deploy_keys/:key_id/disable' do + context 'when the user can admin the project' do + it 'disables the key' do + expect do + delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}/disable", admin) + end.to change { project.deploy_keys.count }.from(1).to(0) + + expect(response).to have_http_status(200) + expect(json_response['id']).to eq(deploy_key.id) + end + end + + context 'when authenticated as non-admin user' do + it 'should return a 404 error' do + delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}/disable", user) + + expect(response).to have_http_status(404) + end + end + end +end -- GitLab From 405379bbfcb7821b3dae77e5254362f2d696bb7d Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Fri, 15 Jul 2016 13:19:29 +0100 Subject: [PATCH 025/153] Store OTP secret key in secrets.yml .secret stores the secret token used for both encrypting login cookies and for encrypting stored OTP secrets. We can't rotate this, because that would invalidate all existing OTP secrets. If the secret token is present in the .secret file or an environment variable, save it as otp_key_base in secrets.yml. Now .secret can be rotated without invalidating OTP secrets. If the secret token isn't present (initial setup), then just generate a separate otp_key_base and save in secrets.yml. Update the docs to reflect that secrets.yml needs to be retained past upgrades, but .secret doesn't. --- CHANGELOG | 1 + app/models/user.rb | 4 +- config/initializers/secret_token.rb | 81 ++++++++------- doc/raketasks/backup_restore.md | 27 ++--- doc/raketasks/user_management.md | 4 +- spec/initializers/secret_token_spec.rb | 135 +++++++++++++++++++++++++ 6 files changed, 199 insertions(+), 53 deletions(-) create mode 100644 spec/initializers/secret_token_spec.rb diff --git a/CHANGELOG b/CHANGELOG index 472faa05b75255..f61c4d78433e17 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -133,6 +133,7 @@ v 8.10.0 - Add API "deploy_keys" for admins to get all deploy keys - Allow to pull code with deploy key from public projects - Use limit parameter rather than hardcoded value in `ldap:check` rake task (Mike Ricketts) + - Store OTP secret key in secrets.yml with other DB encryption keys - Add Sidekiq queue duration to transaction metrics. - Add a new column `artifacts_size` to table `ci_builds`. !4964 - Let Workhorse serve format-patch diffs diff --git a/app/models/user.rb b/app/models/user.rb index db7474349598bc..73368be7b1b176 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -23,13 +23,13 @@ class User < ActiveRecord::Base default_value_for :theme_id, gitlab_config.default_theme attr_encrypted :otp_secret, - key: Gitlab::Application.config.secret_key_base, + key: Gitlab::Application.secrets.otp_key_base, mode: :per_attribute_iv_and_salt, insecure_mode: true, algorithm: 'aes-256-cbc' devise :two_factor_authenticatable, - otp_secret_encryption_key: Gitlab::Application.config.secret_key_base + otp_secret_encryption_key: Gitlab::Application.secrets.otp_key_base devise :two_factor_backupable, otp_number_of_backup_codes: 10 serialize :otp_backup_codes, JSON diff --git a/config/initializers/secret_token.rb b/config/initializers/secret_token.rb index dae3a4a9a93f26..40c93c32dca551 100644 --- a/config/initializers/secret_token.rb +++ b/config/initializers/secret_token.rb @@ -2,49 +2,58 @@ require 'securerandom' -# Your secret key for verifying the integrity of signed cookies. -# If you change this key, all old signed cookies will become invalid! -# Make sure the secret is at least 30 characters and all random, -# no regular words or you'll be exposed to dictionary attacks. - -def find_secure_token - token_file = Rails.root.join('.secret') - if ENV.key?('SECRET_KEY_BASE') - ENV['SECRET_KEY_BASE'] - elsif File.exist? token_file - # Use the existing token. - File.read(token_file).chomp - else - # Generate a new token of 64 random hexadecimal characters and store it in token_file. - token = SecureRandom.hex(64) - File.write(token_file, token) - token - end -end - -Rails.application.config.secret_token = find_secure_token -Rails.application.config.secret_key_base = find_secure_token - -# CI def generate_new_secure_token SecureRandom.hex(64) end -if Rails.application.secrets.db_key_base.blank? - warn "Missing `db_key_base` for '#{Rails.env}' environment. The secrets will be generated and stored in `config/secrets.yml`" +def warn_missing_secret(secret) + warn "Missing `#{secret}` for '#{Rails.env}' environment. The secret will be generated and stored in `config/secrets.yml`" +end + +def create_tokens + secret_file = Rails.root.join('.secret') + file_key = File.read(secret_file).chomp if File.exist?(secret_file) + env_key = ENV['SECRET_KEY_BASE'] + secret_key_base = env_key.present? ? env_key : file_key + + if secret_key_base.blank? + secret_key_base = generate_new_secure_token + File.write(secret_file, secret_key_base) + end + + Rails.application.config.secret_key_base = secret_key_base - all_secrets = YAML.load_file('config/secrets.yml') if File.exist?('config/secrets.yml') - all_secrets ||= {} + otp_key_base = Rails.application.secrets.otp_key_base + db_key_base = Rails.application.secrets.db_key_base + yaml_additions = {} - # generate secrets - env_secrets = all_secrets[Rails.env.to_s] || {} - env_secrets['db_key_base'] ||= generate_new_secure_token - all_secrets[Rails.env.to_s] = env_secrets + if otp_key_base.blank? + warn_missing_secret('otp_key_base') - # save secrets - File.open('config/secrets.yml', 'w', 0600) do |file| - file.write(YAML.dump(all_secrets)) + otp_key_base ||= env_key || file_key || generate_new_secure_token + yaml_additions['otp_key_base'] = otp_key_base end - Rails.application.secrets.db_key_base = env_secrets['db_key_base'] + Rails.application.secrets.otp_key_base = otp_key_base + + if db_key_base.blank? + warn_missing_secret('db_key_base') + + yaml_additions['db_key_base'] = db_key_base = generate_new_secure_token + end + + Rails.application.secrets.db_key_base = db_key_base + + unless yaml_additions.empty? + secrets_yml = Rails.root.join('config/secrets.yml') + all_secrets = YAML.load_file(secrets_yml) if File.exist?(secrets_yml) + all_secrets ||= {} + + env_secrets = all_secrets[Rails.env.to_s] || {} + all_secrets[Rails.env.to_s] = env_secrets.merge(yaml_additions) + + File.write(secrets_yml, YAML.dump(all_secrets), mode: 'w', perm: 0600) + end end + +create_tokens diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 5fa96736d59147..b48a3ea00f4f2b 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -11,12 +11,13 @@ You can only restore a backup to exactly the same version of GitLab that you cre on, for example 7.2.1. The best way to migrate your repositories from one server to another is through backup restore. -You need to keep a separate copy of `/etc/gitlab/gitlab-secrets.json` -(for omnibus packages) or `/home/git/gitlab/.secret` (for installations -from source). This file contains the database encryption key used -for two-factor authentication. If you restore a GitLab backup without -restoring the database encryption key, users who have two-factor -authentication enabled will lose access to your GitLab server. +You need to keep a separate copy of `/etc/gitlab/gitlab-secrets.json` (for +omnibus packages) or `/home/git/gitlab/config/secrets.yml` (for installations +from source). This file contains the database encryption keys used for +two-factor authentication and project import credentials, among other things. If +you restore a GitLab backup without restoring the database encryption key, users +who have two-factor authentication enabled will lose access to your GitLab +server. ``` # use this command if you've installed GitLab with the Omnibus package @@ -221,10 +222,10 @@ of using encryption in the first place! If you use an Omnibus package please see the [instructions in the readme to backup your configuration](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#backup-and-restore-omnibus-gitlab-configuration). If you have a cookbook installation there should be a copy of your configuration in Chef. -If you have an installation from source, please consider backing up your `.secret` file, `gitlab.yml` file, any SSL keys and certificates, and your [SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079). +If you have an installation from source, please consider backing up your `config/secrets.yml` file, `gitlab.yml` file, any SSL keys and certificates, and your [SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079). At the very **minimum** you should backup `/etc/gitlab/gitlab-secrets.json` -(Omnibus) or `/home/git/gitlab/.secret` (source) to preserve your +(Omnibus) or `/home/git/gitlab/config/secrets.yml` (source) to preserve your database encryption key. ## Restore a previously created backup @@ -240,11 +241,11 @@ the SQL database it needs to import data into ('gitlabhq_production'). All existing data will be either erased (SQL) or moved to a separate directory (repositories, uploads). -If some or all of your GitLab users are using two-factor authentication -(2FA) then you must also make sure to restore -`/etc/gitlab/gitlab-secrets.json` (Omnibus) or `/home/git/gitlab/.secret` -(installations from source). Note that you need to run `gitlab-ctl -reconfigure` after changing `gitlab-secrets.json`. +If some or all of your GitLab users are using two-factor authentication (2FA) +then you must also make sure to restore `/etc/gitlab/gitlab-secrets.json` +(Omnibus) or `/home/git/gitlab/config/secrets.yml` (installations from +source). Note that you need to run `gitlab-ctl reconfigure` after changing +`gitlab-secrets.json`. ### Installation from source diff --git a/doc/raketasks/user_management.md b/doc/raketasks/user_management.md index 629d38efc53550..8a5e2d6e16bfe9 100644 --- a/doc/raketasks/user_management.md +++ b/doc/raketasks/user_management.md @@ -60,8 +60,8 @@ block_auto_created_users: false ## Disable Two-factor Authentication (2FA) for all users This task will disable 2FA for all users that have it enabled. This can be -useful if GitLab's `.secret` file has been lost and users are unable to login, -for example. +useful if GitLab's `config/secrets.yml` file has been lost and users are unable +to login, for example. ```bash # omnibus-gitlab diff --git a/spec/initializers/secret_token_spec.rb b/spec/initializers/secret_token_spec.rb new file mode 100644 index 00000000000000..063d1cdd447927 --- /dev/null +++ b/spec/initializers/secret_token_spec.rb @@ -0,0 +1,135 @@ +require 'spec_helper' +require_relative '../../config/initializers/secret_token' + +describe 'create_tokens', lib: true do + let(:config) { ActiveSupport::OrderedOptions.new } + let(:secrets) { ActiveSupport::OrderedOptions.new } + + before do + allow(ENV).to receive(:[]).and_call_original + allow(File).to receive(:write) + allow(Rails).to receive_message_chain(:application, :config).and_return(config) + allow(Rails).to receive_message_chain(:application, :secrets).and_return(secrets) + allow(Rails).to receive_message_chain(:root, :join) { |string| string } + end + + context 'setting otp_key_base' do + context 'when none of the secrets exist' do + before do + allow(ENV).to receive(:[]).with('SECRET_KEY_BASE').and_return(nil) + allow(File).to receive(:exist?).with('.secret').and_return(false) + allow(File).to receive(:exist?).with('config/secrets.yml').and_return(false) + allow(File).to receive(:write) + allow(self).to receive(:warn_missing_secret) + end + + it 'generates different secrets for secret_key_base, otp_key_base, and db_key_base' do + create_tokens + + keys = [config.secret_key_base, secrets.otp_key_base, secrets.db_key_base] + + expect(keys.uniq).to eq(keys) + expect(keys.map(&:length)).to all(eq(128)) + end + + it 'warns about the secrets to add to secrets.yml' do + expect(self).to receive(:warn_missing_secret).with('otp_key_base') + expect(self).to receive(:warn_missing_secret).with('db_key_base') + + create_tokens + end + + it 'writes the secrets to secrets.yml' do + expect(File).to receive(:write).with('config/secrets.yml', any_args) do |filename, contents, options| + new_secrets_yml = YAML.load(contents) + + expect(new_secrets_yml['test']['otp_key_base']).to eq(secrets.otp_key_base) + expect(new_secrets_yml['test']['db_key_base']).to eq(secrets.db_key_base) + end + + create_tokens + end + + it 'writes the secret_key_base to .secret' do + secret_key_base = nil + + expect(File).to receive(:write).with('.secret', any_args) do |filename, contents| + secret_key_base = contents + end + + create_tokens + + expect(secret_key_base).to eq(config.secret_key_base) + end + end + + context 'when the other secrets all exist' do + before do + secrets.db_key_base = 'db_key_base' + + allow(ENV).to receive(:[]).with('SECRET_KEY_BASE').and_return('env_key') + allow(File).to receive(:exist?).with('.secret').and_return(true) + allow(File).to receive(:read).with('.secret').and_return('file_key') + end + + context 'when the otp_key_base secret exists' do + before { secrets.otp_key_base = 'otp_key_base' } + + it 'does not write any files' do + expect(File).not_to receive(:write) + + create_tokens + end + + it 'does not generate any new keys' do + expect(SecureRandom).not_to receive(:hex) + + create_tokens + end + + it 'sets the the keys to the values from the environment and secrets.yml' do + create_tokens + + expect(config.secret_key_base).to eq('env_key') + expect(secrets.otp_key_base).to eq('otp_key_base') + expect(secrets.db_key_base).to eq('db_key_base') + end + end + + context 'when the otp_key_base secret does not exist' do + before do + allow(File).to receive(:exist?).with('config/secrets.yml').and_return(true) + allow(YAML).to receive(:load_file).with('config/secrets.yml').and_return('test' => secrets.to_h.stringify_keys) + allow(self).to receive(:warn_missing_secret) + end + + it 'uses the env secret' do + expect(SecureRandom).not_to receive(:hex) + expect(File).to receive(:write) do |filename, contents, options| + new_secrets_yml = YAML.load(contents) + + expect(new_secrets_yml['test']['otp_key_base']).to eq('env_key') + expect(new_secrets_yml['test']['db_key_base']).to eq('db_key_base') + end + + create_tokens + + expect(secrets.otp_key_base).to eq('env_key') + end + + it 'keeps the other secrets as they were' do + create_tokens + + expect(config.secret_key_base).to eq('env_key') + expect(secrets.db_key_base).to eq('db_key_base') + end + + it 'warns about the missing secret' do + expect(self).to receive(:warn_missing_secret).with('otp_key_base') + + create_tokens + end + end + end + end +end -- GitLab From 379c2cbcbd1544a1f80135c491937dabb04821df Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Sun, 17 Jul 2016 11:01:38 +0100 Subject: [PATCH 026/153] Store all secret keys in secrets.yml Move the last secret from .secret to config/secrets.yml, and delete .secret if it exists. --- CHANGELOG | 2 +- config/initializers/secret_token.rb | 40 ++++++--------- spec/initializers/secret_token_spec.rb | 69 ++++++++++++++------------ 3 files changed, 53 insertions(+), 58 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index f61c4d78433e17..057b4c083e6bda 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -23,6 +23,7 @@ v 8.11.0 (unreleased) - Retrieve rendered HTML from cache in one request - Fix renaming repository when name contains invalid chararacters under project settings - Optimize checking if a user has read access to a list of issues !5370 + - Store all DB secrets in secrets.yml, under descriptive names !5274 - Nokogiri's various parsing methods are now instrumented - Add simple identifier to public SSH keys (muteor) - Add a way to send an email and create an issue based on private personal token. Find the email address from issues page. !3363 @@ -133,7 +134,6 @@ v 8.10.0 - Add API "deploy_keys" for admins to get all deploy keys - Allow to pull code with deploy key from public projects - Use limit parameter rather than hardcoded value in `ldap:check` rake task (Mike Ricketts) - - Store OTP secret key in secrets.yml with other DB encryption keys - Add Sidekiq queue duration to transaction metrics. - Add a new column `artifacts_size` to table `ci_builds`. !4964 - Let Workhorse serve format-patch diffs diff --git a/config/initializers/secret_token.rb b/config/initializers/secret_token.rb index 40c93c32dca551..ac99dcb59fc2e0 100644 --- a/config/initializers/secret_token.rb +++ b/config/initializers/secret_token.rb @@ -14,36 +14,22 @@ def create_tokens secret_file = Rails.root.join('.secret') file_key = File.read(secret_file).chomp if File.exist?(secret_file) env_key = ENV['SECRET_KEY_BASE'] - secret_key_base = env_key.present? ? env_key : file_key - - if secret_key_base.blank? - secret_key_base = generate_new_secure_token - File.write(secret_file, secret_key_base) - end - - Rails.application.config.secret_key_base = secret_key_base - - otp_key_base = Rails.application.secrets.otp_key_base - db_key_base = Rails.application.secrets.db_key_base yaml_additions = {} - if otp_key_base.blank? - warn_missing_secret('otp_key_base') - - otp_key_base ||= env_key || file_key || generate_new_secure_token - yaml_additions['otp_key_base'] = otp_key_base - end - - Rails.application.secrets.otp_key_base = otp_key_base + defaults = { + secret_key_base: env_key || file_key || generate_new_secure_token, + otp_key_base: env_key || file_key || generate_new_secure_token, + db_key_base: generate_new_secure_token + } - if db_key_base.blank? - warn_missing_secret('db_key_base') + defaults.stringify_keys.each do |key, default| + if Rails.application.secrets[key].blank? + warn_missing_secret(key) - yaml_additions['db_key_base'] = db_key_base = generate_new_secure_token + yaml_additions[key] = Rails.application.secrets[key] = default + end end - Rails.application.secrets.db_key_base = db_key_base - unless yaml_additions.empty? secrets_yml = Rails.root.join('config/secrets.yml') all_secrets = YAML.load_file(secrets_yml) if File.exist?(secrets_yml) @@ -54,6 +40,12 @@ def create_tokens File.write(secrets_yml, YAML.dump(all_secrets), mode: 'w', perm: 0600) end + + begin + File.delete(secret_file) if file_key + rescue => e + warn "Error deleting useless .secret file: #{e}" + end end create_tokens diff --git a/spec/initializers/secret_token_spec.rb b/spec/initializers/secret_token_spec.rb index 063d1cdd447927..aa97a8e1d42cc1 100644 --- a/spec/initializers/secret_token_spec.rb +++ b/spec/initializers/secret_token_spec.rb @@ -2,37 +2,36 @@ require_relative '../../config/initializers/secret_token' describe 'create_tokens', lib: true do - let(:config) { ActiveSupport::OrderedOptions.new } let(:secrets) { ActiveSupport::OrderedOptions.new } before do allow(ENV).to receive(:[]).and_call_original allow(File).to receive(:write) - allow(Rails).to receive_message_chain(:application, :config).and_return(config) + allow(File).to receive(:delete) allow(Rails).to receive_message_chain(:application, :secrets).and_return(secrets) allow(Rails).to receive_message_chain(:root, :join) { |string| string } end - context 'setting otp_key_base' do + context 'setting secret_key_base and otp_key_base' do context 'when none of the secrets exist' do before do allow(ENV).to receive(:[]).with('SECRET_KEY_BASE').and_return(nil) allow(File).to receive(:exist?).with('.secret').and_return(false) allow(File).to receive(:exist?).with('config/secrets.yml').and_return(false) - allow(File).to receive(:write) allow(self).to receive(:warn_missing_secret) end it 'generates different secrets for secret_key_base, otp_key_base, and db_key_base' do create_tokens - keys = [config.secret_key_base, secrets.otp_key_base, secrets.db_key_base] + keys = secrets.values_at(:secret_key_base, :otp_key_base, :db_key_base) expect(keys.uniq).to eq(keys) expect(keys.map(&:length)).to all(eq(128)) end it 'warns about the secrets to add to secrets.yml' do + expect(self).to receive(:warn_missing_secret).with('secret_key_base') expect(self).to receive(:warn_missing_secret).with('otp_key_base') expect(self).to receive(:warn_missing_secret).with('db_key_base') @@ -41,25 +40,20 @@ it 'writes the secrets to secrets.yml' do expect(File).to receive(:write).with('config/secrets.yml', any_args) do |filename, contents, options| - new_secrets_yml = YAML.load(contents) + new_secrets = YAML.load(contents)[Rails.env] - expect(new_secrets_yml['test']['otp_key_base']).to eq(secrets.otp_key_base) - expect(new_secrets_yml['test']['db_key_base']).to eq(secrets.db_key_base) + expect(new_secrets['secret_key_base']).to eq(secrets.secret_key_base) + expect(new_secrets['otp_key_base']).to eq(secrets.otp_key_base) + expect(new_secrets['db_key_base']).to eq(secrets.db_key_base) end create_tokens end - it 'writes the secret_key_base to .secret' do - secret_key_base = nil - - expect(File).to receive(:write).with('.secret', any_args) do |filename, contents| - secret_key_base = contents - end + it 'does not write a .secret file' do + expect(File).not_to receive(:write).with('.secret') create_tokens - - expect(secret_key_base).to eq(config.secret_key_base) end end @@ -72,8 +66,11 @@ allow(File).to receive(:read).with('.secret').and_return('file_key') end - context 'when the otp_key_base secret exists' do - before { secrets.otp_key_base = 'otp_key_base' } + context 'when secret_key_base and otp_key_base exist' do + before do + secrets.secret_key_base = 'secret_key_base' + secrets.otp_key_base = 'otp_key_base' + end it 'does not write any files' do expect(File).not_to receive(:write) @@ -81,22 +78,22 @@ create_tokens end - it 'does not generate any new keys' do - expect(SecureRandom).not_to receive(:hex) - - create_tokens - end - - it 'sets the the keys to the values from the environment and secrets.yml' do + it 'sets the the keys to the values from secrets.yml' do create_tokens - expect(config.secret_key_base).to eq('env_key') + expect(secrets.secret_key_base).to eq('secret_key_base') expect(secrets.otp_key_base).to eq('otp_key_base') expect(secrets.db_key_base).to eq('db_key_base') end + + it 'deletes the .secret file' do + expect(File).to receive(:delete).with('.secret') + + create_tokens + end end - context 'when the otp_key_base secret does not exist' do + context 'when secret_key_base and otp_key_base do not exist' do before do allow(File).to receive(:exist?).with('config/secrets.yml').and_return(true) allow(YAML).to receive(:load_file).with('config/secrets.yml').and_return('test' => secrets.to_h.stringify_keys) @@ -104,12 +101,12 @@ end it 'uses the env secret' do - expect(SecureRandom).not_to receive(:hex) expect(File).to receive(:write) do |filename, contents, options| - new_secrets_yml = YAML.load(contents) + new_secrets = YAML.load(contents)[Rails.env] - expect(new_secrets_yml['test']['otp_key_base']).to eq('env_key') - expect(new_secrets_yml['test']['db_key_base']).to eq('db_key_base') + expect(new_secrets['secret_key_base']).to eq('env_key') + expect(new_secrets['otp_key_base']).to eq('env_key') + expect(new_secrets['db_key_base']).to eq('db_key_base') end create_tokens @@ -120,15 +117,21 @@ it 'keeps the other secrets as they were' do create_tokens - expect(config.secret_key_base).to eq('env_key') expect(secrets.db_key_base).to eq('db_key_base') end - it 'warns about the missing secret' do + it 'warns about the missing secrets' do + expect(self).to receive(:warn_missing_secret).with('secret_key_base') expect(self).to receive(:warn_missing_secret).with('otp_key_base') create_tokens end + + it 'deletes the .secret file' do + expect(File).to receive(:delete).with('.secret') + + create_tokens + end end end end -- GitLab From 90565b5f95ce3d6d0b81078fe9fa9a9f196b4cde Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Tue, 19 Jul 2016 15:12:40 +0100 Subject: [PATCH 027/153] Give priority to environment variables If an environment variable exists for secret_key_base, use that - always. But don't save it to secrets.yml. Also ensure that we never write to secrets.yml if there's a non-blank value there. --- config/initializers/secret_token.rb | 24 +++++++-- spec/initializers/secret_token_spec.rb | 74 +++++++++++++++++++++++--- 2 files changed, 88 insertions(+), 10 deletions(-) diff --git a/config/initializers/secret_token.rb b/config/initializers/secret_token.rb index ac99dcb59fc2e0..5d8124b30b211e 100644 --- a/config/initializers/secret_token.rb +++ b/config/initializers/secret_token.rb @@ -7,7 +7,7 @@ def generate_new_secure_token end def warn_missing_secret(secret) - warn "Missing `#{secret}` for '#{Rails.env}' environment. The secret will be generated and stored in `config/secrets.yml`" + warn "Missing Rails.application.secrets.#{secret} for #{Rails.env} environment. The secret will be generated and stored in config/secrets.yml." end def create_tokens @@ -16,8 +16,11 @@ def create_tokens env_key = ENV['SECRET_KEY_BASE'] yaml_additions = {} + # Ensure environment variable always overrides secrets.yml. + Rails.application.secrets.secret_key_base = env_key if env_key.present? + defaults = { - secret_key_base: env_key || file_key || generate_new_secure_token, + secret_key_base: file_key || generate_new_secure_token, otp_key_base: env_key || file_key || generate_new_secure_token, db_key_base: generate_new_secure_token } @@ -34,9 +37,22 @@ def create_tokens secrets_yml = Rails.root.join('config/secrets.yml') all_secrets = YAML.load_file(secrets_yml) if File.exist?(secrets_yml) all_secrets ||= {} - env_secrets = all_secrets[Rails.env.to_s] || {} - all_secrets[Rails.env.to_s] = env_secrets.merge(yaml_additions) + + all_secrets[Rails.env.to_s] = env_secrets.merge(yaml_additions) do |key, old, new| + if old.present? + warn < '<%= an_erb_expression %>') + + allow(File).to receive(:exist?).with('.secret').and_return(false) + allow(File).to receive(:exist?).with('config/secrets.yml').and_return(true) + allow(YAML).to receive(:load_file).with('config/secrets.yml').and_return('test' => yaml_secrets) + allow(self).to receive(:warn_missing_secret) + end + + it 'warns about updating db_key_base' do + expect(self).to receive(:warn_missing_secret).with('db_key_base') + + create_tokens + end + + it 'warns about the blank value existing in secrets.yml and exits' do + expect(self).to receive(:warn) do |warning| + expect(warning).to include('db_key_base') + expect(warning).to include('<%= an_erb_expression %>') + end + + create_tokens + end + + it 'does not update secrets.yml' do + expect(self).to receive(:exit).with(1).and_call_original + expect(File).not_to receive(:write) + + expect { create_tokens }.to raise_error(SystemExit) + end + end end end -- GitLab From 732ad2f6c1cfe126c0b2080c6e8d0fe3e77c4d1e Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Wed, 20 Jul 2016 15:10:28 +0100 Subject: [PATCH 028/153] Clarify intentions of secret token initializer --- config/initializers/secret_token.rb | 90 ++++++++++++++++++----------- 1 file changed, 55 insertions(+), 35 deletions(-) diff --git a/config/initializers/secret_token.rb b/config/initializers/secret_token.rb index 5d8124b30b211e..7ab71b1c2b9aae 100644 --- a/config/initializers/secret_token.rb +++ b/config/initializers/secret_token.rb @@ -2,66 +2,86 @@ require 'securerandom' -def generate_new_secure_token - SecureRandom.hex(64) -end - -def warn_missing_secret(secret) - warn "Missing Rails.application.secrets.#{secret} for #{Rails.env} environment. The secret will be generated and stored in config/secrets.yml." -end - +# Transition material in .secret to the secret_key_base key in config/secrets.yml. +# Historically, ENV['SECRET_KEY_BASE'] takes precedence over .secret, so we maintain that +# behavior. +# +# It also used to be the case that the key material in ENV['SECRET_KEY_BASE'] or .secret +# was used to encrypt OTP (two-factor authentication) data so if present, we copy that key +# material into config/secrets.yml under otp_key_base. +# +# Finally, if we have successfully migrated all secrets to config/secrets.yml, delete the +# .secret file to avoid confusion. +# def create_tokens secret_file = Rails.root.join('.secret') - file_key = File.read(secret_file).chomp if File.exist?(secret_file) - env_key = ENV['SECRET_KEY_BASE'] - yaml_additions = {} + file_secret_key = File.read(secret_file).chomp if File.exist?(secret_file) + env_secret_key = ENV['SECRET_KEY_BASE'] # Ensure environment variable always overrides secrets.yml. - Rails.application.secrets.secret_key_base = env_key if env_key.present? + Rails.application.secrets.secret_key_base = env_secret_key if env_secret_key.present? defaults = { - secret_key_base: file_key || generate_new_secure_token, - otp_key_base: env_key || file_key || generate_new_secure_token, + secret_key_base: file_secret_key || generate_new_secure_token, + otp_key_base: env_secret_key || file_secret_key || generate_new_secure_token, db_key_base: generate_new_secure_token } - defaults.stringify_keys.each do |key, default| + missing_secrets = set_missing_keys(defaults) + write_secrets_yml(missing_secrets) unless missing_secrets.empty? + + begin + File.delete(secret_file) if file_secret_key + rescue => e + warn "Error deleting useless .secret file: #{e}" + end +end + +def generate_new_secure_token + SecureRandom.hex(64) +end + +def warn_missing_secret(secret) + warn "Missing Rails.application.secrets.#{secret} for #{Rails.env} environment. The secret will be generated and stored in config/secrets.yml." +end + +def set_missing_keys(defaults) + defaults.stringify_keys.each_with_object({}) do |(key, default), missing| if Rails.application.secrets[key].blank? warn_missing_secret(key) - yaml_additions[key] = Rails.application.secrets[key] = default + missing[key] = Rails.application.secrets[key] = default end end +end - unless yaml_additions.empty? - secrets_yml = Rails.root.join('config/secrets.yml') - all_secrets = YAML.load_file(secrets_yml) if File.exist?(secrets_yml) - all_secrets ||= {} - env_secrets = all_secrets[Rails.env.to_s] || {} - - all_secrets[Rails.env.to_s] = env_secrets.merge(yaml_additions) do |key, old, new| - if old.present? - warn < e - warn "Error deleting useless .secret file: #{e}" - end + File.write(secrets_yml, YAML.dump(secrets), mode: 'w', perm: 0600) end create_tokens -- GitLab From 5fedd7d1fa482b99ec8f73eb7ff866550bbd5188 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Wed, 20 Jul 2016 17:31:51 +0100 Subject: [PATCH 029/153] Mention that gitlab.rb needs to be kept --- doc/raketasks/backup_restore.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index b48a3ea00f4f2b..40cc21f8f89866 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -11,13 +11,13 @@ You can only restore a backup to exactly the same version of GitLab that you cre on, for example 7.2.1. The best way to migrate your repositories from one server to another is through backup restore. -You need to keep a separate copy of `/etc/gitlab/gitlab-secrets.json` (for -omnibus packages) or `/home/git/gitlab/config/secrets.yml` (for installations -from source). This file contains the database encryption keys used for -two-factor authentication and project import credentials, among other things. If -you restore a GitLab backup without restoring the database encryption key, users -who have two-factor authentication enabled will lose access to your GitLab -server. +You need to keep separate copies of `/etc/gitlab/gitlab-secrets.json` and +`/etc/gitlab/gitlab.rb` (for omnibus packages) or +`/home/git/gitlab/config/secrets.yml` (for installations from source). This file +contains the database encryption keys used for two-factor authentication and +project import credentials, among other things. If you restore a GitLab backup +without restoring the database encryption key, users who have two-factor +authentication enabled will lose access to your GitLab server. ``` # use this command if you've installed GitLab with the Omnibus package -- GitLab From f1a3e8a8be99a9694f4ca007511e824061301e52 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 23 Jul 2016 19:37:34 -0500 Subject: [PATCH 030/153] Convert shortcuts image into markdown --- doc/workflow/shortcuts.md | 73 ++++++++++++++++++++++++++++++++++++- doc/workflow/shortcuts.png | Bin 108209 -> 0 bytes 2 files changed, 72 insertions(+), 1 deletion(-) delete mode 100644 doc/workflow/shortcuts.png diff --git a/doc/workflow/shortcuts.md b/doc/workflow/shortcuts.md index ffcb832cdd7999..36516883ef6234 100644 --- a/doc/workflow/shortcuts.md +++ b/doc/workflow/shortcuts.md @@ -2,4 +2,75 @@ You can see GitLab's keyboard shortcuts by using 'shift + ?' -![Shortcuts](shortcuts.png) \ No newline at end of file +## Global Shortcuts + +| Keyboard Shortcut | Description | +| ----------------- | ----------- | +| s | Focus search | +| ? | Show/hide this dialog | +| + shift + p | Toggle markdown preview | +| | Edit last comment (when focused on an empty textarea) | + +## Project Files Browsing + +| Keyboard Shortcut | Description | +| ----------------- | ----------- | +| | Move selection up | +| | Move selection down | +| enter | Open selection | + +## Finding Project File + +| Keyboard Shortcut | Description | +| ----------------- | ----------- | +| | Move selection up | +| | Move selection down | +| enter | Open selection | +| esc | Go back | + +## Global Dashboard + +| Keyboard Shortcut | Description | +| ----------------- | ----------- | +| g + a | Go to the activity feed | +| g + p | Go to projects | +| g + i | Go to issues | +| g + m | Go to merge requests | + +## Project + +| Keyboard Shortcut | Description | +| ----------------- | ----------- | +| g + p | Go to the project's home page | +| g + e | Go to the project's activity feed | +| g + f | Go to files | +| g + c | Go to commits | +| g + b | Go to builds | +| g + n | Go to network graph | +| g + g | Go to graphs | +| g + i | Go to issues | +| g + m | Go to merge requests | +| g + s | Go to snippets | +| t | Go to finding file | +| i | New issue | + +## Network Graph + +| Keyboard Shortcut | Description | +| ----------------- | ----------- | +| or h | Scroll left | +| or l | Scroll right | +| or k | Scroll up | +| or j | Scroll down | +| shift + or shift + k | Scroll to top | +| shift + or shift + j | Scroll to bottom | + +## Issues and Merge Requests + +| Keyboard Shortcut | Description | +| ----------------- | ----------- | +| a | Change assignee | +| m | Change milestone | +| r | Reply (quoting selected text) | +| e | Edit issue/merge request | +| l | Change label | \ No newline at end of file diff --git a/doc/workflow/shortcuts.png b/doc/workflow/shortcuts.png deleted file mode 100644 index a9b1c4b4dccf119a33781fd5e839e3dc4d6a808f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 108209 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⁢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 Date: Wed, 3 Aug 2016 16:45:35 +0200 Subject: [PATCH 031/153] Use Grape DSL for deploy keys endpoints Also a minor clean up of the post endpoint --- lib/api/deploy_keys.rb | 73 +++++++++++++-------------- spec/requests/api/deploy_keys_spec.rb | 1 - 2 files changed, 34 insertions(+), 40 deletions(-) diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb index ab4868eca2db3b..6a0be345667806 100644 --- a/lib/api/deploy_keys.rb +++ b/lib/api/deploy_keys.rb @@ -10,6 +10,9 @@ class DeployKeys < Grape::API present keys, with: Entities::SSHKey end + params do + requires :id, type: String, desc: 'The ID of the project' + end resource :projects do before { authorize_admin_project } @@ -17,52 +20,42 @@ class DeployKeys < Grape::API # Use "projects/:id/deploy_keys/..." instead. # %w(keys deploy_keys).each do |path| - # Get a specific project's deploy keys - # - # Example Request: - # GET /projects/:id/deploy_keys + desc "Get a specific project's deploy keys" do + success Entities::SSHKey + end get ":id/#{path}" do present user_project.deploy_keys, with: Entities::SSHKey end - # Get single deploy key owned by currently authenticated user - # - # Example Request: - # GET /projects/:id/deploy_keys/:key_id + desc 'Get single deploy key' do + success Entities::SSHKey + end + params do + requires :key_id, type: Integer, desc: 'The ID of the deploy key' + end get ":id/#{path}/:key_id" do key = user_project.deploy_keys.find params[:key_id] present key, with: Entities::SSHKey end - # Add new deploy key to currently authenticated user - # If deploy key already exists - it will be joined to project - # but only if original one was accessible by same user - # - # Parameters: - # key (required) - New deploy Key - # title (required) - New deploy Key's title - # Example Request: - # POST /projects/:id/deploy_keys + desc 'Add new deploy key to currently authenticated user' do + success Entities::SSHKey + end + params do + requires :key, type: String, desc: "The new deploy key" + requires :title, type: String, desc: 'The title to identify the key from' + end post ":id/#{path}" do - attrs = attributes_for_keys [:title, :key] - - if attrs[:key].present? - attrs[:key].strip! + attrs = declared(params, include_parent_namespaces: false).to_h - # check if key already exist in project - key = user_project.deploy_keys.find_by(key: attrs[:key]) - if key - present key, with: Entities::SSHKey - next - end + key = user_project.deploy_keys.find_by(key: attrs[:key]) + present key, with: Entities::SSHKey if key - # Check for available deploy keys in other projects - key = current_user.accessible_deploy_keys.find_by(key: attrs[:key]) - if key - user_project.deploy_keys << key - present key, with: Entities::SSHKey - next - end + # Check for available deploy keys in other projects + key = current_user.accessible_deploy_keys.find_by(key: attrs[:key]) + if key + user_project.deploy_keys << key + present key, with: Entities::SSHKey end key = DeployKey.new attrs @@ -105,12 +98,14 @@ class DeployKeys < Grape::API present key.deploy_key, with: Entities::SSHKey end - # Delete existing deploy key of currently authenticated user - # - # Example Request: - # DELETE /projects/:id/deploy_keys/:key_id + desc 'Delete existing deploy key of currently authenticated user' do + success Key + end + params do + requires :key_id, type: Integer, desc: 'The ID of the deploy key' + end delete ":id/#{path}/:key_id" do - key = user_project.deploy_keys.find params[:key_id] + key = user_project.deploy_keys.find(params[:key_id]) key.destroy end end diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb index 315b2f08e877a9..664ff072f9b559 100644 --- a/spec/requests/api/deploy_keys_spec.rb +++ b/spec/requests/api/deploy_keys_spec.rb @@ -27,7 +27,6 @@ end context 'when authenticated as admin' do - it 'should return all deploy keys' do get api('/deploy_keys', admin) -- GitLab From c19fa02fa020d4b7aa69b3bbc51a4a257163325b Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Thu, 4 Aug 2016 10:17:35 +0100 Subject: [PATCH 032/153] Ignore Rails/Exit cop in initializer We do not want to proceed with loading the app in this case, as it could lose a secret needed to decrypt values in the database. --- config/initializers/secret_token.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/initializers/secret_token.rb b/config/initializers/secret_token.rb index 7ab71b1c2b9aae..291fa6c0abcfd0 100644 --- a/config/initializers/secret_token.rb +++ b/config/initializers/secret_token.rb @@ -75,7 +75,7 @@ def write_secrets_yml(missing_secrets) This probably isn't the expected value for this secret. To keep using a literal Erb string in config/secrets.yml, replace `<%` with `<%%`. EOM - exit 1 + exit 1 # rubocop:disable Rails/Exit end new -- GitLab From e55e224cd9d5dfb3fc351374a0cd4620f8e845b9 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 4 Aug 2016 15:22:34 +0200 Subject: [PATCH 033/153] Fix ArgumentError in GitAccess specs --- spec/lib/gitlab/git_access_spec.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index 8447305a316df1..f12c9a370f741c 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -19,11 +19,11 @@ def disable_protocol(protocol) end it 'blocks ssh git push' do - expect(@acc.check('git-receive-pack').allowed?).to be_falsey + expect(@acc.check('git-receive-pack', '_any').allowed?).to be_falsey end it 'blocks ssh git pull' do - expect(@acc.check('git-upload-pack').allowed?).to be_falsey + expect(@acc.check('git-upload-pack', '_any').allowed?).to be_falsey end end @@ -34,17 +34,17 @@ def disable_protocol(protocol) end it 'blocks http push' do - expect(@acc.check('git-receive-pack').allowed?).to be_falsey + expect(@acc.check('git-receive-pack', '_any').allowed?).to be_falsey end it 'blocks http git pull' do - expect(@acc.check('git-upload-pack').allowed?).to be_falsey + expect(@acc.check('git-upload-pack', '_any').allowed?).to be_falsey end end end describe 'download_access_check' do - subject { access.check('git-upload-pack') } + subject { access.check('git-upload-pack', '_any') } describe 'master permissions' do before { project.team << [user, :master] } @@ -288,7 +288,7 @@ def self.run_permission_checks(permissions_matrix) let(:actor) { key } context 'push code' do - subject { access.check('git-receive-pack') } + subject { access.check('git-receive-pack', '_any') } context 'when project is authorized' do before { key.projects << project } -- GitLab From 2785a56e7d1968dfda03850a14af296d71b06503 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Thu, 4 Aug 2016 15:41:37 +0100 Subject: [PATCH 034/153] Mention that gitlab.rb must be kept, too --- doc/raketasks/backup_restore.md | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 40cc21f8f89866..835af5443a329b 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -14,10 +14,10 @@ another is through backup restore. You need to keep separate copies of `/etc/gitlab/gitlab-secrets.json` and `/etc/gitlab/gitlab.rb` (for omnibus packages) or `/home/git/gitlab/config/secrets.yml` (for installations from source). This file -contains the database encryption keys used for two-factor authentication and -project import credentials, among other things. If you restore a GitLab backup -without restoring the database encryption key, users who have two-factor -authentication enabled will lose access to your GitLab server. +contains the database encryption keys used for two-factor authentication and CI +secret variables, among other things. If you restore a GitLab backup without +restoring the database encryption key, users who have two-factor authentication +enabled will lose access to your GitLab server. ``` # use this command if you've installed GitLab with the Omnibus package @@ -224,9 +224,10 @@ If you use an Omnibus package please see the [instructions in the readme to back If you have a cookbook installation there should be a copy of your configuration in Chef. If you have an installation from source, please consider backing up your `config/secrets.yml` file, `gitlab.yml` file, any SSL keys and certificates, and your [SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079). -At the very **minimum** you should backup `/etc/gitlab/gitlab-secrets.json` -(Omnibus) or `/home/git/gitlab/config/secrets.yml` (source) to preserve your -database encryption key. +At the very **minimum** you should backup `/etc/gitlab/gitlab.rb` and +`/etc/gitlab/gitlab-secrets.json` (Omnibus), or +`/home/git/gitlab/config/secrets.yml` (source) to preserve your database +encryption key. ## Restore a previously created backup @@ -242,10 +243,10 @@ All existing data will be either erased (SQL) or moved to a separate directory (repositories, uploads). If some or all of your GitLab users are using two-factor authentication (2FA) -then you must also make sure to restore `/etc/gitlab/gitlab-secrets.json` -(Omnibus) or `/home/git/gitlab/config/secrets.yml` (installations from -source). Note that you need to run `gitlab-ctl reconfigure` after changing -`gitlab-secrets.json`. +then you must also make sure to restore `/etc/gitlab/gitlab.rb` and +`/etc/gitlab/gitlab-secrets.json` (Omnibus), or +`/home/git/gitlab/config/secrets.yml` (installations from source). Note that you +need to run `gitlab-ctl reconfigure` after changing `gitlab-secrets.json`. ### Installation from source -- GitLab From 460065b743a5f28d92bda71b0e778b01cd369d80 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Thu, 4 Aug 2016 13:09:10 +0200 Subject: [PATCH 035/153] Move deploy_key tests to deploy_key_spec.rb Also, fix the failing test in the process --- lib/api/deploy_keys.rb | 11 ++-- spec/requests/api/deploy_keys_spec.rb | 74 +++++++++++++++++++++++++++ spec/requests/api/projects_spec.rb | 74 +-------------------------- 3 files changed, 81 insertions(+), 78 deletions(-) diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb index 6a0be345667806..52f89373ad3010 100644 --- a/lib/api/deploy_keys.rb +++ b/lib/api/deploy_keys.rb @@ -38,15 +38,16 @@ class DeployKeys < Grape::API present key, with: Entities::SSHKey end + # TODO: for 9.0 we should check if params are there with the params block + # grape provides, at this point we'd change behaviour so we can't + # Behaviour now if you don't provide all required params: it renders a + # validation error or two. desc 'Add new deploy key to currently authenticated user' do success Entities::SSHKey end - params do - requires :key, type: String, desc: "The new deploy key" - requires :title, type: String, desc: 'The title to identify the key from' - end post ":id/#{path}" do - attrs = declared(params, include_parent_namespaces: false).to_h + attrs = attributes_for_keys [:title, :key] + attrs[:key].strip! if attrs[:key] key = user_project.deploy_keys.find_by(key: attrs[:key]) present key, with: Entities::SSHKey if key diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb index 664ff072f9b559..7e7a2733f3c1ce 100644 --- a/spec/requests/api/deploy_keys_spec.rb +++ b/spec/requests/api/deploy_keys_spec.rb @@ -37,6 +37,80 @@ end end + describe 'GET /projects/:id/deploy_keys' do + before { deploy_key } + + it 'should return array of ssh keys' do + get api("/projects/#{project.id}/deploy_keys", admin) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['title']).to eq(deploy_key.title) + end + end + + describe 'GET /projects/:id/deploy_keys/:key_id' do + it 'should return a single key' do + get api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin) + + expect(response).to have_http_status(200) + expect(json_response['title']).to eq(deploy_key.title) + end + + it 'should return 404 Not Found with invalid ID' do + get api("/projects/#{project.id}/deploy_keys/404", admin) + + expect(response).to have_http_status(404) + end + end + + describe 'POST /projects/:id/deploy_keys' do + it 'should not create an invalid ssh key' do + post api("/projects/#{project.id}/deploy_keys", admin), { title: 'invalid key' } + + expect(response).to have_http_status(400) + expect(json_response['message']['key']).to eq([ + 'can\'t be blank', + 'is too short (minimum is 0 characters)', + 'is invalid' + ]) + end + + it 'should not create a key without title' do + post api("/projects/#{project.id}/deploy_keys", admin), key: 'some key' + + expect(response).to have_http_status(400) + expect(json_response['message']['title']).to eq([ + 'can\'t be blank', + 'is too short (minimum is 0 characters)' + ]) + end + + it 'should create new ssh key' do + key_attrs = attributes_for :another_key + + expect do + post api("/projects/#{project.id}/deploy_keys", admin), key_attrs + end.to change{ project.deploy_keys.count }.by(1) + end + end + + describe 'DELETE /projects/:id/deploy_keys/:key_id' do + before { deploy_key } + + it 'should delete existing key' do + expect do + delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin) + end.to change{ project.deploy_keys.count }.by(-1) + end + + it 'should return 404 Not Found with invalid ID' do + delete api("/projects/#{project.id}/deploy_keys/404", admin) + + expect(response).to have_http_status(404) + end + end + describe 'POST /projects/:id/deploy_keys/:key_id/enable' do let(:project2) { create(:empty_project) } diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 8c6a7e6529d0f5..6b78326213b16a 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -641,79 +641,7 @@ expect(response).to have_http_status(404) end end - - describe :deploy_keys do - let(:deploy_keys_project) { create(:deploy_keys_project, project: project) } - let(:deploy_key) { deploy_keys_project.deploy_key } - - describe 'GET /projects/:id/deploy_keys' do - before { deploy_key } - - it 'should return array of ssh keys' do - get api("/projects/#{project.id}/deploy_keys", user) - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['title']).to eq(deploy_key.title) - end - end - - describe 'GET /projects/:id/deploy_keys/:key_id' do - it 'should return a single key' do - get api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", user) - expect(response).to have_http_status(200) - expect(json_response['title']).to eq(deploy_key.title) - end - - it 'should return 404 Not Found with invalid ID' do - get api("/projects/#{project.id}/deploy_keys/404", user) - expect(response).to have_http_status(404) - end - end - - describe 'POST /projects/:id/deploy_keys' do - it 'should not create an invalid ssh key' do - post api("/projects/#{project.id}/deploy_keys", user), { title: 'invalid key' } - expect(response).to have_http_status(400) - expect(json_response['message']['key']).to eq([ - 'can\'t be blank', - 'is too short (minimum is 0 characters)', - 'is invalid' - ]) - end - - it 'should not create a key without title' do - post api("/projects/#{project.id}/deploy_keys", user), key: 'some key' - expect(response).to have_http_status(400) - expect(json_response['message']['title']).to eq([ - 'can\'t be blank', - 'is too short (minimum is 0 characters)' - ]) - end - - it 'should create new ssh key' do - key_attrs = attributes_for :key - expect do - post api("/projects/#{project.id}/deploy_keys", user), key_attrs - end.to change{ project.deploy_keys.count }.by(1) - end - end - - describe 'DELETE /projects/:id/deploy_keys/:key_id' do - before { deploy_key } - - it 'should delete existing key' do - expect do - delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", user) - end.to change{ project.deploy_keys.count }.by(-1) - end - - it 'should return 404 Not Found with invalid ID' do - delete api("/projects/#{project.id}/deploy_keys/404", user) - expect(response).to have_http_status(404) - end - end - end - + describe :fork_admin do let(:project_fork_target) { create(:project) } let(:project_fork_source) { create(:project, :public) } -- GitLab From 926cee002d701548b5344e0b93e95beb3802fd54 Mon Sep 17 00:00:00 2001 From: Gabriel Mazetto Date: Mon, 30 May 2016 18:13:42 -0300 Subject: [PATCH 036/153] Deduplicated resque.yml loading from several places We will trust redis configuration params loading to Gitlab::RedisConfig. --- config/application.rb | 3 +- config/initializers/session_store.rb | 4 +- config/mail_room.yml | 49 ++++++---------- config/resque.yml.example | 34 ++++++++++- doc/install/installation.md | 6 +- lib/gitlab/mail_room.rb | 44 +++++++++++++++ lib/gitlab/redis.rb | 84 +++++++++++++++++++--------- 7 files changed, 159 insertions(+), 65 deletions(-) create mode 100644 lib/gitlab/mail_room.rb diff --git a/config/application.rb b/config/application.rb index 06ebb14a5fea25..4a9ed41cbf88a7 100644 --- a/config/application.rb +++ b/config/application.rb @@ -107,7 +107,8 @@ class Application < Rails::Application end end - redis_config_hash = Gitlab::Redis.redis_store_options + # Use Redis caching across all environments + redis_config_hash = Gitlab::Redis.params redis_config_hash[:namespace] = Gitlab::Redis::CACHE_NAMESPACE redis_config_hash[:expires_in] = 2.weeks # Cache should not grow forever config.cache_store = :redis_store, redis_config_hash diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb index 0d9d87bac00a31..70be2617cabf22 100644 --- a/config/initializers/session_store.rb +++ b/config/initializers/session_store.rb @@ -13,9 +13,9 @@ if Rails.env.test? Gitlab::Application.config.session_store :cookie_store, key: "_gitlab_session" else - redis_config = Gitlab::Redis.redis_store_options + redis_config = Gitlab::Redis.params redis_config[:namespace] = Gitlab::Redis::SESSION_NAMESPACE - + Gitlab::Application.config.session_store( :redis_store, # Using the cookie_store would enable session replay attacks. servers: redis_config, diff --git a/config/mail_room.yml b/config/mail_room.yml index 7cab24b295e863..d93795bbd94408 100644 --- a/config/mail_room.yml +++ b/config/mail_room.yml @@ -1,47 +1,32 @@ :mailboxes: -<% -require "yaml" -require "json" -require_relative "lib/gitlab/redis" unless defined?(Gitlab::Redis) + <% + require_relative "lib/gitlab/mail_room" unless defined?(Gitlab::MailRoom) + config = Gitlab::MailRoom.config -rails_env = ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development" - -config_file = ENV["MAIL_ROOM_GITLAB_CONFIG_FILE"] || "config/gitlab.yml" -if File.exists?(config_file) - all_config = YAML.load_file(config_file)[rails_env] - - config = all_config["incoming_email"] || {} - config['enabled'] = false if config['enabled'].nil? - config['port'] = 143 if config['port'].nil? - config['ssl'] = false if config['ssl'].nil? - config['start_tls'] = false if config['start_tls'].nil? - config['mailbox'] = "inbox" if config['mailbox'].nil? - - if config['enabled'] && config['address'] - redis_url = Gitlab::Redis.new(rails_env).url - %> + if Gitlab::Mailroom.enabled? + %> - - :host: <%= config['host'].to_json %> - :port: <%= config['port'].to_json %> - :ssl: <%= config['ssl'].to_json %> - :start_tls: <%= config['start_tls'].to_json %> - :email: <%= config['user'].to_json %> - :password: <%= config['password'].to_json %> + :host: <%= config[:host].to_json %> + :port: <%= config[:port].to_json %> + :ssl: <%= config[:ssl].to_json %> + :start_tls: <%= config[:start_tls].to_json %> + :email: <%= config[:user].to_json %> + :password: <%= config[:password].to_json %> - :name: <%= config['mailbox'].to_json %> + :name: <%= config[:mailbox].to_json %> :delete_after_delivery: true :delivery_method: sidekiq :delivery_options: - :redis_url: <%= redis_url.to_json %> - :namespace: resque:gitlab + :redis_url: <%= config[:redis_url].to_json %> + :namespace: <%= Gitlab::Redis::SIDEKIQ_NAMESPACE %> :queue: incoming_email :worker: EmailReceiverWorker :arbitration_method: redis :arbitration_options: - :redis_url: <%= redis_url.to_json %> - :namespace: mail_room:gitlab + :redis_url: <%= config[:redis_url].to_json %> + :namespace: <%= Gitlab::Redis::MAILROOM_NAMESPACE %> + <% end %> -<% end %> diff --git a/config/resque.yml.example b/config/resque.yml.example index d98f43f71b2987..753c3308aa5d6d 100644 --- a/config/resque.yml.example +++ b/config/resque.yml.example @@ -1,6 +1,34 @@ # If you change this file in a Merge Request, please also create # a Merge Request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests # -development: redis://localhost:6379 -test: redis://localhost:6379 -production: unix:/var/run/redis/redis.sock +development: + url: redis://localhost:6379 + sentinels: + - + host: localhost + port: 26380 # point to sentinel, not to redis port + - + host: slave2 + port: 26381 # point to sentinel, not to redis port +test: + url: redis://localhost:6379 +production: + # Redis (single instance) + url: unix:/var/run/redis/redis.sock + ## + # Redis + Sentinel (for HA) + # + # Please read instructions carefully before using it as you may loose data: + # http://redis.io/topics/sentinel + # + # You must specify a list of a few sentinels that will handle client connection + # please read here for more information: https://github.com/redis/redis-rb#sentinel-support + ## + #url: redis://master:6379 + # sentinels: + # - + # host: slave1 + # port: 26379 # point to sentinel, not to redis port + # - + # host: slave2 + # port: 26379 # point to sentinel, not to redis port diff --git a/doc/install/installation.md b/doc/install/installation.md index af8e31a705bfeb..3e77880eca0ba3 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -591,12 +591,14 @@ for the changes to take effect. If you'd like Resque to connect to a Redis server on a non-standard port or on a different host, you can configure its connection string via the `config/resque.yml` file. # example - production: redis://redis.example.tld:6379 + production: + url: redis://redis.example.tld:6379 If you want to connect the Redis server via socket, then use the "unix:" URL scheme and the path to the Redis socket file in the `config/resque.yml` file. # example - production: unix:/path/to/redis/socket + production: + url: unix:/path/to/redis/socket ### Custom SSH Connection diff --git a/lib/gitlab/mail_room.rb b/lib/gitlab/mail_room.rb new file mode 100644 index 00000000000000..745cc79a10ee77 --- /dev/null +++ b/lib/gitlab/mail_room.rb @@ -0,0 +1,44 @@ +require 'yaml' +require 'json' +require_relative 'lib/gitlab/redis' unless defined?(Gitlab::Redis) + +module Gitlab + module MailRoom + + class << self + + def enabled? + config[:enabled] && config[:address] + end + + def config + @config ||= fetch_config + end + + private + + def fetch_config + return nil unless File.exists?(config_file) + + rails_env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development' + all_config = YAML.load_file(config_file)[rails_env].deep_symbolize_keys + + config = all_config[:incoming_email] || {} + config[:enabled] = false if config[:enabled].nil? + config[:port] = 143 if config[:port].nil? + config[:ssl] = false if config[:ssl].nil? + config[:start_tls] = false if config[:start_tls].nil? + config[:mailbox] = 'inbox' if config[:mailbox].nil? + + if config[:enabled] && config[:address] + config[:redis_url] = Gitlab::Redis.new(rails_env).url + end + end + + def config_file + file = ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] || 'config/gitlab.yml' + File.expand_path("../../../#{file}", __FILE__) + end + end + end +end diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb index 40766f35f77984..8050b28f33ac09 100644 --- a/lib/gitlab/redis.rb +++ b/lib/gitlab/redis.rb @@ -1,50 +1,84 @@ +# This file should not have any direct dependency on Rails environment +# please require all dependencies below: +require 'active_support/core_ext/hash/keys' + module Gitlab class Redis CACHE_NAMESPACE = 'cache:gitlab' SESSION_NAMESPACE = 'session:gitlab' SIDEKIQ_NAMESPACE = 'resque:gitlab' - - attr_reader :url + MAILROOM_NAMESPACE = 'mail_room:gitlab' + DEFAULT_REDIS_URL = 'redis://localhost:6379' # To be thread-safe we must be careful when writing the class instance # variables @url and @pool. Because @pool depends on @url we need two # mutexes to prevent deadlock. - URL_MUTEX = Mutex.new + PARAMS_MUTEX = Mutex.new POOL_MUTEX = Mutex.new - private_constant :URL_MUTEX, :POOL_MUTEX + private_constant :PARAMS_MUTEX, :POOL_MUTEX - def self.url - @url || URL_MUTEX.synchronize { @url = new.url } - end + class << self + def params + @params || PARAMS_MUTEX.synchronize { @params = new.params } + end + + # @deprecated Use .params instead to get sentinel support + def url + raw_config_hash[:url] + end - def self.with - if @pool.nil? - POOL_MUTEX.synchronize do - @pool = ConnectionPool.new { ::Redis.new(url: url) } + def with + if @pool.nil? + POOL_MUTEX.synchronize do + @pool = ConnectionPool.new { ::Redis.new(params) } + end end + @pool.with { |redis| yield redis } end - @pool.with { |redis| yield redis } end - def self.redis_store_options - url = new.url - redis_config_hash = ::Redis::Store::Factory.extract_host_options_from_uri(url) - # Redis::Store does not handle Unix sockets well, so let's do it for them - redis_uri = URI.parse(url) + def initialize(rails_env=nil) + @rails_env = rails_env || Rails.env + end + + def params + redis_store_options + end + + private + + def redis_store_options + config = raw_config_hash + + redis_uri = URI.parse(config[:url]) if redis_uri.scheme == 'unix' - redis_config_hash[:path] = redis_uri.path + # Redis::Store does not handle Unix sockets well, so let's do it for them + config[:path] = redis_uri.path + config.delete(:url) + else + redis_hash = ::Redis::Store::Factory.extract_host_options_from_uri(redis_uri) + config.merge!(redis_hash) end - redis_config_hash + + config end - def initialize(rails_env=nil) - rails_env ||= Rails.env - config_file = File.expand_path('../../../config/resque.yml', __FILE__) + def raw_config_hash + config_data = fetch_config - @url = "redis://localhost:6379" - if File.exist?(config_file) - @url = YAML.load_file(config_file)[rails_env] + if config_data + config_data.is_a?(String) ? { url: config_data } : config_data.deep_symbolize_keys + else + { url: DEFAULT_REDIS_URL } end end + + def fetch_config + File.exists?(config_file) ? YAML.load_file(config_file)[@rails_env] : false + end + + def config_file + File.expand_path('../../../config/resque.yml', __FILE__) + end end end -- GitLab From ef6043880ee492e6c8fd7672d0e36ca81a62cfc9 Mon Sep 17 00:00:00 2001 From: Gabriel Mazetto Date: Fri, 27 Nov 2015 14:32:32 -0200 Subject: [PATCH 037/153] Specs for RedisConfig Make sure :url is not present on RedisConfig.params after parsing --- lib/gitlab/redis.rb | 2 +- .../fixtures/config/redis_new_format_host.yml | 29 +++++++ .../config/redis_new_format_socket.yml | 6 ++ .../fixtures/config/redis_old_format_host.yml | 5 ++ .../config/redis_old_format_socket.yml | 3 + spec/lib/gitlab/redis_config_spec.rb | 81 +++++++++++++++++++ 6 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 spec/fixtures/config/redis_new_format_host.yml create mode 100644 spec/fixtures/config/redis_new_format_socket.yml create mode 100644 spec/fixtures/config/redis_old_format_host.yml create mode 100644 spec/fixtures/config/redis_old_format_socket.yml create mode 100644 spec/lib/gitlab/redis_config_spec.rb diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb index 8050b28f33ac09..8c277d97f1a76e 100644 --- a/lib/gitlab/redis.rb +++ b/lib/gitlab/redis.rb @@ -54,12 +54,12 @@ def redis_store_options if redis_uri.scheme == 'unix' # Redis::Store does not handle Unix sockets well, so let's do it for them config[:path] = redis_uri.path - config.delete(:url) else redis_hash = ::Redis::Store::Factory.extract_host_options_from_uri(redis_uri) config.merge!(redis_hash) end + config.delete(:url) config end diff --git a/spec/fixtures/config/redis_new_format_host.yml b/spec/fixtures/config/redis_new_format_host.yml new file mode 100644 index 00000000000000..5e5bf6e0ccd612 --- /dev/null +++ b/spec/fixtures/config/redis_new_format_host.yml @@ -0,0 +1,29 @@ +# redis://[:password@]host[:port][/db-number][?option=value] +# more details: http://www.iana.org/assignments/uri-schemes/prov/redis +development: + url: redis://:mypassword@localhost:6379/99 + sentinels: + - + host: localhost + port: 26380 # point to sentinel, not to redis port + - + host: slave2 + port: 26381 # point to sentinel, not to redis port +test: + url: redis://:mypassword@localhost:6379/99 + sentinels: + - + host: localhost + port: 26380 # point to sentinel, not to redis port + - + host: slave2 + port: 26381 # point to sentinel, not to redis port +production: + url: redis://:mypassword@localhost:6379/99 + sentinels: + - + host: slave1 + port: 26380 # point to sentinel, not to redis port + - + host: slave2 + port: 26381 # point to sentinel, not to redis port diff --git a/spec/fixtures/config/redis_new_format_socket.yml b/spec/fixtures/config/redis_new_format_socket.yml new file mode 100644 index 00000000000000..4e76830c281de7 --- /dev/null +++ b/spec/fixtures/config/redis_new_format_socket.yml @@ -0,0 +1,6 @@ +development: + url: unix:/path/to/redis.sock +test: + url: unix:/path/to/redis.sock +production: + url: unix:/path/to/redis.sock diff --git a/spec/fixtures/config/redis_old_format_host.yml b/spec/fixtures/config/redis_old_format_host.yml new file mode 100644 index 00000000000000..253d0a994f5e6f --- /dev/null +++ b/spec/fixtures/config/redis_old_format_host.yml @@ -0,0 +1,5 @@ +# redis://[:password@]host[:port][/db-number][?option=value] +# more details: http://www.iana.org/assignments/uri-schemes/prov/redis +development: redis://:mypassword@localhost:6379/99 +test: redis://:mypassword@localhost:6379/99 +production: redis://:mypassword@localhost:6379/99 diff --git a/spec/fixtures/config/redis_old_format_socket.yml b/spec/fixtures/config/redis_old_format_socket.yml new file mode 100644 index 00000000000000..cb39b6fb9e230c --- /dev/null +++ b/spec/fixtures/config/redis_old_format_socket.yml @@ -0,0 +1,3 @@ +development: unix:/path/to/redis.sock +test: unix:/path/to/redis.sock +production: unix:/path/to/redis.sock diff --git a/spec/lib/gitlab/redis_config_spec.rb b/spec/lib/gitlab/redis_config_spec.rb new file mode 100644 index 00000000000000..cb98dd6d994ae3 --- /dev/null +++ b/spec/lib/gitlab/redis_config_spec.rb @@ -0,0 +1,81 @@ +require 'spec_helper' + +describe Gitlab::RedisConfig do + let(:redis_config) { Rails.root.join('config', 'resque.yml') } + + describe '.params' do + subject { described_class.params } + + context 'when url contains unix socket reference' do + let(:config_old) { Rails.root.join('spec/fixtures/config/redis_old_format_socket.yml') } + let(:config_new) { Rails.root.join('spec/fixtures/config/redis_new_format_socket.yml') } + + context 'with old format' do + it 'returns path key instead' do + allow(Gitlab::RedisConfig).to receive(:config_file) { config_old } + + is_expected.to include(path: '/path/to/redis.sock') + is_expected.not_to have_key(:url) + end + end + + context 'with new format' do + it 'returns path key instead' do + allow(Gitlab::RedisConfig).to receive(:config_file) { config_new } + + is_expected.to include(path: '/path/to/redis.sock') + is_expected.not_to have_key(:url) + end + end + end + + context 'when url is host based' do + let(:config_old) { Rails.root.join('spec/fixtures/config/redis_old_format_host.yml') } + let(:config_new) { Rails.root.join('spec/fixtures/config/redis_new_format_host.yml') } + + context 'with old format' do + it 'returns hash with host, port, db, and password' do + allow(Gitlab::RedisConfig).to receive(:config_file) { config_old } + + is_expected.to include(host: 'localhost', password: 'mypassword', port: 6379, db: 99) + is_expected.not_to have_key(:url) + end + end + + context 'with new format' do + it 'returns hash with host, port, db, and password' do + allow(Gitlab::RedisConfig).to receive(:config_file) { config_new } + + is_expected.to include(host: 'localhost', password: 'mypassword', port: 6379, db: 99) + is_expected.not_to have_key(:url) + end + end + end + end + + describe '.raw_params' do + subject { described_class.send(:raw_params) } + + it 'returns default redis url when no config file is present' do + expect(Gitlab::RedisConfig).to receive(:fetch_config) { false } + + is_expected.to eq(url: Gitlab::RedisConfig::DEFAULT_REDIS_URL) + end + + it 'returns old-style single url config in a hash' do + expect(Gitlab::RedisConfig).to receive(:fetch_config) { 'redis://myredis:6379' } + is_expected.to eq(url: 'redis://myredis:6379') + end + + end + + describe '.fetch_config' do + subject { described_class.send(:fetch_config) } + + it 'returns false when no config file is present' do + allow(File).to receive(:exists?).with(redis_config) { false } + + is_expected.to be_falsey + end + end +end -- GitLab From 2625a4ccbd28805e8a657776ca09e080cc826d55 Mon Sep 17 00:00:00 2001 From: Gabriel Mazetto Date: Fri, 27 Nov 2015 18:41:21 -0200 Subject: [PATCH 038/153] Make sidekiq get config settings from Gitlab::RedisConfig --- config/initializers/sidekiq.rb | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index cf49ec2194c8e1..f7e714cd6bc785 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -1,8 +1,9 @@ +# Custom Redis configuration +redis_config_hash = Gitlab::Redis.params +redis_config_hash[:namespace] = Gitlab::Redis::SIDEKIQ_NAMESPACE + Sidekiq.configure_server do |config| - config.redis = { - url: Gitlab::Redis.url, - namespace: Gitlab::Redis::SIDEKIQ_NAMESPACE - } + config.redis = redis_config_hash config.server_middleware do |chain| chain.add Gitlab::SidekiqMiddleware::ArgumentsLogger if ENV['SIDEKIQ_LOG_ARGUMENTS'] @@ -39,8 +40,5 @@ end Sidekiq.configure_client do |config| - config.redis = { - url: Gitlab::Redis.url, - namespace: Gitlab::Redis::SIDEKIQ_NAMESPACE - } + config.redis = redis_config_hash end -- GitLab From eacdb1012032136f32bb0470e6f85714eb7fca6d Mon Sep 17 00:00:00 2001 From: Gabriel Mazetto Date: Mon, 30 May 2016 18:17:16 -0300 Subject: [PATCH 039/153] Reverted resque.yml -> redis.yml renaming --- doc/install/installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/install/installation.md b/doc/install/installation.md index 3e77880eca0ba3..acf294b397d972 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -588,7 +588,7 @@ for the changes to take effect. ### Custom Redis Connection -If you'd like Resque to connect to a Redis server on a non-standard port or on a different host, you can configure its connection string via the `config/resque.yml` file. +If you'd like to connect to a Redis server on a non-standard port or on a different host, you can configure its connection string via the `config/resque.yml` file. # example production: -- GitLab From 6f318795083ca3d3726bb6bf5f4dc4081cfba8bf Mon Sep 17 00:00:00 2001 From: Gabriel Mazetto Date: Fri, 8 Jul 2016 03:33:37 +0200 Subject: [PATCH 040/153] Fixed specs for Gitlab::Redis and code for Redis Sentinel support --- config/mail_room.yml | 2 +- lib/gitlab/mail_room.rb | 10 +++--- lib/gitlab/redis.rb | 9 ++--- .../{redis_config_spec.rb => redis_spec.rb} | 36 +++++++++---------- 4 files changed, 27 insertions(+), 30 deletions(-) rename spec/lib/gitlab/{redis_config_spec.rb => redis_spec.rb} (62%) diff --git a/config/mail_room.yml b/config/mail_room.yml index d93795bbd94408..febd5f7c1f158b 100644 --- a/config/mail_room.yml +++ b/config/mail_room.yml @@ -3,7 +3,7 @@ require_relative "lib/gitlab/mail_room" unless defined?(Gitlab::MailRoom) config = Gitlab::MailRoom.config - if Gitlab::Mailroom.enabled? + if Gitlab::MailRoom.enabled? %> - :host: <%= config[:host].to_json %> diff --git a/lib/gitlab/mail_room.rb b/lib/gitlab/mail_room.rb index 745cc79a10ee77..781c89579b68b7 100644 --- a/lib/gitlab/mail_room.rb +++ b/lib/gitlab/mail_room.rb @@ -1,12 +1,11 @@ require 'yaml' require 'json' -require_relative 'lib/gitlab/redis' unless defined?(Gitlab::Redis) +require_relative 'redis' unless defined?(Gitlab::Redis) module Gitlab module MailRoom class << self - def enabled? config[:enabled] && config[:address] end @@ -18,7 +17,7 @@ def config private def fetch_config - return nil unless File.exists?(config_file) + return {} unless File.exist?(config_file) rails_env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development' all_config = YAML.load_file(config_file)[rails_env].deep_symbolize_keys @@ -33,11 +32,12 @@ def fetch_config if config[:enabled] && config[:address] config[:redis_url] = Gitlab::Redis.new(rails_env).url end + + config end def config_file - file = ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] || 'config/gitlab.yml' - File.expand_path("../../../#{file}", __FILE__) + ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] || File.expand_path('../../../config/gitlab.yml', __FILE__) end end end diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb index 8c277d97f1a76e..9c4b01bfe1a161 100644 --- a/lib/gitlab/redis.rb +++ b/lib/gitlab/redis.rb @@ -19,7 +19,7 @@ class Redis class << self def params - @params || PARAMS_MUTEX.synchronize { @params = new.params } + PARAMS_MUTEX.synchronize { new.params } end # @deprecated Use .params instead to get sentinel support @@ -38,7 +38,7 @@ def with end def initialize(rails_env=nil) - @rails_env = rails_env || Rails.env + @rails_env = rails_env || ::Rails.env end def params @@ -55,7 +55,7 @@ def redis_store_options # Redis::Store does not handle Unix sockets well, so let's do it for them config[:path] = redis_uri.path else - redis_hash = ::Redis::Store::Factory.extract_host_options_from_uri(redis_uri) + redis_hash = ::Redis::Store::Factory.extract_host_options_from_uri(config[:url]) config.merge!(redis_hash) end @@ -74,7 +74,8 @@ def raw_config_hash end def fetch_config - File.exists?(config_file) ? YAML.load_file(config_file)[@rails_env] : false + file = config_file + File.exist?(file) ? YAML.load_file(file)[@rails_env] : false end def config_file diff --git a/spec/lib/gitlab/redis_config_spec.rb b/spec/lib/gitlab/redis_spec.rb similarity index 62% rename from spec/lib/gitlab/redis_config_spec.rb rename to spec/lib/gitlab/redis_spec.rb index cb98dd6d994ae3..04257f89627718 100644 --- a/spec/lib/gitlab/redis_config_spec.rb +++ b/spec/lib/gitlab/redis_spec.rb @@ -1,18 +1,18 @@ require 'spec_helper' -describe Gitlab::RedisConfig do - let(:redis_config) { Rails.root.join('config', 'resque.yml') } +describe Gitlab::Redis do + let(:redis_config) { Rails.root.join('config', 'resque.yml').to_s } describe '.params' do subject { described_class.params } context 'when url contains unix socket reference' do - let(:config_old) { Rails.root.join('spec/fixtures/config/redis_old_format_socket.yml') } - let(:config_new) { Rails.root.join('spec/fixtures/config/redis_new_format_socket.yml') } + let(:config_old) { Rails.root.join('spec/fixtures/config/redis_old_format_socket.yml').to_s } + let(:config_new) { Rails.root.join('spec/fixtures/config/redis_new_format_socket.yml').to_s } context 'with old format' do it 'returns path key instead' do - allow(Gitlab::RedisConfig).to receive(:config_file) { config_old } + expect_any_instance_of(described_class).to receive(:config_file) { config_old } is_expected.to include(path: '/path/to/redis.sock') is_expected.not_to have_key(:url) @@ -21,7 +21,7 @@ context 'with new format' do it 'returns path key instead' do - allow(Gitlab::RedisConfig).to receive(:config_file) { config_new } + expect_any_instance_of(described_class).to receive(:config_file) { config_new } is_expected.to include(path: '/path/to/redis.sock') is_expected.not_to have_key(:url) @@ -35,7 +35,7 @@ context 'with old format' do it 'returns hash with host, port, db, and password' do - allow(Gitlab::RedisConfig).to receive(:config_file) { config_old } + expect_any_instance_of(described_class).to receive(:config_file) { config_old } is_expected.to include(host: 'localhost', password: 'mypassword', port: 6379, db: 99) is_expected.not_to have_key(:url) @@ -44,7 +44,7 @@ context 'with new format' do it 'returns hash with host, port, db, and password' do - allow(Gitlab::RedisConfig).to receive(:config_file) { config_new } + expect_any_instance_of(described_class).to receive(:config_file) { config_new } is_expected.to include(host: 'localhost', password: 'mypassword', port: 6379, db: 99) is_expected.not_to have_key(:url) @@ -53,29 +53,25 @@ end end - describe '.raw_params' do - subject { described_class.send(:raw_params) } - + describe '#raw_config_hash' do it 'returns default redis url when no config file is present' do - expect(Gitlab::RedisConfig).to receive(:fetch_config) { false } + expect(subject).to receive(:fetch_config) { false } - is_expected.to eq(url: Gitlab::RedisConfig::DEFAULT_REDIS_URL) + expect(subject.send(:raw_config_hash)).to eq(url: Gitlab::Redis::DEFAULT_REDIS_URL) end it 'returns old-style single url config in a hash' do - expect(Gitlab::RedisConfig).to receive(:fetch_config) { 'redis://myredis:6379' } - is_expected.to eq(url: 'redis://myredis:6379') + expect(subject).to receive(:fetch_config) { 'redis://myredis:6379' } + expect(subject.send(:raw_config_hash)).to eq(url: 'redis://myredis:6379') end end - describe '.fetch_config' do - subject { described_class.send(:fetch_config) } - + describe '#fetch_config' do it 'returns false when no config file is present' do - allow(File).to receive(:exists?).with(redis_config) { false } + allow(File).to receive(:exist?).with(redis_config) { false } - is_expected.to be_falsey + expect(subject.send(:fetch_config)).to be_falsey end end end -- GitLab From 67ae8adc72c1b59c440e0bfbde31df4e0b9920ec Mon Sep 17 00:00:00 2001 From: Gabriel Mazetto Date: Sat, 9 Jul 2016 17:13:19 +0200 Subject: [PATCH 041/153] Fixed MailRoom specs and make sure it works with new resque.yml format Some codestyle changes --- lib/gitlab/mail_room.rb | 4 ++++ lib/gitlab/redis.rb | 6 ++++- spec/config/mail_room_spec.rb | 43 ++++++++++++++++------------------- 3 files changed, 28 insertions(+), 25 deletions(-) diff --git a/lib/gitlab/mail_room.rb b/lib/gitlab/mail_room.rb index 781c89579b68b7..b49cf1c633b873 100644 --- a/lib/gitlab/mail_room.rb +++ b/lib/gitlab/mail_room.rb @@ -14,6 +14,10 @@ def config @config ||= fetch_config end + def reload_config! + @config = fetch_config + end + private def fetch_config diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb index 9c4b01bfe1a161..70e333eb29f2b8 100644 --- a/lib/gitlab/redis.rb +++ b/lib/gitlab/redis.rb @@ -24,7 +24,7 @@ def params # @deprecated Use .params instead to get sentinel support def url - raw_config_hash[:url] + new.url end def with @@ -45,6 +45,10 @@ def params redis_store_options end + def url + raw_config_hash[:url] + end + private def redis_store_options diff --git a/spec/config/mail_room_spec.rb b/spec/config/mail_room_spec.rb index 6fad7e2b9e7700..2c988f1c883be0 100644 --- a/spec/config/mail_room_spec.rb +++ b/spec/config/mail_room_spec.rb @@ -1,53 +1,48 @@ -require "spec_helper" +require 'spec_helper' -describe "mail_room.yml" do - let(:config_path) { "config/mail_room.yml" } +describe 'mail_room.yml' do + let(:config_path) { 'config/mail_room.yml' } let(:configuration) { YAML.load(ERB.new(File.read(config_path)).result) } - context "when incoming email is disabled" do + context 'when incoming email is disabled' do before do - ENV["MAIL_ROOM_GITLAB_CONFIG_FILE"] = Rails.root.join("spec/fixtures/mail_room_disabled.yml").to_s + ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = Rails.root.join('spec/fixtures/mail_room_disabled.yml').to_s + Gitlab::MailRoom.reload_config! end after do - ENV["MAIL_ROOM_GITLAB_CONFIG_FILE"] = nil + ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = nil end - it "contains no configuration" do + it 'contains no configuration' do expect(configuration[:mailboxes]).to be_nil end end - context "when incoming email is enabled" do + context 'when incoming email is enabled' do before do - ENV["MAIL_ROOM_GITLAB_CONFIG_FILE"] = Rails.root.join("spec/fixtures/mail_room_enabled.yml").to_s + ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = Rails.root.join('spec/fixtures/mail_room_enabled.yml').to_s + Gitlab::MailRoom.reload_config! end after do - ENV["MAIL_ROOM_GITLAB_CONFIG_FILE"] = nil + ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = nil end - it "contains the intended configuration" do + it 'contains the intended configuration' do expect(configuration[:mailboxes].length).to eq(1) mailbox = configuration[:mailboxes].first - expect(mailbox[:host]).to eq("imap.gmail.com") + expect(mailbox[:host]).to eq('imap.gmail.com') expect(mailbox[:port]).to eq(993) expect(mailbox[:ssl]).to eq(true) expect(mailbox[:start_tls]).to eq(false) - expect(mailbox[:email]).to eq("gitlab-incoming@gmail.com") - expect(mailbox[:password]).to eq("[REDACTED]") - expect(mailbox[:name]).to eq("inbox") - - redis_config_file = Rails.root.join('config', 'resque.yml') - - redis_url = - if File.exist?(redis_config_file) - YAML.load_file(redis_config_file)[Rails.env] - else - "redis://localhost:6379" - end + expect(mailbox[:email]).to eq('gitlab-incoming@gmail.com') + expect(mailbox[:password]).to eq('[REDACTED]') + expect(mailbox[:name]).to eq('inbox') + + redis_url = Gitlab::Redis.url expect(mailbox[:delivery_options][:redis_url]).to eq(redis_url) expect(mailbox[:arbitration_options][:redis_url]).to eq(redis_url) -- GitLab From 3a93bae25f03a2992401e1de5bfbf52c3921b1a4 Mon Sep 17 00:00:00 2001 From: Gabriel Mazetto Date: Tue, 12 Jul 2016 17:10:03 +0200 Subject: [PATCH 042/153] Few minor fixes to Redis params order and commented out sentinel config in resque.yml.example Codestyle changes --- config/resque.yml.example | 32 ++++++++++++++++---------------- lib/gitlab/mail_room.rb | 1 - lib/gitlab/redis.rb | 13 +++++++------ 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/config/resque.yml.example b/config/resque.yml.example index 753c3308aa5d6d..20a1c07690f71c 100644 --- a/config/resque.yml.example +++ b/config/resque.yml.example @@ -3,13 +3,13 @@ # development: url: redis://localhost:6379 - sentinels: - - - host: localhost - port: 26380 # point to sentinel, not to redis port - - - host: slave2 - port: 26381 # point to sentinel, not to redis port + # sentinels: + # - + # host: localhost + # port: 26380 # point to sentinel, not to redis port + # - + # host: slave2 + # port: 26381 # point to sentinel, not to redis port test: url: redis://localhost:6379 production: @@ -18,17 +18,17 @@ production: ## # Redis + Sentinel (for HA) # - # Please read instructions carefully before using it as you may loose data: + # Please read instructions carefully before using it as you may lose data: # http://redis.io/topics/sentinel # # You must specify a list of a few sentinels that will handle client connection # please read here for more information: https://github.com/redis/redis-rb#sentinel-support ## - #url: redis://master:6379 - # sentinels: - # - - # host: slave1 - # port: 26379 # point to sentinel, not to redis port - # - - # host: slave2 - # port: 26379 # point to sentinel, not to redis port + # url: redis://master:6379 + # sentinels: + # - + # host: slave1 + # port: 26379 # point to sentinel, not to redis port + # - + # host: slave2 + # port: 26379 # point to sentinel, not to redis port diff --git a/lib/gitlab/mail_room.rb b/lib/gitlab/mail_room.rb index b49cf1c633b873..1f68e09fa2b7ac 100644 --- a/lib/gitlab/mail_room.rb +++ b/lib/gitlab/mail_room.rb @@ -4,7 +4,6 @@ module Gitlab module MailRoom - class << self def enabled? config[:enabled] && config[:address] diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb index 70e333eb29f2b8..17ac15a01dd40b 100644 --- a/lib/gitlab/redis.rb +++ b/lib/gitlab/redis.rb @@ -53,18 +53,19 @@ def url def redis_store_options config = raw_config_hash + redis_url = config.delete(:url) + redis_uri = URI.parse(redis_url) - redis_uri = URI.parse(config[:url]) if redis_uri.scheme == 'unix' # Redis::Store does not handle Unix sockets well, so let's do it for them config[:path] = redis_uri.path + config else - redis_hash = ::Redis::Store::Factory.extract_host_options_from_uri(config[:url]) - config.merge!(redis_hash) + redis_hash = ::Redis::Store::Factory.extract_host_options_from_uri(redis_url) + # order is important here, sentinels must be after the connection keys. + # {url: ..., port: ..., sentinels: [...]} + redis_hash.merge(config) end - - config.delete(:url) - config end def raw_config_hash -- GitLab From 32bb42119527891bbc5cf3b86af8a75b8ac28ed6 Mon Sep 17 00:00:00 2001 From: Gabriel Mazetto Date: Thu, 14 Jul 2016 03:33:00 +0200 Subject: [PATCH 043/153] Synced mail_room.yml with the one in omnibus Added a comment to remember developers to open merge request to omnibus in future changes of the file. --- config/mail_room.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config/mail_room.yml b/config/mail_room.yml index febd5f7c1f158b..c639f8260aa1b2 100644 --- a/config/mail_room.yml +++ b/config/mail_room.yml @@ -1,3 +1,6 @@ +# If you change this file in a Merge Request, please also create +# a Merge Request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests +# :mailboxes: <% require_relative "lib/gitlab/mail_room" unless defined?(Gitlab::MailRoom) @@ -12,6 +15,7 @@ :start_tls: <%= config[:start_tls].to_json %> :email: <%= config[:user].to_json %> :password: <%= config[:password].to_json %> + :idle_timeout: 60 :name: <%= config[:mailbox].to_json %> -- GitLab From a5c1e2e55e6345192cf01dc422c07b894195a0d2 Mon Sep 17 00:00:00 2001 From: Gabriel Mazetto Date: Thu, 14 Jul 2016 03:57:47 +0200 Subject: [PATCH 044/153] Added Redis Sentinel support documentation --- config/resque.yml.example | 2 +- doc/administration/high_availability/redis.md | 107 ++++++++++++++++++ 2 files changed, 108 insertions(+), 1 deletion(-) diff --git a/config/resque.yml.example b/config/resque.yml.example index 20a1c07690f71c..0c19d8bc1d36af 100644 --- a/config/resque.yml.example +++ b/config/resque.yml.example @@ -22,7 +22,7 @@ production: # http://redis.io/topics/sentinel # # You must specify a list of a few sentinels that will handle client connection - # please read here for more information: https://github.com/redis/redis-rb#sentinel-support + # please read here for more information: https://docs.gitlab.com/ce/administration/high_availability/redis.html ## # url: redis://master:6379 # sentinels: diff --git a/doc/administration/high_availability/redis.md b/doc/administration/high_availability/redis.md index f6153216f333a9..1adc681e98bc12 100644 --- a/doc/administration/high_availability/redis.md +++ b/doc/administration/high_availability/redis.md @@ -3,6 +3,113 @@ You can choose to install and manage Redis yourself, or you can use GitLab Omnibus packages to help. +## Experimental Redis Sentinel support + +Since 8.10 release, you can configure a list of Redis Sentinel servers that +will monitor a group of Redis servers to provide you with a standard failover +support. + +There is currently one exception to the Sentinel support: **mail_room**, the +component that process incoming emails. + +It doesn't support Sentinel yet, but we hope to integrate a future release +that does support it. + +To get a better understanding on how to correctly setup Sentinel, please read +[Redis Sentinel documentation](http://redis.io/topics/sentinel) first, as +faling to configure it correctly can lead to data-loss. + +### Redis setup + +You must have at least 2 Redis servers: 1 Master, 1 or more Slaves. +They should be configured the same way and with similar server specs, as +in a failover situation, any Slave can be elected as the new Master by +the Sentinels servers. + +In a minimal setup, the only required change for the slaves in `redis.conf` +is the addition of a `slaveof` line pointing to the initial master like this: + +```conf +slaveof 192.168.1.1 6379 +``` + +You can increase the security by defining a `requirepass` configuration in +the master: + +```conf +requirepass " +``` + +and adding this line to all the slave servers: + +```conf +masterauth "" +``` + +> **Note** This setup is not safe to be used by a machine accessible by the +internet. Use it in combination with tight firewall rules. + +### Sentinel setup + +The support for Sentinel in ruby have some [caveats](https://github.com/redis/redis-rb/issues/531). +While you can give any name for the `master-group-name` part of the +configuration, as in this example: + +```conf +sentinel monitor ` +``` + +For it to work in ruby, you have to use the "hostname" of the master redis +server otherwhise you will get an error message like this one: +`Redis::CannotConnectError: No sentinels available.`. + + +Here is an example configuration file (`sentinel.conf`) for a Sentinel node: + +```conf +port 26379 +sentinel monitor master-redis.example.com 10.10.10.10 6379 1 +sentinel down-after-milliseconds master-redis.example.com 10000 +sentinel config-epoch master-redis.example.com 0 +sentinel leader-epoch locmaster-redis.example.comalhost 0 +``` + +### GitLab setup + +You can enable or disable sentinel support at any time in new or existing +installs. From the GitLab application perspective, all it requires is +the correct credentials for the Master redis and for a few Sentinels nodes. + +It doesn't require a list of all sentinel nodes, as in case of a failure, +the application will need to query only one of them. + +For a source based install, you must change `/home/git/gitlab/config/resque.yml`, +following the example in `/home/git/gitlab/config/resque.yml.example` and +uncommenting the sentinels line, changing to the correct server credentials, +and resstart GitLab. + +For a Omnibus install you have to add/change this lines from the +`/etc/gitlab/gitlab.rb` configuration file: + +```ruby +gitlab['gitlab-rails']['redis_host'] = "master-redis.example.com" +gitlab['gitlab-rails']['redis_port'] = 6379 +gitlab['gitlab-rails']['redis_password'] = "redis-secure-password-here" +gitlab['gitlab-rails']['redis_socket'] = nil +gitlab['gitlab-rails']['redis_sentinels'] = [ + {'host' => '10.10.10.1', 'port' => 26379}, + {'host' => '10.10.10.2', 'port' => 26379}, + {'host' => '10.10.10.3', 'port' => 26379} +] +``` + +After the change run the reconfigure command: + +```bash +sudo gitlab-ctl reconfigure + +``` + ## Configure your own Redis server If you're hosting GitLab on a cloud provider, you can optionally use a -- GitLab From ca7c6cc7d660a7c17ce5002ff3a9e94dcc0421cb Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 19 Jul 2016 14:27:50 +0300 Subject: [PATCH 045/153] Refactor Redis HA docs to also include Sentinel --- doc/administration/high_availability/redis.md | 347 +++++++++++++----- 1 file changed, 245 insertions(+), 102 deletions(-) diff --git a/doc/administration/high_availability/redis.md b/doc/administration/high_availability/redis.md index 1adc681e98bc12..8a6c5869e70ccd 100644 --- a/doc/administration/high_availability/redis.md +++ b/doc/administration/high_availability/redis.md @@ -1,163 +1,300 @@ # Configuring Redis for GitLab HA -You can choose to install and manage Redis yourself, or you can use GitLab -Omnibus packages to help. +You can choose to install and manage Redis yourself, or you can use the one +that comes bundled with GitLab Omnibus packages. -## Experimental Redis Sentinel support +> **Note:** Redis does not require authentication by default. See + [Redis Security](http://redis.io/topics/security) documentation for more + information. We recommend using a combination of a Redis password and tight + firewall rules to secure your Redis service. + +## Configure your own Redis server + +If you're hosting GitLab on a cloud provider, you can optionally use a +managed service for Redis. For example, AWS offers a managed ElastiCache service +that runs Redis. + +## Configure Redis using Omnibus + +If you don't want to bother setting up your own Redis server, you can use the +one bundled with Omnibus. In this case, you should disable all services except +Redis. + +1. Download/install GitLab Omnibus using **steps 1 and 2** from + [GitLab downloads](https://about.gitlab.com/downloads). Do not complete other + steps on the download page. +1. Create/edit `/etc/gitlab/gitlab.rb` and use the following configuration. + Be sure to change the `external_url` to match your eventual GitLab front-end + URL: + + ```ruby + external_url 'https://gitlab.example.com' + + # Disable all services except Redis + redis['enable'] = true + bootstrap['enable'] = false + nginx['enable'] = false + unicorn['enable'] = false + sidekiq['enable'] = false + postgresql['enable'] = false + gitlab_workhorse['enable'] = false + mailroom['enable'] = false + + # Redis configuration + redis['port'] = 6379 + redis['bind'] = '0.0.0.0' + + # If you wish to use Redis authentication (recommended) + redis['password'] = 'Redis Password' + ``` + +1. Run `sudo gitlab-ctl reconfigure` to install and configure PostgreSQL. + + > **Note**: This `reconfigure` step will result in some errors. + That's OK - don't be alarmed. + +1. Run `touch /etc/gitlab/skip-auto-migrations` to prevent database migrations + from running on upgrade. Only the primary GitLab application server should + handle migrations. -Since 8.10 release, you can configure a list of Redis Sentinel servers that +## Experimental Redis sentinel support + +> [Introduced][ce-1877] in GitLab 8.10. + +Since GitLab 8.10, you can configure a list of Redis Sentinel servers that will monitor a group of Redis servers to provide you with a standard failover support. -There is currently one exception to the Sentinel support: **mail_room**, the -component that process incoming emails. - -It doesn't support Sentinel yet, but we hope to integrate a future release -that does support it. +There is currently one exception to the Sentinel support: `mail_room`, the +component that processes incoming emails. It doesn't support Sentinel yet, but +we hope to integrate a future release that does support it. To get a better understanding on how to correctly setup Sentinel, please read -[Redis Sentinel documentation](http://redis.io/topics/sentinel) first, as -faling to configure it correctly can lead to data-loss. +the [Redis Sentinel documentation](http://redis.io/topics/sentinel) first, as +failing to configure it correctly can lead to data loss. + +The configuration consists of three parts: -### Redis setup +- Redis setup +- Sentinel setup +- GitLab setup + +Read carefully how to configure those components below. + +### Sentinel - Redis setup You must have at least 2 Redis servers: 1 Master, 1 or more Slaves. They should be configured the same way and with similar server specs, as in a failover situation, any Slave can be elected as the new Master by -the Sentinels servers. +the Sentinel servers. -In a minimal setup, the only required change for the slaves in `redis.conf` -is the addition of a `slaveof` line pointing to the initial master like this: +In a minimal setup, the only required change for the slaves in `redis.conf` +is the addition of a `slaveof` line pointing to the initial master. +You can increase the security by defining a `requirepass` configuration in +the master, and `masterauth` in slaves. -```conf -slaveof 192.168.1.1 6379 -``` +--- -You can increase the security by defining a `requirepass` configuration in -the master: +**Configuring your own Redis server** -```conf -requirepass " -``` +1. Add to the slaves' `redis.conf`: -and adding this line to all the slave servers: + ```conf + # IP and port of the master Redis server + slaveof 10.10.10.10 6379 + ``` -```conf -masterauth "" -``` +1. Optionally, set up password authentication for increased security. + Add the following to master's `redis.conf`: + + ```conf + # Optional password authentication for increased security + requirepass "" + ``` + +1. Then add this line to all the slave servers' `redis.conf`: -> **Note** This setup is not safe to be used by a machine accessible by the -internet. Use it in combination with tight firewall rules. + ```conf + masterauth "" + ``` + +1. Restart the Redis services for the changes to take effect. + +--- + +**Using Redis via Omnibus** + +1. Edit `/etc/gitlab/gitlab.rb`: + + ```ruby + # IP and port of the master Redis server + gitlab['redis']['master_ip'] = '10.10.10.10' + gitlab['redis']['master_port'] = 6379 + ``` + +1. Optionally, set up password authentication for increased security. + Add the following to master's `/etc/gitlab/gitlab.rb`: + + ```conf + # Optional password authentication for increased security + gitlab['redis']['password'] = '' + ``` + +1. Then add this line to all the slave servers' `redis.conf`: + + ```conf + gitlab['redis']['master_password'] = '' + ``` + +1. Reconfigure the GitLab for the changes to take effect. + +--- + +Now that the Redis servers are all set up, let's configure the Sentinel +servers. ### Sentinel setup -The support for Sentinel in ruby have some [caveats](https://github.com/redis/redis-rb/issues/531). -While you can give any name for the `master-group-name` part of the -configuration, as in this example: +The support for Sentinel in Ruby has some [caveats](https://github.com/redis/redis-rb/issues/531). +While you can give any name for the `master-group-name` part of the +configuration, as in this example: ```conf -sentinel monitor ` +sentinel monitor ``` -For it to work in ruby, you have to use the "hostname" of the master redis -server otherwhise you will get an error message like this one: -`Redis::CannotConnectError: No sentinels available.`. - +,for it to work in Ruby, you have to use the "hostname" of the master Redis +server, otherwise you will get an error message like: +`Redis::CannotConnectError: No sentinels available.`. Read +[Sentinel troubleshooting](#sentinel-troubleshooting) for more information. Here is an example configuration file (`sentinel.conf`) for a Sentinel node: - + ```conf port 26379 sentinel monitor master-redis.example.com 10.10.10.10 6379 1 sentinel down-after-milliseconds master-redis.example.com 10000 sentinel config-epoch master-redis.example.com 0 -sentinel leader-epoch locmaster-redis.example.comalhost 0 +sentinel leader-epoch master-redis.example.com 0 ``` -### GitLab setup +--- + +The final part is to inform the main GitLab application server of the Redis +master and the new sentinels servers. + +### Sentinel - GitLab setup You can enable or disable sentinel support at any time in new or existing -installs. From the GitLab application perspective, all it requires is +installations. From the GitLab application perspective, all it requires is the correct credentials for the Master redis and for a few Sentinels nodes. It doesn't require a list of all sentinel nodes, as in case of a failure, the application will need to query only one of them. -For a source based install, you must change `/home/git/gitlab/config/resque.yml`, -following the example in `/home/git/gitlab/config/resque.yml.example` and -uncommenting the sentinels line, changing to the correct server credentials, -and resstart GitLab. - -For a Omnibus install you have to add/change this lines from the -`/etc/gitlab/gitlab.rb` configuration file: - -```ruby -gitlab['gitlab-rails']['redis_host'] = "master-redis.example.com" -gitlab['gitlab-rails']['redis_port'] = 6379 -gitlab['gitlab-rails']['redis_password'] = "redis-secure-password-here" -gitlab['gitlab-rails']['redis_socket'] = nil -gitlab['gitlab-rails']['redis_sentinels'] = [ - {'host' => '10.10.10.1', 'port' => 26379}, - {'host' => '10.10.10.2', 'port' => 26379}, - {'host' => '10.10.10.3', 'port' => 26379} -] -``` +>**Note:** +The following steps should be performed in the [GitLab application server](gitlab.md). + +**For source based installations** -After the change run the reconfigure command: +1. Edit `/home/git/gitlab/config/resque.yml` following the example in + `/home/git/gitlab/config/resque.yml.example`i, and uncomment the sentinels + line, changing to the correct server credentials. +1. Restart GitLab for the changes to take effect. -```bash -sudo gitlab-ctl reconfigure +**For Omnibus installations** +1. Edit `/etc/gitlab/gitlab.rb` and add/change the following lines: + + ```ruby + gitlab-rails['redis_host'] = "master-redis.example.com" + gitlab-rails['redis_port'] = 6379 + gitlab-rails['redis_password'] = "redis-secure-password-here" + gitlab-rails['redis_socket'] = nil + gitlab-rails['redis_sentinels'] = [ + {'host' => '10.10.10.1', 'port' => 26379}, + {'host' => '10.10.10.2', 'port' => 26379}, + {'host' => '10.10.10.3', 'port' => 26379} + ] + ``` + +1. [Reconfigure] the GitLab for the changes to take effect. + +### Sentinel troubleshooting + +If you get an error like: `Redis::CannotConnectError: No sentinels available.`, +there may be something wrong with your configuration files or it can be related +to [this issue][gh-531] ([pull request][gh-534] that should make things better). + +It's a bit rigid the way you have to config `resque.yml` and `sentinel.conf`, +otherwise `redis-rb` will not work properly. + +The hostname ('my-primary-redis') of the primary Redis server (`sentinel.conf`) +**must** match the one configured in GitLab (`resque.yml` for source installations +or `gitlab-rails['redis_*']` in Omnibus) and it must be valid ex: + +```conf +# sentinel.conf: +sentinel monitor my-primary-redis 10.10.10.10 6379 1 +sentinel down-after-milliseconds my-primary-redis 10000 +sentinel config-epoch my-primary-redis 0 +sentinel leader-epoch my-primary-redis 0 ``` -## Configure your own Redis server +```yaml +# resque.yaml +production: + url: redis://my-primary-redis:6378 + sentinels: + - + host: slave1 + port: 26380 # point to sentinel, not to redis port + - + host: slave2 + port: 26381 # point to sentinel, not to redis port +``` -If you're hosting GitLab on a cloud provider, you can optionally use a -managed service for Redis. For example, AWS offers a managed ElastiCache service -that runs Redis. +When in doubt, please read [Redis Sentinel documentation](http://redis.io/topics/sentinel) -> **Note:** Redis does not require authentication by default. See - [Redis Security](http://redis.io/topics/security) documentation for more - information. We recommend using a combination of a Redis password and tight - firewall rules to secure your Redis service. +--- -## Configure using Omnibus +To make sure your configuration is correct: -1. Download/install GitLab Omnibus using **steps 1 and 2** from - [GitLab downloads](https://about.gitlab.com/downloads). Do not complete other - steps on the download page. -1. Create/edit `/etc/gitlab/gitlab.rb` and use the following configuration. - Be sure to change the `external_url` to match your eventual GitLab front-end - URL. +1. SSH into your GitLab application server +1. Enter the Rails console: + + ``` + # For Omnibus installations + sudo gitlab-rails console + + # For source installations + sudo -u git rails console RAILS_ENV=production + ``` + +1. Run in the console: ```ruby - external_url 'https://gitlab.example.com' - - # Disable all components except Redis - redis['enable'] = true - bootstrap['enable'] = false - nginx['enable'] = false - unicorn['enable'] = false - sidekiq['enable'] = false - postgresql['enable'] = false - gitlab_workhorse['enable'] = false - mailroom['enable'] = false - - # Redis configuration - redis['port'] = 6379 - redis['bind'] = '0.0.0.0' - - # If you wish to use Redis authentication (recommended) - redis['password'] = 'Redis Password' + redis = Redis.new(Gitlab::Redis.params) + redis.info ``` -1. Run `sudo gitlab-ctl reconfigure` to install and configure PostgreSQL. + Keep this screen open and try to simulate a failover below. - > **Note**: This `reconfigure` step will result in some errors. - That's OK - don't be alarmed. -1. Run `touch /etc/gitlab/skip-auto-migrations` to prevent database migrations - from running on upgrade. Only the primary GitLab application server should - handle migrations. +1. To simulate a failover on master Redis, SSH into the Redis server and run: + + ```bash + # port must match your master redis port + redis-cli -h localhost -p 6379 DEBUG sleep 60 + ``` + +1. Then back in the Rails console from the first step, run: + + ``` + redis.info + ``` + + You should see a different port after a few seconds delay + (the failover/reconnect time). --- @@ -167,3 +304,9 @@ Read more on high-availability configuration: 1. [Configure NFS](nfs.md) 1. [Configure the GitLab application servers](gitlab.md) 1. [Configure the load balancers](load_balancer.md) + +[ce-1877]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/1877 +[restart]: ../restart_gitlab.md#installations-from-source +[reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure +[gh-531]: https://github.com/redis/redis-rb/issues/531 +[gh-534]: https://github.com/redis/redis-rb/issues/534 -- GitLab From c6738a44232fb30e7cb3711bccf62af3bfc9e3a3 Mon Sep 17 00:00:00 2001 From: Gabriel Mazetto Date: Tue, 2 Aug 2016 03:25:28 +0200 Subject: [PATCH 046/153] Updated documentations for Redis Sentinel experimental support --- doc/administration/high_availability/redis.md | 51 ++++++++++--------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/doc/administration/high_availability/redis.md b/doc/administration/high_availability/redis.md index 8a6c5869e70ccd..65ab8e387cd9b3 100644 --- a/doc/administration/high_availability/redis.md +++ b/doc/administration/high_availability/redis.md @@ -57,7 +57,7 @@ Redis. from running on upgrade. Only the primary GitLab application server should handle migrations. -## Experimental Redis sentinel support +## Experimental Redis Sentinel support > [Introduced][ce-1877] in GitLab 8.10. @@ -81,7 +81,7 @@ The configuration consists of three parts: Read carefully how to configure those components below. -### Sentinel - Redis setup +### Redis setup You must have at least 2 Redis servers: 1 Master, 1 or more Slaves. They should be configured the same way and with similar server specs, as @@ -124,29 +124,31 @@ the master, and `masterauth` in slaves. **Using Redis via Omnibus** -1. Edit `/etc/gitlab/gitlab.rb`: +1. Edit `/etc/gitlab/gitlab.rb` of a master Redis machine (usualy a single machine): ```ruby - # IP and port of the master Redis server - gitlab['redis']['master_ip'] = '10.10.10.10' - gitlab['redis']['master_port'] = 6379 - ``` - -1. Optionally, set up password authentication for increased security. - Add the following to master's `/etc/gitlab/gitlab.rb`: + ## Redis TCP support (will disable UNIX socket transport) + redis['bind'] = '0.0.0.0' # or specify an IP to bind to a single one + redis['port'] = 6379 - ```conf - # Optional password authentication for increased security - gitlab['redis']['password'] = '' + ## Master redis instance + redis['password'] = '' ``` -1. Then add this line to all the slave servers' `redis.conf`: +1. Edit `/etc/gitlab/gitlab.rb` of a slave Redis machine (should be one or more machines): - ```conf - gitlab['redis']['master_password'] = '' + ```ruby + ## Redis TCP support (will disable UNIX socket transport) + redis['bind'] = '0.0.0.0' # or specify an IP to bind to a single one + redis['port'] = 6379 + + ## Slave redis instance + redis['master_ip'] = '10.10.10.10' # IP of master Redis server + redis['master_port'] = 6379 # Port of master Redis server + redis['master_password'] = "" ``` -1. Reconfigure the GitLab for the changes to take effect. +1. Reconfigure the GitLab for the changes to take effect: `sudo gitlab-ctl reconfigure` --- @@ -155,6 +157,10 @@ servers. ### Sentinel setup +We don't provide yet an automated way to setup and run the Sentinel daemon +from Omnibus installation method. You must follow the instructions below and +run it by yourself. + The support for Sentinel in Ruby has some [caveats](https://github.com/redis/redis-rb/issues/531). While you can give any name for the `master-group-name` part of the configuration, as in this example: @@ -183,13 +189,13 @@ sentinel leader-epoch master-redis.example.com 0 The final part is to inform the main GitLab application server of the Redis master and the new sentinels servers. -### Sentinel - GitLab setup +### GitLab setup You can enable or disable sentinel support at any time in new or existing installations. From the GitLab application perspective, all it requires is -the correct credentials for the Master redis and for a few Sentinels nodes. +the correct credentials for the master Redis and for a few Sentinel nodes. -It doesn't require a list of all sentinel nodes, as in case of a failure, +It doesn't require a list of all Sentinel nodes, as in case of a failure, the application will need to query only one of them. >**Note:** @@ -198,7 +204,7 @@ The following steps should be performed in the [GitLab application server](gitla **For source based installations** 1. Edit `/home/git/gitlab/config/resque.yml` following the example in - `/home/git/gitlab/config/resque.yml.example`i, and uncomment the sentinels + `/home/git/gitlab/config/resque.yml.example`, and uncomment the sentinels line, changing to the correct server credentials. 1. Restart GitLab for the changes to take effect. @@ -209,8 +215,7 @@ The following steps should be performed in the [GitLab application server](gitla ```ruby gitlab-rails['redis_host'] = "master-redis.example.com" gitlab-rails['redis_port'] = 6379 - gitlab-rails['redis_password'] = "redis-secure-password-here" - gitlab-rails['redis_socket'] = nil + gitlab-rails['redis_password'] = '' gitlab-rails['redis_sentinels'] = [ {'host' => '10.10.10.1', 'port' => 26379}, {'host' => '10.10.10.2', 'port' => 26379}, -- GitLab From 45b392b2033e3704ca641547e001fcc6df599a88 Mon Sep 17 00:00:00 2001 From: Gabriel Mazetto Date: Thu, 14 Jul 2016 03:33:15 +0200 Subject: [PATCH 047/153] Added Redis sentinel support to the CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 77bcea54cf9a59..cc59b6a818c9dd 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -18,6 +18,7 @@ v 8.11.0 (unreleased) - Optimize maximum user access level lookup in loading of notes - Add "No one can push" as an option for protected branches. !5081 - Improve performance of AutolinkFilter#text_parse by using XPath + - Add experimental Redis Sentinel support !1877 - Environments have an url to link to - Remove unused images (ClemMakesApps) - Limit git rev-list output count to one in forced push check -- GitLab From ce41b5c73f7574ec19c47f24e9450ff1b54ef1b1 Mon Sep 17 00:00:00 2001 From: Gabriel Mazetto Date: Thu, 4 Aug 2016 18:55:24 +0200 Subject: [PATCH 048/153] Small refactor and a few documentation fixes --- doc/administration/high_availability/redis.md | 4 ++-- lib/gitlab/mail_room.rb | 4 ++-- lib/gitlab/redis.rb | 2 +- spec/config/mail_room_spec.rb | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/doc/administration/high_availability/redis.md b/doc/administration/high_availability/redis.md index 65ab8e387cd9b3..bc424330656099 100644 --- a/doc/administration/high_availability/redis.md +++ b/doc/administration/high_availability/redis.md @@ -59,9 +59,9 @@ Redis. ## Experimental Redis Sentinel support -> [Introduced][ce-1877] in GitLab 8.10. +> [Introduced][ce-1877] in GitLab 8.11. -Since GitLab 8.10, you can configure a list of Redis Sentinel servers that +Since GitLab 8.11, you can configure a list of Redis Sentinel servers that will monitor a group of Redis servers to provide you with a standard failover support. diff --git a/lib/gitlab/mail_room.rb b/lib/gitlab/mail_room.rb index 1f68e09fa2b7ac..12999a90a298fa 100644 --- a/lib/gitlab/mail_room.rb +++ b/lib/gitlab/mail_room.rb @@ -13,8 +13,8 @@ def config @config ||= fetch_config end - def reload_config! - @config = fetch_config + def reset_config! + @config = nil end private diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb index 17ac15a01dd40b..72fa80e61e22bf 100644 --- a/lib/gitlab/redis.rb +++ b/lib/gitlab/redis.rb @@ -37,7 +37,7 @@ def with end end - def initialize(rails_env=nil) + def initialize(rails_env = nil) @rails_env = rails_env || ::Rails.env end diff --git a/spec/config/mail_room_spec.rb b/spec/config/mail_room_spec.rb index 2c988f1c883be0..c5d3cd70acc87d 100644 --- a/spec/config/mail_room_spec.rb +++ b/spec/config/mail_room_spec.rb @@ -7,7 +7,7 @@ context 'when incoming email is disabled' do before do ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = Rails.root.join('spec/fixtures/mail_room_disabled.yml').to_s - Gitlab::MailRoom.reload_config! + Gitlab::MailRoom.reset_config! end after do @@ -22,7 +22,7 @@ context 'when incoming email is enabled' do before do ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = Rails.root.join('spec/fixtures/mail_room_enabled.yml').to_s - Gitlab::MailRoom.reload_config! + Gitlab::MailRoom.reset_config! end after do -- GitLab From 5b5150301725fd192c328ee811facf2c89ffb528 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 4 Aug 2016 10:09:13 -0500 Subject: [PATCH 049/153] Fix filter label tooltip HTML rendering --- CHANGELOG | 1 + app/assets/stylesheets/pages/labels.scss | 11 +++++++++++ app/views/shared/_labels_row.html.haml | 6 +----- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 77bcea54cf9a59..2edebcc2110591 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -10,6 +10,7 @@ v 8.11.0 (unreleased) - Add support for relative links starting with ./ or / to RelativeLinkFilter (winniehell) - Fix CI status icon link underline (ClemMakesApps) - The Repository class is now instrumented + - Fix filter label tooltip HTML rendering (ClemMakesApps) - Cache the commit author in RequestStore to avoid extra lookups in PostReceive - Expand commit message width in repo view (ClemMakesApps) - Cache highlighted diff lines for merge requests diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 3b1e38fc07ddd4..606459f82cd03c 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -182,6 +182,17 @@ .btn { color: inherit; } + + a.btn { + padding: 0; + + .has-tooltip { + top: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + line-height: 1.1; + } + } } .label-options-toggle { diff --git a/app/views/shared/_labels_row.html.haml b/app/views/shared/_labels_row.html.haml index dce492352ac618..e324d0e5203e4a 100644 --- a/app/views/shared/_labels_row.html.haml +++ b/app/views/shared/_labels_row.html.haml @@ -1,9 +1,5 @@ - labels.each do |label| %span.label-row.btn-group{ role: "group", aria: { label: label.name }, style: "color: #{text_color_for_bg(label.color)}" } - = link_to label.name, label_filter_path(@project, label, type: controller.controller_name), - class: "btn btn-transparent has-tooltip", - style: "background-color: #{label.color};", - title: escape_once(label.description), - data: { container: "body" } + = link_to_label(label, css_class: 'btn btn-transparent') %button.btn.btn-transparent.label-remove.js-label-filter-remove{ type: "button", style: "background-color: #{label.color};", data: { label: label.title } } = icon("times") -- GitLab From a92281cecb80b5d85ebac3f9801de02bd3b3a6a8 Mon Sep 17 00:00:00 2001 From: joshbabier Date: Wed, 3 Aug 2016 20:42:18 -0400 Subject: [PATCH 050/153] TST: Use more accurate time windows so tests do not fail Currently, the way the 'starts_at' and 'ends_at' attributes are set, if the specs are run at one second to midnight, the broadcast message will expire in one second. I have changed it so that we are guaranteed a period of one day until expiration. I believe this is the desired behaviour and it's also consistent with the rest of the factory. This corrects the following three specs that can fail depending upon where and when they are run: ./spec/helpers/broadcast_messages_helper_spec.rb:42 ./spec/models/broadcast_message_spec.rb:26 ./spec/models/broadcast_message_spec.rb:47 --- spec/factories/broadcast_messages.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/factories/broadcast_messages.rb b/spec/factories/broadcast_messages.rb index efe9803b1a7159..c2fdf89213a614 100644 --- a/spec/factories/broadcast_messages.rb +++ b/spec/factories/broadcast_messages.rb @@ -1,8 +1,8 @@ FactoryGirl.define do factory :broadcast_message do message "MyText" - starts_at Date.yesterday - ends_at Date.tomorrow + starts_at 1.day.ago + ends_at 1.day.from_now trait :expired do starts_at 5.days.ago -- GitLab From ceff0c91700b15ecf26931c23f40637dcdf7204d Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Tue, 2 Aug 2016 13:41:22 -0300 Subject: [PATCH 051/153] Check out locally PRs where the source/target branch were removed --- lib/gitlab/github_import/importer.rb | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 3932fcb1eda26f..3b501533ffbad0 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -71,11 +71,12 @@ def import_pull_requests pull_requests = client.pull_requests(repo, state: :all, sort: :created, direction: :asc, per_page: 100) pull_requests = pull_requests.map { |raw| PullRequestFormatter.new(project, raw) }.select(&:valid?) - source_branches_removed = pull_requests.reject(&:source_branch_exists?).map { |pr| [pr.source_branch_name, pr.source_branch_sha] } + source_branches_removed = pull_requests.reject(&:source_branch_exists?).map { |pr| [pr.source_branch_name, pr.number] } target_branches_removed = pull_requests.reject(&:target_branch_exists?).map { |pr| [pr.target_branch_name, pr.target_branch_sha] } branches_removed = source_branches_removed | target_branches_removed - restore_branches(branches_removed) + restore_source_branches(source_branches_removed) + restore_target_branches(target_branches_removed) pull_requests.each do |pull_request| merge_request = pull_request.create! @@ -120,17 +121,20 @@ def hooks end end - def restore_branches(branches) - branches.each do |name, sha| - client.create_ref(repo, "refs/heads/#{name}", sha) + def restore_source_branches(branches) + branches.each do |name, number| + project.repository.fetch_ref(repo_url, "pull/#{number}/head", name) end + end - project.repository.fetch_ref(repo_url, '+refs/heads/*', 'refs/heads/*') + def restore_target_branches(branches) + branches.each do |name, sha| + project.repository.create_branch(name, sha) + end end def clean_up_restored_branches(branches) branches.each do |name, _| - client.delete_ref(repo, "heads/#{name}") project.repository.delete_branch(name) rescue Rugged::ReferenceError end -- GitLab From 51cf14b37c64b1afab25b10dfe94e281ff58d288 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Tue, 2 Aug 2016 13:46:02 -0300 Subject: [PATCH 052/153] Does not need to disable GitHub webhooks since PRs are check out locally --- app/views/import/github/status.html.haml | 4 -- lib/gitlab/github_import/hook_formatter.rb | 23 ------- lib/gitlab/github_import/importer.rb | 31 --------- .../github_import/hook_formatter_spec.rb | 65 ------------------- 4 files changed, 123 deletions(-) delete mode 100644 lib/gitlab/github_import/hook_formatter.rb delete mode 100644 spec/lib/gitlab/github_import/hook_formatter_spec.rb diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml index deaaf9af875186..54ff1d27c67bd1 100644 --- a/app/views/import/github/status.html.haml +++ b/app/views/import/github/status.html.haml @@ -4,10 +4,6 @@ %i.fa.fa-github Import projects from GitHub -%p - %i.fa.fa-warning - To import GitHub pull requests, any pull request source branches that had been deleted are temporarily restored on GitHub. To prevent any connected CI services from being overloaded with dozens of irrelevant branches being created and deleted again, GitHub webhooks are temporarily disabled during the import process, but only if you have admin access to the GitHub repository. - %p.light Select projects you want to import. %hr diff --git a/lib/gitlab/github_import/hook_formatter.rb b/lib/gitlab/github_import/hook_formatter.rb deleted file mode 100644 index db1fabaa18af50..00000000000000 --- a/lib/gitlab/github_import/hook_formatter.rb +++ /dev/null @@ -1,23 +0,0 @@ -module Gitlab - module GithubImport - class HookFormatter - EVENTS = %w[* create delete pull_request push].freeze - - attr_reader :raw - - delegate :id, :name, :active, to: :raw - - def initialize(raw) - @raw = raw - end - - def config - raw.config.attrs - end - - def valid? - (EVENTS & raw.events).any? && active - end - end - end -end diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 3b501533ffbad0..294e44b7124029 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -66,8 +66,6 @@ def import_issues end def import_pull_requests - disable_webhooks - pull_requests = client.pull_requests(repo, state: :all, sort: :created, direction: :asc, per_page: 100) pull_requests = pull_requests.map { |raw| PullRequestFormatter.new(project, raw) }.select(&:valid?) @@ -90,35 +88,6 @@ def import_pull_requests raise Projects::ImportService::Error, e.message ensure clean_up_restored_branches(branches_removed) - clean_up_disabled_webhooks - end - - def disable_webhooks - update_webhooks(hooks, active: false) - end - - def clean_up_disabled_webhooks - update_webhooks(hooks, active: true) - end - - def update_webhooks(hooks, options) - hooks.each do |hook| - client.edit_hook(repo, hook.id, hook.name, hook.config, options) - end - end - - def hooks - @hooks ||= - begin - client.hooks(repo).map { |raw| HookFormatter.new(raw) }.select(&:valid?) - - # The GitHub Repository Webhooks API returns 404 for users - # without admin access to the repository when listing hooks. - # In this case we just want to return gracefully instead of - # spitting out an error and stop the import process. - rescue Octokit::NotFound - [] - end end def restore_source_branches(branches) diff --git a/spec/lib/gitlab/github_import/hook_formatter_spec.rb b/spec/lib/gitlab/github_import/hook_formatter_spec.rb deleted file mode 100644 index 110ba428258ece..00000000000000 --- a/spec/lib/gitlab/github_import/hook_formatter_spec.rb +++ /dev/null @@ -1,65 +0,0 @@ -require 'spec_helper' - -describe Gitlab::GithubImport::HookFormatter, lib: true do - describe '#id' do - it 'returns raw id' do - raw = double(id: 100000) - formatter = described_class.new(raw) - expect(formatter.id).to eq 100000 - end - end - - describe '#name' do - it 'returns raw id' do - raw = double(name: 'web') - formatter = described_class.new(raw) - expect(formatter.name).to eq 'web' - end - end - - describe '#config' do - it 'returns raw config.attrs' do - raw = double(config: double(attrs: { url: 'http://something.com/webhook' })) - formatter = described_class.new(raw) - expect(formatter.config).to eq({ url: 'http://something.com/webhook' }) - end - end - - describe '#valid?' do - it 'returns true when events contains the wildcard event' do - raw = double(events: ['*', 'commit_comment'], active: true) - formatter = described_class.new(raw) - expect(formatter.valid?).to eq true - end - - it 'returns true when events contains the create event' do - raw = double(events: ['create', 'commit_comment'], active: true) - formatter = described_class.new(raw) - expect(formatter.valid?).to eq true - end - - it 'returns true when events contains delete event' do - raw = double(events: ['delete', 'commit_comment'], active: true) - formatter = described_class.new(raw) - expect(formatter.valid?).to eq true - end - - it 'returns true when events contains pull_request event' do - raw = double(events: ['pull_request', 'commit_comment'], active: true) - formatter = described_class.new(raw) - expect(formatter.valid?).to eq true - end - - it 'returns false when events does not contains branch related events' do - raw = double(events: ['member', 'commit_comment'], active: true) - formatter = described_class.new(raw) - expect(formatter.valid?).to eq false - end - - it 'returns false when hook is not active' do - raw = double(events: ['pull_request', 'commit_comment'], active: false) - formatter = described_class.new(raw) - expect(formatter.valid?).to eq false - end - end -end -- GitLab From 1e736fb5272b75eb363a1ef766e29d35d53babb6 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Tue, 2 Aug 2016 23:25:45 -0300 Subject: [PATCH 053/153] Allow users to import cross-repository pull requests from GitHub --- doc/workflow/importing/import_projects_from_github.md | 3 --- lib/gitlab/github_import/pull_request_formatter.rb | 6 +----- .../gitlab/github_import/pull_request_formatter_spec.rb | 8 ++++---- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/doc/workflow/importing/import_projects_from_github.md b/doc/workflow/importing/import_projects_from_github.md index a2b2a4b88f9d66..306caabf6e6fb1 100644 --- a/doc/workflow/importing/import_projects_from_github.md +++ b/doc/workflow/importing/import_projects_from_github.md @@ -18,9 +18,6 @@ At its current state, GitHub importer can import: With GitLab 8.7+, references to pull requests and issues are preserved. -It is not yet possible to import your cross-repository pull requests (those from -forks). We are working on improving this in the near future. - The importer page is visible when you [create a new project][new-project]. Click on the **GitHub** link and, if you are logged in via the GitHub integration, you will be redirected to GitHub for permission to access your diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb index a4ea2210abdba7..f7f8a4ce9842b2 100644 --- a/lib/gitlab/github_import/pull_request_formatter.rb +++ b/lib/gitlab/github_import/pull_request_formatter.rb @@ -33,7 +33,7 @@ def number end def valid? - source_branch.valid? && target_branch.valid? && !cross_project? + source_branch.valid? && target_branch.valid? end def source_branch @@ -68,10 +68,6 @@ def body raw_data.body || "" end - def cross_project? - source_branch_repo.id != target_branch_repo.id - end - def description formatter.author_line(author) + body end diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb index 79931ecd134e9e..ad79715a2d47b0 100644 --- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb @@ -178,8 +178,8 @@ let(:source_repo) { double(id: 2) } let(:raw_data) { double(base_data) } - it 'returns false' do - expect(pull_request.valid?).to eq false + it 'returns true' do + expect(pull_request.valid?).to eq true end end @@ -187,8 +187,8 @@ let(:target_repo) { double(id: 2) } let(:raw_data) { double(base_data) } - it 'returns false' do - expect(pull_request.valid?).to eq false + it 'returns true' do + expect(pull_request.valid?).to eq true end end end -- GitLab From 65871dfda5fe864c58e6b373408bca372b3c6c06 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 5 Aug 2016 08:11:01 +0200 Subject: [PATCH 054/153] Remove empty context from CI config processor specs --- spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index a7d1d6b0a8bfda..85374b8761dd93 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -533,9 +533,6 @@ module Ci } end - context 'when also global variables are defined' do - end - context 'when syntax is correct' do let(:variables) do { VAR1: 'value1', VAR2: 'value2' } -- GitLab From 13cabe7184e372c6d903a836c180231d2e78f517 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 3 Aug 2016 12:26:24 +0300 Subject: [PATCH 055/153] Add newlines styleguide for Ruby code --- CONTRIBUTING.md | 2 + doc/development/newlines_styleguide.md | 102 +++++++++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 doc/development/newlines_styleguide.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 14ff05c9aa3b97..92a34171e0d776 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -459,6 +459,7 @@ merge request: - multi-line method chaining style **Option B**: dot `.` on previous line - string literal quoting style **Option A**: single quoted by default 1. [Rails](https://github.com/bbatsov/rails-style-guide) +1. [Newlines styleguide][newlines-styleguide] 1. [Testing](doc/development/testing.md) 1. [CoffeeScript](https://github.com/thoughtbot/guides/tree/master/style/coffeescript) 1. [SCSS styleguide][scss-styleguide] @@ -530,6 +531,7 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor [rss-naming]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#naming [doc-styleguide]: doc/development/doc_styleguide.md "Documentation styleguide" [scss-styleguide]: doc/development/scss_styleguide.md "SCSS styleguide" +[newlines-styleguide]: doc/development/newlines_styleguide.md "Newlines styleguide" [gitlab-design]: https://gitlab.com/gitlab-org/gitlab-design [free Antetype viewer (Mac OSX only)]: https://itunes.apple.com/us/app/antetype-viewer/id824152298?mt=12 [`gitlab8.atype` file]: https://gitlab.com/gitlab-org/gitlab-design/tree/master/current/ diff --git a/doc/development/newlines_styleguide.md b/doc/development/newlines_styleguide.md new file mode 100644 index 00000000000000..e03adcaadea5d3 --- /dev/null +++ b/doc/development/newlines_styleguide.md @@ -0,0 +1,102 @@ +# Newlines styleguide + +This style guide recommends best practices for newlines in Ruby code. + +## Rule: separate code with newlines only when it makes sense from logic perspectice + +```ruby +# bad +def method + issue = Issue.new + + issue.save + + render json: issue +end +``` + +```ruby +# good +def method + issue = Issue.new + issue.save + + render json: issue +end +``` + +## Rule: separate code and block with newlines + +### Newline before block + +```ruby +# bad +def method + issue = Issue.new + if issue.save + render json: issue + end +end +``` + +```ruby +# good +def method + issue = Issue.new + + if issue.save + render json: issue + end +end +``` + +## Newline after block + +```ruby +# bad +def method + if issue.save + issue.send_email + end + render json: issue +end +``` + +```ruby +# good +def method + if issue.save + issue.send_email + end + + render json: issue +end +``` + +### Exception: no need for newline when code block starts or ends right inside another code block + +```ruby +# bad +def method + + if issue + + if issue.valid? + issue.save + end + + end + +end +``` + +```ruby +# good +def method + if issue + if issue.valid? + issue.save + end + end +end +``` -- GitLab From 554e18ab025fcd86001faa57fab14fe3ca28a672 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Fri, 5 Aug 2016 11:35:44 +0200 Subject: [PATCH 056/153] Create service for enabling deploy keys --- .../projects/deploy_keys_controller.rb | 20 +++++--------- app/services/enable_deploy_key_service.rb | 14 ++++++++++ lib/api/deploy_keys.rb | 6 ++--- spec/requests/api/deploy_keys_spec.rb | 13 +++++---- .../enable_deploy_key_service_spec.rb | 27 +++++++++++++++++++ 5 files changed, 58 insertions(+), 22 deletions(-) create mode 100644 app/services/enable_deploy_key_service.rb create mode 100644 spec/services/enable_deploy_key_service_spec.rb diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb index 83d5ced9be8b60..ade2c54552b98b 100644 --- a/app/controllers/projects/deploy_keys_controller.rb +++ b/app/controllers/projects/deploy_keys_controller.rb @@ -12,8 +12,7 @@ def index end def new - redirect_to namespace_project_deploy_keys_path(@project.namespace, - @project) + redirect_to namespace_project_deploy_keys_path(@project.namespace, @project) end def create @@ -21,19 +20,16 @@ def create set_index_vars if @key.valid? && @project.deploy_keys << @key - redirect_to namespace_project_deploy_keys_path(@project.namespace, - @project) + redirect_to namespace_project_deploy_keys_path(@project.namespace, @project) else render "index" end end def enable - @key = accessible_keys.find(params[:id]) - @project.deploy_keys << @key + EnableDeployKeyService.new(@project, current_user, params).execute - redirect_to namespace_project_deploy_keys_path(@project.namespace, - @project) + redirect_to namespace_project_deploy_keys_path(@project.namespace, @project) end def disable @@ -45,9 +41,9 @@ def disable protected def set_index_vars - @enabled_keys ||= @project.deploy_keys + @enabled_keys ||= @project.deploy_keys - @available_keys ||= accessible_keys - @enabled_keys + @available_keys ||= current_user.accessible_deploy_keys - @enabled_keys @available_project_keys ||= current_user.project_deploy_keys - @enabled_keys @available_public_keys ||= DeployKey.are_public - @enabled_keys @@ -56,10 +52,6 @@ def set_index_vars @available_public_keys -= @available_project_keys end - def accessible_keys - @accessible_keys ||= current_user.accessible_deploy_keys - end - def deploy_key_params params.require(:deploy_key).permit(:key, :title) end diff --git a/app/services/enable_deploy_key_service.rb b/app/services/enable_deploy_key_service.rb new file mode 100644 index 00000000000000..baa4a9dd2d4fc0 --- /dev/null +++ b/app/services/enable_deploy_key_service.rb @@ -0,0 +1,14 @@ +class EnableDeployKeyService < BaseService + def execute + key = accessible_keys.find_by(id: params[:key_id] || params[:id]) + + project.deploy_keys << key if key + key + end + + private + + def accessible_keys + current_user.accessible_deploy_keys + end +end diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb index 52f89373ad3010..6dc9beb57ec918 100644 --- a/lib/api/deploy_keys.rb +++ b/lib/api/deploy_keys.rb @@ -76,12 +76,12 @@ class DeployKeys < Grape::API requires :key_id, type: Integer, desc: 'The ID of the deploy key' end post ":id/#{path}/:key_id/enable" do - key = DeployKey.find(params[:key_id]) + key = EnableDeployKeyService.new(user_project, current_user, declared(params)).execute - if user_project.deploy_keys << key + if key present key, with: Entities::SSHKey else - render_validation_error!(key) + not_found!('Deploy Key') end end diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb index 7e7a2733f3c1ce..7d8cc45327c73f 100644 --- a/spec/requests/api/deploy_keys_spec.rb +++ b/spec/requests/api/deploy_keys_spec.rb @@ -3,11 +3,14 @@ describe API::API, api: true do include ApiHelpers - let(:user) { create(:user) } - let(:admin) { create(:admin) } - let(:project) { create(:project, creator_id: user.id) } - let!(:deploy_keys_project) { create(:deploy_keys_project, project: project) } - let(:deploy_key) { deploy_keys_project.deploy_key } + let(:user) { create(:user) } + let(:admin) { create(:admin) } + let(:project) { create(:project, creator_id: user.id) } + let(:deploy_key) { create(:deploy_key, public: true) } + + let!(:deploy_keys_project) do + create(:deploy_keys_project, project: project, deploy_key: deploy_key) + end describe 'GET /deploy_keys' do context 'when unauthenticated' do diff --git a/spec/services/enable_deploy_key_service_spec.rb b/spec/services/enable_deploy_key_service_spec.rb new file mode 100644 index 00000000000000..abb5710dfc036a --- /dev/null +++ b/spec/services/enable_deploy_key_service_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe EnableDeployKeyService, services: true do + let(:deploy_key) { create(:deploy_key, public: true) } + let(:project) { create(:empty_project) } + let(:user) { project.creator} + let!(:params) { { key_id: deploy_key.id } } + + it 'enables the key' do + expect do + service.execute + end.to change { project.deploy_keys.count }.from(0).to(1) + end + + context 'trying to add an unaccessable key' do + let(:another_key) { create(:another_key) } + let!(:params) { { key_id: another_key.id } } + + it 'returns nil if the key cannot be added' do + expect(service.execute).to be nil + end + end + + def service + EnableDeployKeyService.new(project, user, params) + end +end -- GitLab From 81e839f41b38d165bd6af4899e12be2967d607e8 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Fri, 5 Aug 2016 11:38:43 +0200 Subject: [PATCH 057/153] Fix styling on docs of Deploy Keys endpoints --- doc/api/deploy_keys.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/doc/api/deploy_keys.md b/doc/api/deploy_keys.md index a0340fd4d37c80..a288de5fc9773e 100644 --- a/doc/api/deploy_keys.md +++ b/doc/api/deploy_keys.md @@ -164,7 +164,7 @@ Example response: Enables a deploy key for a project so this can be used. Returns the enabled key, with a status code 201 when successful. -``` +```bash curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/deploy_keys/13/enable ``` @@ -175,7 +175,6 @@ curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com Example response: -```json ```json { "key" : "ssh-rsa AAAA...", @@ -189,7 +188,7 @@ Example response: Disable a deploy key for a project. Returns the disabled key. -``` +```bash curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/deploy_keys/13/disable ``` -- GitLab From 8720a6e4ef47675f5778b140b6b56615f808aff4 Mon Sep 17 00:00:00 2001 From: Tom Bell Date: Wed, 3 Aug 2016 20:30:49 +0100 Subject: [PATCH 058/153] Update to send changed password notification emails Add the devise initializer config setting to enable the sending of notification emails when a user changes their password. --- CHANGELOG | 1 + config/initializers/devise.rb | 3 +++ 2 files changed, 4 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index b6532cafbcddef..d343a4f234a0af 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -73,6 +73,7 @@ v 8.11.0 (unreleased) - Add description to new_issue email and new_merge_request_email in text/plain content type. !5663 (dixpac) - Speed up and reduce memory usage of Commit#repo_changes, Repository#expire_avatar_cache and IrkerWorker - Add unfold links for Side-by-Side view. !5415 (Tim Masliuchenko) + - Update devise initializer to turn on changed password notification emails. !5648 (tombell) v 8.10.5 (unreleased) diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 73977341b73ae6..a0a8f88584c3dc 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -100,6 +100,9 @@ # secure: true in order to force SSL only cookies. # config.cookie_options = {} + # Send a notification email when the user's password is changed + config.send_password_change_notification = true + # ==> Configuration for :validatable # Range for password length. Default is 6..128. config.password_length = 8..128 -- GitLab From ed0a7c254ab32130296fb7ee467f655d200d7854 Mon Sep 17 00:00:00 2001 From: Gabriel Mazetto Date: Sat, 6 Aug 2016 03:01:52 +0200 Subject: [PATCH 059/153] Small refactor in Redis class and improved specs --- lib/gitlab/redis.rb | 6 +++++- spec/fixtures/config/redis_new_format_host.yml | 6 +++--- spec/fixtures/config/redis_old_format_socket.yml | 6 +++--- spec/lib/gitlab/redis_spec.rb | 7 +++++-- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb index 72fa80e61e22bf..9376b54f43bc10 100644 --- a/lib/gitlab/redis.rb +++ b/lib/gitlab/redis.rb @@ -19,7 +19,7 @@ class Redis class << self def params - PARAMS_MUTEX.synchronize { new.params } + @params || PARAMS_MUTEX.synchronize { @params = new.params } end # @deprecated Use .params instead to get sentinel support @@ -35,6 +35,10 @@ def with end @pool.with { |redis| yield redis } end + + def reset_params! + @params = nil + end end def initialize(rails_env = nil) diff --git a/spec/fixtures/config/redis_new_format_host.yml b/spec/fixtures/config/redis_new_format_host.yml index 5e5bf6e0ccd612..13772677a45c2e 100644 --- a/spec/fixtures/config/redis_new_format_host.yml +++ b/spec/fixtures/config/redis_new_format_host.yml @@ -1,7 +1,7 @@ # redis://[:password@]host[:port][/db-number][?option=value] # more details: http://www.iana.org/assignments/uri-schemes/prov/redis development: - url: redis://:mypassword@localhost:6379/99 + url: redis://:mynewpassword@localhost:6379/99 sentinels: - host: localhost @@ -10,7 +10,7 @@ development: host: slave2 port: 26381 # point to sentinel, not to redis port test: - url: redis://:mypassword@localhost:6379/99 + url: redis://:mynewpassword@localhost:6379/99 sentinels: - host: localhost @@ -19,7 +19,7 @@ test: host: slave2 port: 26381 # point to sentinel, not to redis port production: - url: redis://:mypassword@localhost:6379/99 + url: redis://:mynewpassword@localhost:6379/99 sentinels: - host: slave1 diff --git a/spec/fixtures/config/redis_old_format_socket.yml b/spec/fixtures/config/redis_old_format_socket.yml index cb39b6fb9e230c..fd31ce8ea3d77f 100644 --- a/spec/fixtures/config/redis_old_format_socket.yml +++ b/spec/fixtures/config/redis_old_format_socket.yml @@ -1,3 +1,3 @@ -development: unix:/path/to/redis.sock -test: unix:/path/to/redis.sock -production: unix:/path/to/redis.sock +development: unix:/path/to/old/redis.sock +test: unix:/path/to/old/redis.sock +production: unix:/path/to/old/redis.sock diff --git a/spec/lib/gitlab/redis_spec.rb b/spec/lib/gitlab/redis_spec.rb index 04257f89627718..879ed30841cc1d 100644 --- a/spec/lib/gitlab/redis_spec.rb +++ b/spec/lib/gitlab/redis_spec.rb @@ -3,6 +3,9 @@ describe Gitlab::Redis do let(:redis_config) { Rails.root.join('config', 'resque.yml').to_s } + before(:each) { described_class.reset_params! } + after(:each) { described_class.reset_params! } + describe '.params' do subject { described_class.params } @@ -14,7 +17,7 @@ it 'returns path key instead' do expect_any_instance_of(described_class).to receive(:config_file) { config_old } - is_expected.to include(path: '/path/to/redis.sock') + is_expected.to include(path: '/path/to/old/redis.sock') is_expected.not_to have_key(:url) end end @@ -46,7 +49,7 @@ it 'returns hash with host, port, db, and password' do expect_any_instance_of(described_class).to receive(:config_file) { config_new } - is_expected.to include(host: 'localhost', password: 'mypassword', port: 6379, db: 99) + is_expected.to include(host: 'localhost', password: 'mynewpassword', port: 6379, db: 99) is_expected.not_to have_key(:url) end end -- GitLab From 7a6c88ddd9f6cc5869a03abf6278aa3eb5f7f33c Mon Sep 17 00:00:00 2001 From: Christian Couder Date: Sun, 31 Jul 2016 18:36:04 +0200 Subject: [PATCH 060/153] Add an initial .mailmap This is to fix the output of `git shortlog` and `git check-mailmap`. I only added names and addresses for developers with a very large number of commits. It could be improved to add more. --- .mailmap | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .mailmap diff --git a/.mailmap b/.mailmap new file mode 100644 index 00000000000000..bd5ac22132c413 --- /dev/null +++ b/.mailmap @@ -0,0 +1,35 @@ +# +# This list is used by git-shortlog to make contributions from the +# same person appearing to be so. +# + +Achilleas Pipinellis +Achilleas Pipinellis +Dmitriy Zaporozhets +Dmitriy Zaporozhets +Douwe Maan +Douwe Maan +Grzegorz Bizon +Grzegorz Bizon +Jacob Vosmaer +Jacob Vosmaer Jacob Vosmaer (GitLab) +Jacob Schatz +Jacob Schatz +Jacob Schatz +James Lopez +James Lopez +Kamil Trzciński +Marin Jankovski +Phil Hughes +Rémy Coutable +Robert Schilling +Robert Schilling +Robert Speicher +Stan Hu +Stan Hu +Stan Hu +Stan Hu stanhu +Sytse Sijbrandij +Sytse Sijbrandij +Sytse Sijbrandij +Sytse Sijbrandij dosire -- GitLab From 5e1ac6275ffa292fe7f6b8b5b74d92b99385d7d5 Mon Sep 17 00:00:00 2001 From: winniehell Date: Fri, 1 Jul 2016 17:29:25 +0200 Subject: [PATCH 061/153] Add test coverage analysis for CoffeeScript (!5052) --- .gitignore | 1 + .gitlab-ci.yml | 10 ++++++++++ spec/teaspoon_env.rb | 8 ++++---- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index ce6a363fe35fe4..1bf9a47aef6de8 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ /config/secrets.yml /config/sidekiq.yml /coverage/* +/coverage-javascript/ /db/*.sqlite3 /db/*.sqlite3-journal /db/data.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8da9acf9066a4d..5aff5078386a85 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -222,7 +222,15 @@ teaspoon: stage: test <<: *use-db script: + - curl --silent --location https://deb.nodesource.com/setup_6.x | bash - + - apt-get install --assume-yes nodejs + - npm install --global istanbul - teaspoon + artifacts: + name: coverage-javascript + expire_in: 31d + paths: + - coverage-javascript/default/ bundler:audit: stage: test @@ -269,10 +277,12 @@ pages: stage: pages dependencies: - coverage + - teaspoon script: - mv public/ .public/ - mkdir public/ - mv coverage public/coverage-ruby + - mv coverage-javascript/default/ public/coverage-javascript/ artifacts: paths: - public diff --git a/spec/teaspoon_env.rb b/spec/teaspoon_env.rb index 1a3bbb9c8cc3fd..5ea020f313c9af 100644 --- a/spec/teaspoon_env.rb +++ b/spec/teaspoon_env.rb @@ -149,7 +149,7 @@ # Specify that you always want a coverage configuration to be used. Otherwise, specify that you want coverage # on the CLI. # Set this to "true" or the name of your coverage config. - # config.use_coverage = nil + config.use_coverage = true # You can have multiple coverage configs by passing a name to config.coverage. # e.g. config.coverage :ci do |coverage| @@ -158,15 +158,15 @@ # Which coverage reports Istanbul should generate. Correlates directly to what Istanbul supports. # # Available: text-summary, text, html, lcov, lcovonly, cobertura, teamcity - # coverage.reports = ["text-summary", "html"] + coverage.reports = ["text-summary", "html"] # The path that the coverage should be written to - when there's an artifact to write to disk. # Note: Relative to `config.root`. - # coverage.output_path = "coverage" + coverage.output_path = "coverage-javascript" # Assets to be ignored when generating coverage reports. Accepts an array of filenames or regular expressions. The # default excludes assets from vendor, gems and support libraries. - # coverage.ignore = [%r{/lib/ruby/gems/}, %r{/vendor/assets/}, %r{/support/}, %r{/(.+)_helper.}] + coverage.ignore = [%r{vendor/}, %r{spec/}] # Various thresholds requirements can be defined, and those thresholds will be checked at the end of a run. If any # aren't met the run will fail with a message. Thresholds can be defined as a percentage (0-100), or nil. -- GitLab From 6907af28d05c22431b69f5ea7c0c685ac21131e9 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Mon, 8 Aug 2016 09:40:29 +0200 Subject: [PATCH 062/153] Use FA GitLab Icon for import project button --- app/views/projects/new.html.haml | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index facdfcc9447f71..432887c1adb0f2 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -46,28 +46,24 @@ %div - if github_import_enabled? = link_to new_import_github_path, class: 'btn import_github' do - = icon 'github', text: 'GitHub' + = icon('github', text: 'GitHub') %div - if bitbucket_import_enabled? - if bitbucket_import_configured? = link_to status_import_bitbucket_path, class: 'btn import_bitbucket', "data-no-turbolink" => "true" do - %i.fa.fa-bitbucket - Bitbucket + = icon('bitbucket', text: 'Bitbucket') - else = link_to status_import_bitbucket_path, class: 'how_to_import_link btn import_bitbucket', "data-no-turbolink" => "true" do - %i.fa.fa-bitbucket - Bitbucket + = icon('bitbucket', text: 'Bitbucket') = render 'bitbucket_import_modal' %div - if gitlab_import_enabled? - if gitlab_import_configured? = link_to status_import_gitlab_path, class: 'btn import_gitlab' do - %i.fa.fa-heart - GitLab.com + = icon('gitlab', text: 'GitLab.com') - else = link_to status_import_gitlab_path, class: 'how_to_import_link btn import_gitlab' do - %i.fa.fa-heart - GitLab.com + = icon('gitlab', text: 'GitLab.com') = render 'gitlab_import_modal' %div - if gitorious_import_enabled? @@ -77,23 +73,19 @@ %div - if google_code_import_enabled? = link_to new_import_google_code_path, class: 'btn import_google_code' do - %i.fa.fa-google - Google Code + = icon('google', text: 'Google Code') %div - if fogbugz_import_enabled? = link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do - %i.fa.fa-bug - Fogbugz + = icon('bug', text: 'Fogbugz') %div - if git_import_enabled? = link_to "#", class: 'btn js-toggle-button import_git' do - %i.fa.fa-git - %span Repo by URL + = icon('git', text: 'Repo by URL') %div{ class: 'import_gitlab_project' } - if gitlab_project_import_enabled? = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do - %i.fa.fa-gitlab - %span GitLab export + = icon('gitlab', text: 'GitLab export') .js-toggle-content.hide = render "shared/import_form", f: f @@ -159,4 +151,4 @@ $('.import_git').click(function( event ) { $projectImportUrl = $('#project_import_url') $projectImportUrl.attr('disabled', !$projectImportUrl.attr('disabled')) - }); \ No newline at end of file + }); -- GitLab From 1b5e2303debf00784603d6bd9d87d613de3c8091 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 28 Jul 2016 15:39:41 +0200 Subject: [PATCH 063/153] Use new badge template to render build status badge --- app/controllers/projects/badges_controller.rb | 3 ++- app/views/projects/badges/badge.svg.erb | 2 +- features/steps/project/badges/build.rb | 2 +- lib/gitlab/badge/build.rb | 4 ++++ 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/controllers/projects/badges_controller.rb b/app/controllers/projects/badges_controller.rb index a9f482c8787b0e..d0f5071d2cc211 100644 --- a/app/controllers/projects/badges_controller.rb +++ b/app/controllers/projects/badges_controller.rb @@ -8,8 +8,9 @@ def build respond_to do |format| format.html { render_404 } + format.svg do - send_data(badge.data, type: badge.type, disposition: 'inline') + render 'badge', locals: { badge: badge.template } end end end diff --git a/app/views/projects/badges/badge.svg.erb b/app/views/projects/badges/badge.svg.erb index 5a71419d3af91f..de461997c4699a 100644 --- a/app/views/projects/badges/badge.svg.erb +++ b/app/views/projects/badges/badge.svg.erb @@ -10,7 +10,7 @@ - + diff --git a/features/steps/project/badges/build.rb b/features/steps/project/badges/build.rb index 66a48a176e5870..96c59322f9b2f5 100644 --- a/features/steps/project/badges/build.rb +++ b/features/steps/project/badges/build.rb @@ -26,7 +26,7 @@ class Spinach::Features::ProjectBadgesBuild < Spinach::FeatureSteps def expect_badge(status) svg = Nokogiri::XML.parse(page.body) - expect(page.response_headers).to include('Content-Type' => 'image/svg+xml') + expect(page.response_headers['Content-Type']).to include('image/svg+xml') expect(svg.at(%Q{text:contains("#{status}")})).to be_truthy end end diff --git a/lib/gitlab/badge/build.rb b/lib/gitlab/badge/build.rb index 7bc6f285ce1b07..c94ef3e9678f39 100644 --- a/lib/gitlab/badge/build.rb +++ b/lib/gitlab/badge/build.rb @@ -21,6 +21,10 @@ def metadata Build::Metadata.new(@project, @ref) end + def template + Build::Template.new(status) + end + def type 'image/svg+xml' end -- GitLab From bc17996227117bcaf11dc41a0dd2b2f11a7329f4 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 8 Aug 2016 12:38:36 +0200 Subject: [PATCH 064/153] Improve builds badge specs, remove legacy methods --- lib/gitlab/badge/build.rb | 21 +++++---------- spec/lib/gitlab/badge/build_spec.rb | 40 +++++++++++------------------ 2 files changed, 21 insertions(+), 40 deletions(-) diff --git a/lib/gitlab/badge/build.rb b/lib/gitlab/badge/build.rb index c94ef3e9678f39..1de721a2269633 100644 --- a/lib/gitlab/badge/build.rb +++ b/lib/gitlab/badge/build.rb @@ -4,35 +4,26 @@ module Badge # Build badge # class Build + delegate :key_text, :value_text, to: :template + def initialize(project, ref) @project = project @ref = ref + @sha = @project.commit(@ref).try(:sha) end def status - sha = @project.commit(@ref).try(:sha) - @project.pipelines - .where(sha: sha, ref: @ref) + .where(sha: @sha, ref: @ref) .status || 'unknown' end def metadata - Build::Metadata.new(@project, @ref) + @metadata ||= Build::Metadata.new(@project, @ref) end def template - Build::Template.new(status) - end - - def type - 'image/svg+xml' - end - - def data - File.read( - Rails.root.join('public/ci', 'build-' + status + '.svg') - ) + @template ||= Build::Template.new(status) end end end diff --git a/spec/lib/gitlab/badge/build_spec.rb b/spec/lib/gitlab/badge/build_spec.rb index 1b36c0a36eaeaa..f9abbdaf585fa9 100644 --- a/spec/lib/gitlab/badge/build_spec.rb +++ b/spec/lib/gitlab/badge/build_spec.rb @@ -6,11 +6,6 @@ let(:branch) { 'master' } let(:badge) { described_class.new(project, branch) } - describe '#type' do - subject { badge.type } - it { is_expected.to eq 'image/svg+xml' } - end - describe '#metadata' do it 'returns badge metadata' do expect(badge.metadata.image_url) @@ -18,6 +13,12 @@ end end + describe '#key_text' do + it 'always says build' do + expect(badge.key_text).to eq 'build' + end + end + context 'build exists' do let!(:build) { create_build(project, sha, branch) } @@ -30,11 +31,9 @@ end end - describe '#data' do - let(:data) { badge.data } - - it 'contains information about success' do - expect(status_node(data, 'success')).to be_truthy + describe '#value_text' do + it 'returns correct value text' do + expect(badge.value_text).to eq 'success' end end end @@ -48,11 +47,9 @@ end end - describe '#data' do - let(:data) { badge.data } - - it 'contains information about failure' do - expect(status_node(data, 'failed')).to be_truthy + describe '#value_text' do + it 'has correct value text' do + expect(badge.value_text).to eq 'failed' end end end @@ -65,11 +62,9 @@ end end - describe '#data' do - let(:data) { badge.data } - - it 'contains infromation about unknown build' do - expect(status_node(data, 'unknown')).to be_truthy + describe '#value_text' do + it 'has correct value text' do + expect(badge.value_text).to eq 'unknown' end end end @@ -95,9 +90,4 @@ def create_build(project, sha, branch) create(:ci_build, pipeline: pipeline, stage: 'notify') end - - def status_node(data, status) - xml = Nokogiri::XML.parse(data) - xml.at(%Q{text:contains("#{status}")}) - end end -- GitLab From 89f2be7d5867991c1fe964e8d9a94ff64c13ce61 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 8 Aug 2016 12:49:26 +0200 Subject: [PATCH 065/153] Improve indentation in svg badge template --- app/views/projects/badges/badge.svg.erb | 25 ++++++++++++++++++------- lib/gitlab/badge/build/template.rb | 2 +- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/app/views/projects/badges/badge.svg.erb b/app/views/projects/badges/badge.svg.erb index de461997c4699a..9d9a919f915fb7 100644 --- a/app/views/projects/badges/badge.svg.erb +++ b/app/views/projects/badges/badge.svg.erb @@ -9,17 +9,28 @@ - - - + + + - <%= badge.key_text %> - <%= badge.key_text %> - <%= badge.value_text %> - <%= badge.value_text %> + + <%= badge.key_text %> + + + <%= badge.key_text %> + + + <%= badge.value_text %> + + + <%= badge.value_text %> + diff --git a/lib/gitlab/badge/build/template.rb b/lib/gitlab/badge/build/template.rb index a7c2e17693569d..deba3b669b3af9 100644 --- a/lib/gitlab/badge/build/template.rb +++ b/lib/gitlab/badge/build/template.rb @@ -2,7 +2,7 @@ module Gitlab module Badge class Build ## - # Abstract class for build badge template. + # Class that represents a build badge template. # # Template object will be passed to badge.svg.erb template. # -- GitLab From 74d12b6b4708164fe14c6019874384615ed3c711 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 8 Aug 2016 14:22:26 +0200 Subject: [PATCH 066/153] Remove legacy Ci::StaticModel we do not use anymore --- lib/ci/static_model.rb | 49 ------------------------------------------ 1 file changed, 49 deletions(-) delete mode 100644 lib/ci/static_model.rb diff --git a/lib/ci/static_model.rb b/lib/ci/static_model.rb deleted file mode 100644 index bb2bdbed495198..00000000000000 --- a/lib/ci/static_model.rb +++ /dev/null @@ -1,49 +0,0 @@ -# Provides an ActiveRecord-like interface to a model whose data is not persisted to a database. -module Ci - module StaticModel - extend ActiveSupport::Concern - - module ClassMethods - # Used by ActiveRecord's polymorphic association to set object_id - def primary_key - 'id' - end - - # Used by ActiveRecord's polymorphic association to set object_type - def base_class - self - end - end - - # Used by AR for fetching attributes - # - # Pass it along if we respond to it. - def [](key) - send(key) if respond_to?(key) - end - - def to_param - id - end - - def new_record? - false - end - - def persisted? - false - end - - def destroyed? - false - end - - def ==(other) - if other.is_a? ::Ci::StaticModel - id == other.id - else - super - end - end - end -end -- GitLab From f9cffe104489416d9a640557d87b94fd39ea4e7e Mon Sep 17 00:00:00 2001 From: Jeffrey Lin Date: Thu, 4 Aug 2016 13:56:01 -0400 Subject: [PATCH 067/153] "This file is managed by gitlab-ctl. Manual changes will be erased!" --- CHANGELOG | 1 + app/views/admin/application_settings/_form.html.haml | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 48235e2c1f7ffb..696c4fff9d115d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -34,6 +34,7 @@ v 8.11.0 (unreleased) - Optimize checking if a user has read access to a list of issues !5370 - Nokogiri's various parsing methods are now instrumented - Add simple identifier to public SSH keys (muteor) + - Admin page now references docs instead of a specific file !5600 (AnAverageHuman) - Add a way to send an email and create an issue based on private personal token. Find the email address from issues page. !3363 - Fix filter input alignment (ClemMakesApps) - Include old revision in merge request update hooks (Ben Boeckel) diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 23f864df147aa7..c7fd344eea2d47 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -366,7 +366,9 @@ .col-sm-10 = f.select :repository_storage, repository_storage_options_for_select, {}, class: 'form-control' .help-block - You can manage the repository storage paths in your gitlab.yml configuration file + Manage repository storage paths. Learn more in the + = succeed "." do + = link_to "repository storages documentation", help_page_path("administration/repository_storages") %fieldset %legend Repository Checks -- GitLab From 23ddd5eca22ebd53267f5fdbce204cbf9191ad53 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Mon, 8 Aug 2016 17:14:36 +0200 Subject: [PATCH 068/153] Link to doc root, not readme of GDK --- doc/development/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/development/README.md b/doc/development/README.md index 7b5f7ff8ad3f05..e64b23277718c0 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -4,7 +4,7 @@ - [CONTRIBUTING.md](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md) main contributing guide - [PROCESS.md](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/PROCESS.md) contributing process -- [GitLab Development Kit (GDK)](https://gitlab.com/gitlab-org/gitlab-development-kit) to install a development version +- [GitLab Development Kit (GDK)](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/howto/README.md) to install a development version ## Styleguides -- GitLab From ace19ec58f39faa084e6f5372732e3c094011c8e Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Fri, 5 Aug 2016 12:44:13 -0500 Subject: [PATCH 069/153] Style deploy button --- app/assets/stylesheets/pages/environments.scss | 17 +++++++++++++++++ app/views/projects/deployments/_actions.haml | 6 +++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index e160d676e35c3d..e9097d41c61619 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -1,5 +1,22 @@ .environments { + .commit-title { margin: 0; } + + .fa-play { + font-size: 14px; + } + + .dropdown-new { + color: $table-text-gray; + } + + .dropdown-menu { + + .fa { + margin-right: 6px; + color: $table-text-gray; + } + } } diff --git a/app/views/projects/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml index f70dba224faabe..f7bf3b834ef469 100644 --- a/app/views/projects/deployments/_actions.haml +++ b/app/views/projects/deployments/_actions.haml @@ -2,9 +2,9 @@ .pull-right - actions = deployment.manual_actions - if actions.present? - .btn-group.inline - .btn-group - %a.dropdown-toggle.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'} + .inline + .dropdown + %a.dropdown-new.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'} = icon("play") %b.caret %ul.dropdown-menu.dropdown-menu-align-right -- GitLab From 415921a7f38a19bc907ce0b3db21e4e66e9b1ac2 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Fri, 5 Aug 2016 14:08:39 -0500 Subject: [PATCH 070/153] Add avatar to commit message; environment style updates to match pipelines page --- app/assets/stylesheets/pages/environments.scss | 13 +++++++++++++ app/helpers/avatars_helper.rb | 2 -- app/views/projects/deployments/_commit.html.haml | 8 ++++++-- .../projects/environments/_environment.html.haml | 7 ++++--- app/views/projects/environments/index.html.haml | 4 ++-- 5 files changed, 25 insertions(+), 9 deletions(-) diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index e9097d41c61619..55f9d4a001123f 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -19,4 +19,17 @@ color: $table-text-gray; } } + + .branch-name { + color: $gl-dark-link-color; + } +} + +.table.builds.environments { + min-width: 500px; + + .icon-container { + width: 20px; + text-align: center; + } } diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb index 6ff40c6b461a10..010246b6c2308f 100644 --- a/app/helpers/avatars_helper.rb +++ b/app/helpers/avatars_helper.rb @@ -8,8 +8,6 @@ def author_avatar(commit_or_event, options = {}) })) end - private - def user_avatar(options = {}) avatar_size = options[:size] || 16 user_name = options[:user].try(:name) || options[:user_name] diff --git a/app/views/projects/deployments/_commit.html.haml b/app/views/projects/deployments/_commit.html.haml index 0f9d9512d887b1..a90c873386261e 100644 --- a/app/views/projects/deployments/_commit.html.haml +++ b/app/views/projects/deployments/_commit.html.haml @@ -1,12 +1,16 @@ %div.branch-commit - if deployment.ref - = link_to deployment.ref, namespace_project_commits_path(@project.namespace, @project, deployment.ref), class: "monospace" - · + .icon-container + = deployment.tag? ? icon('tag') : icon('code-fork') + = link_to deployment.ref, namespace_project_commits_path(@project.namespace, @project, deployment.ref), class: "monospace branch-name" + .icon-container + = custom_icon("icon_commit") = link_to deployment.short_sha, namespace_project_commit_path(@project.namespace, @project, deployment.sha), class: "commit-id monospace" %p.commit-title %span - if commit_title = deployment.commit_title + = user_avatar(user: deployment.user, size: 20) = link_to_gfm commit_title, namespace_project_commit_path(@project.namespace, @project, deployment.sha), class: "commit-row-message" - else Cant find HEAD commit for this branch diff --git a/app/views/projects/environments/_environment.html.haml b/app/views/projects/environments/_environment.html.haml index e2453395602c1f..d04967fa72fc7c 100644 --- a/app/views/projects/environments/_environment.html.haml +++ b/app/views/projects/environments/_environment.html.haml @@ -2,8 +2,7 @@ %tr.environment %td - %strong - = link_to environment.name, namespace_project_environment_path(@project.namespace, @project, environment) + = link_to environment.name, namespace_project_environment_path(@project.namespace, @project, environment) %td - if last_deployment @@ -14,7 +13,9 @@ %td - if last_deployment - #{time_ago_with_tooltip(last_deployment.created_at)} + %p.finished-at + = icon("calendar") + #{time_ago_with_tooltip(last_deployment.created_at, short_format: true, skip_js: true)} %td = render 'projects/deployments/actions', deployment: last_deployment diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index a6dd34653abd84..fe8ddb4716e1ab 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -23,10 +23,10 @@ New environment - else .table-holder - %table.table.environments + %table.table.builds.environments %tbody %th Environment %th Last deployment - %th Date + %th %th = render @environments -- GitLab From 60a347228d90ce68eedb5bf4b75a4cad94a1dfb2 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Fri, 5 Aug 2016 14:54:17 -0500 Subject: [PATCH 071/153] Format environment history page --- app/views/projects/environments/show.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index a07436ad7c97dd..8f8c1c4ce22c09 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -23,13 +23,13 @@ = link_to "Read more", help_page_path("ci/environments"), class: "btn btn-success" - else .table-holder - %table.table.environments + %table.table.builds.environments %thead %tr %th ID %th Commit %th Build - %th Date + %th %th = render @deployments -- GitLab From 0a15ad31ad4bafe0a87cb0c4fd345a5f82c44799 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Mon, 8 Aug 2016 11:39:24 -0500 Subject: [PATCH 072/153] Add deployment ID and gravatar to environments page --- app/views/projects/deployments/_commit.html.haml | 2 +- app/views/projects/environments/_environment.html.haml | 9 ++++++--- app/views/projects/environments/index.html.haml | 1 + 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/views/projects/deployments/_commit.html.haml b/app/views/projects/deployments/_commit.html.haml index a90c873386261e..28813babd7be6b 100644 --- a/app/views/projects/deployments/_commit.html.haml +++ b/app/views/projects/deployments/_commit.html.haml @@ -10,7 +10,7 @@ %p.commit-title %span - if commit_title = deployment.commit_title - = user_avatar(user: deployment.user, size: 20) + = author_avatar(deployment.commit, size: 20) = link_to_gfm commit_title, namespace_project_commit_path(@project.namespace, @project, deployment.sha), class: "commit-row-message" - else Cant find HEAD commit for this branch diff --git a/app/views/projects/environments/_environment.html.haml b/app/views/projects/environments/_environment.html.haml index d04967fa72fc7c..718eca1574165f 100644 --- a/app/views/projects/environments/_environment.html.haml +++ b/app/views/projects/environments/_environment.html.haml @@ -4,6 +4,11 @@ %td = link_to environment.name, namespace_project_environment_path(@project.namespace, @project, environment) + %td + - if last_deployment + = user_avatar(user: last_deployment.user, size: 20) + %span ##{last_deployment.id} + %td - if last_deployment = render 'projects/deployments/commit', deployment: last_deployment @@ -13,9 +18,7 @@ %td - if last_deployment - %p.finished-at - = icon("calendar") - #{time_ago_with_tooltip(last_deployment.created_at, short_format: true, skip_js: true)} + #{time_ago_with_tooltip(last_deployment.created_at)} %td = render 'projects/deployments/actions', deployment: last_deployment diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index fe8ddb4716e1ab..8bb238bae4ee30 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -26,6 +26,7 @@ %table.table.builds.environments %tbody %th Environment + %th Deployment ID %th Last deployment %th %th -- GitLab From f6c0c96b80b971ffaa7aa49553e13f6849f8ec4a Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Mon, 8 Aug 2016 11:43:12 -0500 Subject: [PATCH 073/153] Add gravatars to build history --- app/views/projects/deployments/_deployment.html.haml | 1 + app/views/projects/environments/_environment.html.haml | 2 +- app/views/projects/environments/index.html.haml | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml index baf02f1e6a013c..cd95841ca5a9fb 100644 --- a/app/views/projects/deployments/_deployment.html.haml +++ b/app/views/projects/deployments/_deployment.html.haml @@ -8,6 +8,7 @@ %td - if deployment.deployable = link_to [@project.namespace.becomes(Namespace), @project, deployment.deployable] do + = user_avatar(user: deployment.user, size: 20) = "#{deployment.deployable.name} (##{deployment.deployable.id})" %td diff --git a/app/views/projects/environments/_environment.html.haml b/app/views/projects/environments/_environment.html.haml index 718eca1574165f..36a6162a5a862e 100644 --- a/app/views/projects/environments/_environment.html.haml +++ b/app/views/projects/environments/_environment.html.haml @@ -7,7 +7,7 @@ %td - if last_deployment = user_avatar(user: last_deployment.user, size: 20) - %span ##{last_deployment.id} + %strong ##{last_deployment.id} %td - if last_deployment diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index 8bb238bae4ee30..b3eb5b0011a961 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -26,8 +26,8 @@ %table.table.builds.environments %tbody %th Environment - %th Deployment ID - %th Last deployment + %th Last Deployment + %th Commit %th %th = render @environments -- GitLab From ed63ead22aa9fd9fe509f3bebd73223b4ff8b8c5 Mon Sep 17 00:00:00 2001 From: Carlos Ribeiro Date: Mon, 8 Aug 2016 11:03:30 -0300 Subject: [PATCH 074/153] Avoid to show the original password field when password is automatically seted --- CHANGELOG | 1 + .../profiles/passwords_controller.rb | 1 + spec/features/profiles/password_spec.rb | 45 +++++++++++++++++++ 3 files changed, 47 insertions(+) create mode 100644 spec/features/profiles/password_spec.rb diff --git a/CHANGELOG b/CHANGELOG index f5416434ab1dab..bf250d4e34efd6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -77,6 +77,7 @@ v 8.11.0 (unreleased) - Speed up and reduce memory usage of Commit#repo_changes, Repository#expire_avatar_cache and IrkerWorker - Add unfold links for Side-by-Side view. !5415 (Tim Masliuchenko) - Adds support for pending invitation project members importing projects + - Avoid to show the original password field when password is automatically set. !5712 (duduribeiro) v 8.10.5 (unreleased) diff --git a/app/controllers/profiles/passwords_controller.rb b/app/controllers/profiles/passwords_controller.rb index c780e0983f93ae..6217ec5ecef196 100644 --- a/app/controllers/profiles/passwords_controller.rb +++ b/app/controllers/profiles/passwords_controller.rb @@ -50,6 +50,7 @@ def update flash[:notice] = "Password was successfully updated. Please login with it" redirect_to new_user_session_path else + @user.reload render 'edit' end end diff --git a/spec/features/profiles/password_spec.rb b/spec/features/profiles/password_spec.rb new file mode 100644 index 00000000000000..4cbdd89d46f028 --- /dev/null +++ b/spec/features/profiles/password_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' + +describe 'Profile > Password', feature: true do + let(:user) { create(:user, password_automatically_set: true) } + + before do + login_as(user) + visit edit_profile_password_path + end + + def fill_passwords(password, confirmation) + fill_in 'New password', with: password + fill_in 'Password confirmation', with: confirmation + + click_button 'Save password' + end + + context 'User with password automatically set' do + describe 'User puts different passwords in the field and in the confirmation' do + it 'shows an error message' do + fill_passwords('mypassword', 'mypassword2') + + page.within('.alert-danger') do + expect(page).to have_content("Password confirmation doesn't match Password") + end + end + + it 'does not contains the current password field after an error' do + fill_passwords('mypassword', 'mypassword2') + + expect(page).to have_no_field('user[current_password]') + end + end + + describe 'User puts the same passwords in the field and in the confirmation' do + it 'shows a success message' do + fill_passwords('mypassword', 'mypassword') + + page.within('.flash-notice') do + expect(page).to have_content('Password was successfully updated. Please login with it') + end + end + end + end +end -- GitLab From 6af4efea872407bf7f3957f3009984989a3a8e8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= Date: Mon, 8 Aug 2016 14:36:39 -0400 Subject: [PATCH 075/153] Update version_sorter and use new interface for faster tag sorting --- CHANGELOG | 1 + Gemfile | 2 +- Gemfile.lock | 4 ++-- app/models/repository.rb | 4 +--- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index f5416434ab1dab..a00b98cfe071b8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -33,6 +33,7 @@ v 8.11.0 (unreleased) - Retrieve rendered HTML from cache in one request - Fix renaming repository when name contains invalid chararacters under project settings - Fix devise deprecation warnings. + - Update version_sorter and use new interface for faster tag sorting - Optimize checking if a user has read access to a list of issues !5370 - Nokogiri's various parsing methods are now instrumented - Add simple identifier to public SSH keys (muteor) diff --git a/Gemfile b/Gemfile index 8f94ee72a322ba..104929665e8f9a 100644 --- a/Gemfile +++ b/Gemfile @@ -154,7 +154,7 @@ gem 'settingslogic', '~> 2.0.9' # Misc -gem 'version_sorter', '~> 2.0.0' +gem 'version_sorter', '~> 2.1.0' # Cache gem 'redis-rails', '~> 4.0.0' diff --git a/Gemfile.lock b/Gemfile.lock index 870f9397b9e1e3..87f08d6f372d07 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -778,7 +778,7 @@ GEM uniform_notifier (1.10.0) uuid (2.3.8) macaddr (~> 1.0) - version_sorter (2.0.0) + version_sorter (2.1.0) virtus (1.0.5) axiom-types (~> 0.1) coercible (~> 1.0) @@ -989,7 +989,7 @@ DEPENDENCIES unf (~> 0.1.4) unicorn (~> 4.9.0) unicorn-worker-killer (~> 0.4.2) - version_sorter (~> 2.0.0) + version_sorter (~> 2.1.0) virtus (~> 1.0.1) vmstat (~> 2.1.1) web-console (~> 2.0) diff --git a/app/models/repository.rb b/app/models/repository.rb index 701f867f67cf7c..e56bac509a4ed1 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -636,9 +636,7 @@ def branches_sorted_by(value) def tags_sorted_by(value) case value when 'name' - # Would be better to use `sort_by` but `version_sorter` only exposes - # `sort` and `rsort` - VersionSorter.rsort(tag_names).map { |tag_name| find_tag(tag_name) } + VersionSorter.rsort(tags) { |tag| tag.name } when 'updated_desc' tags_sorted_by_committed_date.reverse when 'updated_asc' -- GitLab From 7e47a82899bdb10d2cdc61ce237a25bfa7f8a392 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Mon, 8 Aug 2016 20:32:10 +0200 Subject: [PATCH 076/153] Namespace EnableDeployKeyService under Projects --- .../projects/deploy_keys_controller.rb | 2 +- app/services/enable_deploy_key_service.rb | 14 -------------- .../projects/enable_deploy_key_service.rb | 17 +++++++++++++++++ lib/api/deploy_keys.rb | 3 ++- .../enable_deploy_key_service_spec.rb | 4 ++-- 5 files changed, 22 insertions(+), 18 deletions(-) delete mode 100644 app/services/enable_deploy_key_service.rb create mode 100644 app/services/projects/enable_deploy_key_service.rb rename spec/services/{ => projects}/enable_deploy_key_service_spec.rb (83%) diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb index ade2c54552b98b..529e0aa2d33e13 100644 --- a/app/controllers/projects/deploy_keys_controller.rb +++ b/app/controllers/projects/deploy_keys_controller.rb @@ -27,7 +27,7 @@ def create end def enable - EnableDeployKeyService.new(@project, current_user, params).execute + Projects::EnableDeployKeyService.new(@project, current_user, params).execute redirect_to namespace_project_deploy_keys_path(@project.namespace, @project) end diff --git a/app/services/enable_deploy_key_service.rb b/app/services/enable_deploy_key_service.rb deleted file mode 100644 index baa4a9dd2d4fc0..00000000000000 --- a/app/services/enable_deploy_key_service.rb +++ /dev/null @@ -1,14 +0,0 @@ -class EnableDeployKeyService < BaseService - def execute - key = accessible_keys.find_by(id: params[:key_id] || params[:id]) - - project.deploy_keys << key if key - key - end - - private - - def accessible_keys - current_user.accessible_deploy_keys - end -end diff --git a/app/services/projects/enable_deploy_key_service.rb b/app/services/projects/enable_deploy_key_service.rb new file mode 100644 index 00000000000000..3cf4264ce9bf3b --- /dev/null +++ b/app/services/projects/enable_deploy_key_service.rb @@ -0,0 +1,17 @@ +module Projects + class EnableDeployKeyService < BaseService + def execute + key = accessible_keys.find_by(id: params[:key_id] || params[:id]) + return unless key + + project.deploy_keys << key + key + end + + private + + def accessible_keys + current_user.accessible_deploy_keys + end + end +end diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb index 6dc9beb57ec918..825e05fbae3d72 100644 --- a/lib/api/deploy_keys.rb +++ b/lib/api/deploy_keys.rb @@ -76,7 +76,8 @@ class DeployKeys < Grape::API requires :key_id, type: Integer, desc: 'The ID of the deploy key' end post ":id/#{path}/:key_id/enable" do - key = EnableDeployKeyService.new(user_project, current_user, declared(params)).execute + key = ::Projects::EnableDeployKeyService.new(user_project, + current_user, declared(params)).execute if key present key, with: Entities::SSHKey diff --git a/spec/services/enable_deploy_key_service_spec.rb b/spec/services/projects/enable_deploy_key_service_spec.rb similarity index 83% rename from spec/services/enable_deploy_key_service_spec.rb rename to spec/services/projects/enable_deploy_key_service_spec.rb index abb5710dfc036a..a37510cf159897 100644 --- a/spec/services/enable_deploy_key_service_spec.rb +++ b/spec/services/projects/enable_deploy_key_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe EnableDeployKeyService, services: true do +describe Projects::EnableDeployKeyService, services: true do let(:deploy_key) { create(:deploy_key, public: true) } let(:project) { create(:empty_project) } let(:user) { project.creator} @@ -22,6 +22,6 @@ end def service - EnableDeployKeyService.new(project, user, params) + Projects::EnableDeployKeyService.new(project, user, params) end end -- GitLab From 141d6c91eeef811f3c8af552477260ca712a9895 Mon Sep 17 00:00:00 2001 From: Ruben Davila Date: Mon, 8 Aug 2016 16:14:13 -0500 Subject: [PATCH 077/153] Update VERSION to 8.11.0-rc1-ee --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index efbb18d7844705..5b42822dbe2c2c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.11.0-ee-pre +8.11.0-rc1-ee -- GitLab From 2bb93c2aa0dd2d94001a7e68bc285d18a254711b Mon Sep 17 00:00:00 2001 From: Ruben Davila Date: Mon, 8 Aug 2016 16:48:29 -0500 Subject: [PATCH 078/153] Update VERSION to 8.11.0-rc1 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 542e7824102447..2e1d2912f106ae 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.11.0-pre +8.11.0-rc1 -- GitLab From a361f314f8f7f00a7541a5af1b8a2a92ced475e9 Mon Sep 17 00:00:00 2001 From: winniehell Date: Mon, 8 Aug 2016 14:42:20 +0200 Subject: [PATCH 079/153] add linting script for documentation --- .gitlab-ci.yml | 7 +++++++ scripts/lint-doc.sh | 15 +++++++++++++++ 2 files changed, 22 insertions(+) create mode 100755 scripts/lint-doc.sh diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8da9acf9066a4d..4f118288902459 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -224,6 +224,13 @@ teaspoon: script: - teaspoon +lint-doc: + stage: test + image: "phusion/baseimage:latest" + before_script: [] + script: + - scripts/lint-doc.sh + bundler:audit: stage: test <<: *ruby-static-analysis diff --git a/scripts/lint-doc.sh b/scripts/lint-doc.sh new file mode 100755 index 00000000000000..bc6e4d94061142 --- /dev/null +++ b/scripts/lint-doc.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +cd "$(dirname "$0")/.." + +# Use long options (e.g. --header instead of -H) for curl examples in documentation. +grep --perl-regexp --recursive --color=auto 'curl (.+ )?-[^- ].*' doc/ +if [ $? == 0 ] +then + echo '✖ ERROR: Short options should not be used in documentation!' >&2 + exit 1 +fi + +echo "✔ Linting passed" +exit 0 + -- GitLab From 167a0c7f0bd88e565f580e144b59d9eebe7e24f6 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Thu, 4 Aug 2016 17:15:00 -0300 Subject: [PATCH 080/153] Remove SHA suffix for removed branches name when importing PR from GH --- lib/gitlab/github_import/branch_formatter.rb | 2 +- lib/gitlab/github_import/importer.rb | 54 +++++++++---------- .../github_import/branch_formatter_spec.rb | 8 +-- 3 files changed, 28 insertions(+), 36 deletions(-) diff --git a/lib/gitlab/github_import/branch_formatter.rb b/lib/gitlab/github_import/branch_formatter.rb index 7d2d545b84e501..4be4fc8fe37d17 100644 --- a/lib/gitlab/github_import/branch_formatter.rb +++ b/lib/gitlab/github_import/branch_formatter.rb @@ -8,7 +8,7 @@ def exists? end def name - @name ||= exists? ? ref : "#{ref}-#{short_id}" + ref end def valid? diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 294e44b7124029..9ddc8905bd6149 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -12,7 +12,6 @@ def initialize(project) if credentials @client = Client.new(credentials[:user]) - @formatter = Gitlab::ImportFormatter.new else raise Projects::ImportService::Error, "Unable to find project import data credentials for project ID: #{@project.id}" end @@ -69,43 +68,42 @@ def import_pull_requests pull_requests = client.pull_requests(repo, state: :all, sort: :created, direction: :asc, per_page: 100) pull_requests = pull_requests.map { |raw| PullRequestFormatter.new(project, raw) }.select(&:valid?) - source_branches_removed = pull_requests.reject(&:source_branch_exists?).map { |pr| [pr.source_branch_name, pr.number] } - target_branches_removed = pull_requests.reject(&:target_branch_exists?).map { |pr| [pr.target_branch_name, pr.target_branch_sha] } - branches_removed = source_branches_removed | target_branches_removed - - restore_source_branches(source_branches_removed) - restore_target_branches(target_branches_removed) - pull_requests.each do |pull_request| - merge_request = pull_request.create! - apply_labels(merge_request) - import_comments(merge_request) - import_comments_on_diff(merge_request) + begin + restore_source_branch(pull_request) unless pull_request.source_branch_exists? + restore_target_branch(pull_request) unless pull_request.target_branch_exists? + + merge_request = pull_request.create! + apply_labels(merge_request) + import_comments(merge_request) + import_comments_on_diff(merge_request) + rescue ActiveRecord::RecordInvalid => e + raise Projects::ImportService::Error, e.message + ensure + clean_up_restored_branches(pull_request) + end end true - rescue ActiveRecord::RecordInvalid => e - raise Projects::ImportService::Error, e.message - ensure - clean_up_restored_branches(branches_removed) end - def restore_source_branches(branches) - branches.each do |name, number| - project.repository.fetch_ref(repo_url, "pull/#{number}/head", name) - end + def restore_source_branch(pull_request) + project.repository.fetch_ref(repo_url, "pull/#{pull_request.number}/head", pull_request.source_branch_name) end - def restore_target_branches(branches) - branches.each do |name, sha| - project.repository.create_branch(name, sha) - end + def restore_target_branch(pull_request) + project.repository.create_branch(pull_request.target_branch_name, pull_request.target_branch_sha) end - def clean_up_restored_branches(branches) - branches.each do |name, _| - project.repository.delete_branch(name) rescue Rugged::ReferenceError - end + def remove_branch(name) + project.repository.delete_branch(name) + rescue Rugged::ReferenceError + nil + end + + def clean_up_restored_branches(pull_request) + remove_branch(pull_request.source_branch_name) unless pull_request.source_branch_exists? + remove_branch(pull_request.target_branch_name) unless pull_request.target_branch_exists? project.repository.after_remove_branch end diff --git a/spec/lib/gitlab/github_import/branch_formatter_spec.rb b/spec/lib/gitlab/github_import/branch_formatter_spec.rb index fc9d5204148f31..e01a80054a3350 100644 --- a/spec/lib/gitlab/github_import/branch_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/branch_formatter_spec.rb @@ -33,17 +33,11 @@ end describe '#name' do - it 'returns raw ref when branch exists' do + it 'returns raw ref' do branch = described_class.new(project, double(raw)) expect(branch.name).to eq 'feature' end - - it 'returns formatted ref when branch does not exist' do - branch = described_class.new(project, double(raw.merge(ref: 'removed-branch', sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b'))) - - expect(branch.name).to eq 'removed-branch-2e5d3239' - end end describe '#repo' do -- GitLab From 44a196f8bc978ec09256abdc67c7e331b07cb7c3 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Tue, 2 Aug 2016 23:27:43 -0300 Subject: [PATCH 081/153] Update CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 77bcea54cf9a59..d08d7790568ae5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -39,6 +39,7 @@ v 8.11.0 (unreleased) - Make fork counter always clickable. !5463 (winniehell) - Gitlab::Highlight is now instrumented - All created issues, API or WebUI, can be submitted to Akismet for spam check !5333 + - Allow users to import cross-repository pull requests from GitHub - The overhead of instrumented method calls has been reduced - Remove `search_id` of labels dropdown filter to fix 'Missleading URI for labels in Merge Requests and Issues view'. !5368 (Scott Le) - Load project invited groups and members eagerly in `ProjectTeam#fetch_members` -- GitLab From 0a1535a9f44b12accdf3d6585ff7fee53737da51 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Mon, 8 Aug 2016 20:24:40 -0300 Subject: [PATCH 082/153] Prefixes removed branches name with PR number when importing PR from GH --- lib/gitlab/github_import/branch_formatter.rb | 4 -- .../github_import/pull_request_formatter.rb | 16 +++++++- .../github_import/branch_formatter_spec.rb | 8 ---- .../pull_request_formatter_spec.rb | 37 +++++++++++++++++++ 4 files changed, 51 insertions(+), 14 deletions(-) diff --git a/lib/gitlab/github_import/branch_formatter.rb b/lib/gitlab/github_import/branch_formatter.rb index 4be4fc8fe37d17..4750675ae9ddf7 100644 --- a/lib/gitlab/github_import/branch_formatter.rb +++ b/lib/gitlab/github_import/branch_formatter.rb @@ -7,10 +7,6 @@ def exists? branch_exists? && commit_exists? end - def name - ref - end - def valid? repo.present? end diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb index f7f8a4ce9842b2..b84538a090a268 100644 --- a/lib/gitlab/github_import/pull_request_formatter.rb +++ b/lib/gitlab/github_import/pull_request_formatter.rb @@ -1,8 +1,8 @@ module Gitlab module GithubImport class PullRequestFormatter < BaseFormatter - delegate :exists?, :name, :project, :repo, :sha, to: :source_branch, prefix: true - delegate :exists?, :name, :project, :repo, :sha, to: :target_branch, prefix: true + delegate :exists?, :project, :ref, :repo, :sha, to: :source_branch, prefix: true + delegate :exists?, :project, :ref, :repo, :sha, to: :target_branch, prefix: true def attributes { @@ -40,10 +40,22 @@ def source_branch @source_branch ||= BranchFormatter.new(project, raw_data.head) end + def source_branch_name + @source_branch_name ||= begin + source_branch_exists? ? source_branch_ref : "pull/#{number}/#{source_branch_ref}" + end + end + def target_branch @target_branch ||= BranchFormatter.new(project, raw_data.base) end + def target_branch_name + @target_branch_name ||= begin + target_branch_exists? ? target_branch_ref : "pull/#{number}/#{target_branch_ref}" + end + end + private def assigned? diff --git a/spec/lib/gitlab/github_import/branch_formatter_spec.rb b/spec/lib/gitlab/github_import/branch_formatter_spec.rb index e01a80054a3350..e5300dbba1ee49 100644 --- a/spec/lib/gitlab/github_import/branch_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/branch_formatter_spec.rb @@ -32,14 +32,6 @@ end end - describe '#name' do - it 'returns raw ref' do - branch = described_class.new(project, double(raw)) - - expect(branch.name).to eq 'feature' - end - end - describe '#repo' do it 'returns raw repo' do branch = described_class.new(project, double(raw)) diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb index ad79715a2d47b0..aa28e360993434 100644 --- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb @@ -9,6 +9,7 @@ let(:source_branch) { double(ref: 'feature', repo: source_repo, sha: source_sha) } let(:target_repo) { repository } let(:target_branch) { double(ref: 'master', repo: target_repo, sha: target_sha) } + let(:removed_branch) { double(ref: 'removed-branch', repo: source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b') } let(:octocat) { double(id: 123456, login: 'octocat') } let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } @@ -165,6 +166,42 @@ end end + describe '#source_branch_name' do + context 'when source branch exists' do + let(:raw_data) { double(base_data) } + + it 'returns branch ref' do + expect(pull_request.source_branch_name).to eq 'feature' + end + end + + context 'when source branch does not exist' do + let(:raw_data) { double(base_data.merge(head: removed_branch)) } + + it 'prefixes branch name with pull request number' do + expect(pull_request.source_branch_name).to eq 'pull/1347/removed-branch' + end + end + end + + describe '#target_branch_name' do + context 'when source branch exists' do + let(:raw_data) { double(base_data) } + + it 'returns branch ref' do + expect(pull_request.target_branch_name).to eq 'master' + end + end + + context 'when target branch does not exist' do + let(:raw_data) { double(base_data.merge(base: removed_branch)) } + + it 'prefixes branch name with pull request number' do + expect(pull_request.target_branch_name).to eq 'pull/1347/removed-branch' + end + end + end + describe '#valid?' do context 'when source, and target repos are not a fork' do let(:raw_data) { double(base_data) } -- GitLab From e0a858efcc66246b2811f89b04b3479089345476 Mon Sep 17 00:00:00 2001 From: winniehell Date: Mon, 8 Aug 2016 09:47:17 +0200 Subject: [PATCH 083/153] use long options for curl examples in API documentation (!5703) --- CHANGELOG | 1 + doc/api/README.md | 6 +++--- doc/api/award_emoji.md | 16 ++++++++-------- doc/api/branches.md | 12 ++++++------ doc/api/build_triggers.md | 8 ++++---- doc/api/build_variables.md | 10 +++++----- doc/api/builds.md | 20 ++++++++++---------- doc/api/ci/builds.md | 12 ++++++------ doc/api/ci/runners.md | 4 ++-- doc/api/commits.md | 14 +++++++------- doc/api/deploy_key_multiple_projects.md | 8 ++++---- doc/api/deploy_keys.md | 14 +++++++------- doc/api/enviroments.md | 8 ++++---- doc/api/groups.md | 4 ++-- doc/api/issues.md | 22 +++++++++++----------- doc/api/labels.md | 12 ++++++------ doc/api/licenses.md | 2 +- doc/api/merge_requests.md | 10 +++++----- doc/api/milestones.md | 2 +- doc/api/namespaces.md | 4 ++-- doc/api/notes.md | 6 +++--- doc/api/oauth2.md | 2 +- doc/api/projects.md | 8 ++++---- doc/api/repository_files.md | 8 ++++---- doc/api/runners.md | 16 ++++++++-------- doc/api/session.md | 2 +- doc/api/settings.md | 4 ++-- doc/api/sidekiq_metrics.md | 8 ++++---- doc/api/system_hooks.md | 8 ++++---- doc/api/tags.md | 2 +- doc/api/todos.md | 6 +++--- doc/ci/examples/php.md | 4 ++-- doc/ci/triggers/README.md | 20 ++++++++++---------- doc/development/doc_styleguide.md | 14 +++++++------- doc/install/installation.md | 6 +++--- doc/monitoring/health_check.md | 4 ++-- doc/update/4.0-to-4.1.md | 2 +- doc/update/4.2-to-5.0.md | 2 +- doc/update/5.0-to-5.1.md | 2 +- doc/update/5.2-to-5.3.md | 2 +- doc/update/5.3-to-5.4.md | 2 +- doc/update/6.9-to-7.0.md | 2 +- doc/update/7.0-to-7.1.md | 2 +- doc/update/7.14-to-8.0.md | 2 +- 44 files changed, 162 insertions(+), 161 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 951c30d8eefdca..3f11ba019ddf35 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -7,6 +7,7 @@ v 8.11.0 (unreleased) - Improve diff performance by eliminating redundant checks for text blobs - Convert switch icon into icon font (ClemMakesApps) - API: Endpoints for enabling and disabling deploy keys + - Use long options for curl examples in documentation !5703 (winniehell) - Remove magic comments (`# encoding: UTF-8`) from Ruby files. !5456 (winniehell) - Add support for relative links starting with ./ or / to RelativeLinkFilter (winniehell) - Ignore URLs starting with // in Markdown links !5677 (winniehell) diff --git a/doc/api/README.md b/doc/api/README.md index 21141d350cf166..a357af3831d7ad 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -74,7 +74,7 @@ You can use an OAuth 2 token to authenticate with the API by passing it either i Example of using the OAuth2 token in the header: ```shell -curl -H "Authorization: Bearer OAUTH-TOKEN" https://gitlab.example.com/api/v3/projects +curl --header "Authorization: Bearer OAUTH-TOKEN" https://gitlab.example.com/api/v3/projects ``` Read more about [GitLab as an OAuth2 client](oauth2.md). @@ -204,7 +204,7 @@ resources you can pass the following parameters: In the example below, we list 50 [namespaces](namespaces.md) per page. ```bash -curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/namespaces?per_page=50 +curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/namespaces?per_page=50 ``` ### Pagination Link header @@ -218,7 +218,7 @@ and we request the second page (`page=2`) of [comments](notes.md) of the issue with ID `8` which belongs to the project with ID `8`: ```bash -curl -I -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/8/issues/8/notes?per_page=3&page=2 +curl --head --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/8/issues/8/notes?per_page=3&page=2 ``` The response will then be: diff --git a/doc/api/award_emoji.md b/doc/api/award_emoji.md index 158fb189005fec..72ec99b7c56f3f 100644 --- a/doc/api/award_emoji.md +++ b/doc/api/award_emoji.md @@ -25,7 +25,7 @@ Parameters: | `awardable_id` | integer | yes | The ID of an awardable | ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji ``` Example Response: @@ -85,7 +85,7 @@ Parameters: | `award_id` | integer | yes | The ID of the award emoji | ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/1 +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/1 ``` Example Response: @@ -127,7 +127,7 @@ Parameters: | `name` | string | yes | The name of the emoji, without colons | ```bash -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji?name=blowfish +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji?name=blowfish ``` Example Response: @@ -170,7 +170,7 @@ Parameters: | `award_id` | integer | yes | The ID of a award_emoji | ```bash -curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/344 +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/344 ``` Example Response: @@ -217,7 +217,7 @@ Parameters: ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/notes/1/award_emoji +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/notes/1/award_emoji ``` Example Response: @@ -259,7 +259,7 @@ Parameters: | `award_id` | integer | yes | The ID of the award emoji | ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/notes/1/award_emoji/2 +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/notes/1/award_emoji/2 ``` Example Response: @@ -299,7 +299,7 @@ Parameters: | `name` | string | yes | The name of the emoji, without colons | ```bash -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/notes/1/award_emoji?name=rocket +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/notes/1/award_emoji?name=rocket ``` Example Response: @@ -342,7 +342,7 @@ Parameters: | `award_id` | integer | yes | The ID of a award_emoji | ```bash -curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/345 +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/345 ``` Example Response: diff --git a/doc/api/branches.md b/doc/api/branches.md index dbe8306c66f42b..0b5f7778fc764e 100644 --- a/doc/api/branches.md +++ b/doc/api/branches.md @@ -13,7 +13,7 @@ GET /projects/:id/repository/branches | `id` | integer | yes | The ID of a project | ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches ``` Example response: @@ -57,7 +57,7 @@ GET /projects/:id/repository/branches/:branch | `branch` | string | yes | The name of the branch | ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches/master +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches/master ``` Example response: @@ -95,7 +95,7 @@ PUT /projects/:id/repository/branches/:branch/protect ``` ```bash -curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches/master/protect?developers_can_push=true&developers_can_merge=true +curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches/master/protect?developers_can_push=true&developers_can_merge=true ``` | Attribute | Type | Required | Description | @@ -140,7 +140,7 @@ PUT /projects/:id/repository/branches/:branch/unprotect ``` ```bash -curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches/master/unprotect +curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches/master/unprotect ``` | Attribute | Type | Required | Description | @@ -185,7 +185,7 @@ POST /projects/:id/repository/branches | `ref` | string | yes | The branch name or commit SHA to create branch from | ```bash -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/branches?branch_name=newbranch&ref=master" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/branches?branch_name=newbranch&ref=master" ``` Example response: @@ -230,7 +230,7 @@ It returns `200` if it succeeds, `404` if the branch to be deleted does not exis or `400` for other reasons. In case of an error, an explaining message is provided. ```bash -curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/branches/newbranch" +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/branches/newbranch" ``` Example response: diff --git a/doc/api/build_triggers.md b/doc/api/build_triggers.md index 0881a7d7a90408..1b7a18401384dd 100644 --- a/doc/api/build_triggers.md +++ b/doc/api/build_triggers.md @@ -15,7 +15,7 @@ GET /projects/:id/triggers | `id` | integer | yes | The ID of a project | ``` -curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers" +curl --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers" ``` ```json @@ -51,7 +51,7 @@ GET /projects/:id/triggers/:token | `token` | string | yes | The `token` of a trigger | ``` -curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers/7b9148c158980bbd9bcea92c17522d" +curl --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers/7b9148c158980bbd9bcea92c17522d" ``` ```json @@ -77,7 +77,7 @@ POST /projects/:id/triggers | `id` | integer | yes | The ID of a project | ``` -curl -X POST -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers" +curl --request POST --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers" ``` ```json @@ -104,7 +104,7 @@ DELETE /projects/:id/triggers/:token | `token` | string | yes | The `token` of a trigger | ``` -curl -X DELETE -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers/7b9148c158980bbd9bcea92c17522d" +curl --request DELETE --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers/7b9148c158980bbd9bcea92c17522d" ``` ```json diff --git a/doc/api/build_variables.md b/doc/api/build_variables.md index b96f1bdac8ab09..a21751a49eabd2 100644 --- a/doc/api/build_variables.md +++ b/doc/api/build_variables.md @@ -13,7 +13,7 @@ GET /projects/:id/variables | `id` | integer | yes | The ID of a project | ``` -curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables" +curl --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables" ``` ```json @@ -43,7 +43,7 @@ GET /projects/:id/variables/:key | `key` | string | yes | The `key` of a variable | ``` -curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/TEST_VARIABLE_1" +curl --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/TEST_VARIABLE_1" ``` ```json @@ -68,7 +68,7 @@ POST /projects/:id/variables | `value` | string | yes | The `value` of a variable | ``` -curl -X POST -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables" -F "key=NEW_VARIABLE" -F "value=new value" +curl --request POST --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables" --form "key=NEW_VARIABLE" --form "value=new value" ``` ```json @@ -93,7 +93,7 @@ PUT /projects/:id/variables/:key | `value` | string | yes | The `value` of a variable | ``` -curl -X PUT -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/NEW_VARIABLE" -F "value=updated value" +curl --request PUT --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/NEW_VARIABLE" --form "value=updated value" ``` ```json @@ -117,7 +117,7 @@ DELETE /projects/:id/variables/:key | `key` | string | yes | The `key` of a variable | ``` -curl -X DELETE -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/VARIABLE_1" +curl --request DELETE --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/VARIABLE_1" ``` ```json diff --git a/doc/api/builds.md b/doc/api/builds.md index 24d90e22a9b676..8864df03c98c4e 100644 --- a/doc/api/builds.md +++ b/doc/api/builds.md @@ -14,7 +14,7 @@ GET /projects/:id/builds | `scope` | string **or** array of strings | no | The scope of builds to show, one or array of: `pending`, `running`, `failed`, `success`, `canceled`; showing all builds if none provided | ``` -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds" ``` Example of response @@ -123,7 +123,7 @@ GET /projects/:id/repository/commits/:sha/builds | `scope` | string **or** array of strings | no | The scope of builds to show, one or array of: `pending`, `running`, `failed`, `success`, `canceled`; showing all builds if none provided | ``` -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/repository/commits/0ff3ae198f8601a285adcf5c0fff204ee6fba5fd/builds" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/repository/commits/0ff3ae198f8601a285adcf5c0fff204ee6fba5fd/builds" ``` Example of response @@ -209,7 +209,7 @@ GET /projects/:id/builds/:build_id | `build_id` | integer | yes | The ID of a build | ``` -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8" ``` Example of response @@ -271,7 +271,7 @@ GET /projects/:id/builds/:build_id/artifacts | `build_id` | integer | yes | The ID of a build | ``` -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8/artifacts" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8/artifacts" ``` Response: @@ -305,7 +305,7 @@ Parameters Example request: ``` -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/artifacts/master/download?job=test" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/artifacts/master/download?job=test" ``` Example response: @@ -331,7 +331,7 @@ GET /projects/:id/builds/:build_id/trace | build_id | integer | yes | The ID of a build | ``` -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8/trace" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8/trace" ``` Response: @@ -355,7 +355,7 @@ POST /projects/:id/builds/:build_id/cancel | `build_id` | integer | yes | The ID of a build | ``` -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/cancel" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/cancel" ``` Example of response @@ -401,7 +401,7 @@ POST /projects/:id/builds/:build_id/retry | `build_id` | integer | yes | The ID of a build | ``` -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/retry" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/retry" ``` Example of response @@ -451,7 +451,7 @@ Parameters Example of request ``` -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/erase" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/erase" ``` Example of response @@ -501,7 +501,7 @@ Parameters Example request: ``` -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/artifacts/keep" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/artifacts/keep" ``` Example response: diff --git a/doc/api/ci/builds.md b/doc/api/ci/builds.md index d779463fd8cb04..2a71b087f193a0 100644 --- a/doc/api/ci/builds.md +++ b/doc/api/ci/builds.md @@ -35,7 +35,7 @@ POST /ci/api/v1/builds/register ``` -curl -X POST "https://gitlab.example.com/ci/api/v1/builds/register" -F "token=t0k3n" +curl --request POST "https://gitlab.example.com/ci/api/v1/builds/register" --form "token=t0k3n" ``` ### Update details of an existing build @@ -52,7 +52,7 @@ PUT /ci/api/v1/builds/:id | `trace` | string | no | The trace of a build | ``` -curl -X PUT "https://gitlab.example.com/ci/api/v1/builds/1234" -F "token=t0k3n" -F "state=running" -F "trace=Running git clone...\n" +curl --request PUT "https://gitlab.example.com/ci/api/v1/builds/1234" --form "token=t0k3n" --form "state=running" --form "trace=Running git clone...\n" ``` ### Incremental build trace update @@ -87,7 +87,7 @@ Headers: | `Content-Range` | string | yes | Bytes range of trace that is sent | ``` -curl -X PATCH "https://gitlab.example.com/ci/api/v1/builds/1234/trace.txt" -H "BUILD-TOKEN=build_t0k3n" -H "Content-Range=0-21" -d "Running git clone...\n" +curl --request PATCH "https://gitlab.example.com/ci/api/v1/builds/1234/trace.txt" --header "BUILD-TOKEN=build_t0k3n" --header "Content-Range=0-21" --data "Running git clone...\n" ``` @@ -104,7 +104,7 @@ POST /ci/api/v1/builds/:id/artifacts | `file` | mixed | yes | Artifacts file | ``` -curl -X POST "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" -F "token=build_t0k3n" -F "file=@/path/to/file" +curl --request POST "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" --form "token=build_t0k3n" --form "file=@/path/to/file" ``` ### Download the artifacts file from build @@ -119,7 +119,7 @@ GET /ci/api/v1/builds/:id/artifacts | `token` | string | yes | The build authorization token | ``` -curl "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" -F "token=build_t0k3n" +curl "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" --form "token=build_t0k3n" ``` ### Remove the artifacts file from build @@ -134,5 +134,5 @@ DELETE /ci/api/v1/builds/:id/artifacts | `token` | string | yes | The build authorization token | ``` -curl -X DELETE "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" -F "token=build_t0k3n" +curl --request DELETE "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" --form "token=build_t0k3n" ``` diff --git a/doc/api/ci/runners.md b/doc/api/ci/runners.md index 96b3c42f773a69..ecec53fde03718 100644 --- a/doc/api/ci/runners.md +++ b/doc/api/ci/runners.md @@ -35,7 +35,7 @@ POST /ci/api/v1/runners/register Example request: ```sh -curl -X POST "https://gitlab.example.com/ci/api/v1/runners/register" -F "token=t0k3n" +curl --request POST "https://gitlab.example.com/ci/api/v1/runners/register" --form "token=t0k3n" ``` ## Delete a Runner @@ -53,5 +53,5 @@ DELETE /ci/api/v1/runners/delete Example request: ```sh -curl -X DELETE "https://gitlab.example.com/ci/api/v1/runners/delete" -F "token=t0k3n" +curl --request DELETE "https://gitlab.example.com/ci/api/v1/runners/delete" --form "token=t0k3n" ``` diff --git a/doc/api/commits.md b/doc/api/commits.md index 2960c2ae428db0..5c98c5d7565b58 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -16,7 +16,7 @@ GET /projects/:id/repository/commits | `until` | string | no | Only commits before or in this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ | ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits" ``` Example response: @@ -62,7 +62,7 @@ Parameters: | `sha` | string | yes | The commit hash or name of a repository branch or tag | ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master ``` Example response: @@ -106,7 +106,7 @@ Parameters: | `sha` | string | yes | The commit hash or name of a repository branch or tag | ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master/diff" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master/diff" ``` Example response: @@ -142,7 +142,7 @@ Parameters: | `sha` | string | yes | The commit hash or name of a repository branch or tag | ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master/comments" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master/comments" ``` Example response: @@ -195,7 +195,7 @@ POST /projects/:id/repository/commits/:sha/comments | `line_type` | string | no | The line type. Takes `new` or `old` as arguments | ```bash -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -F "note=Nice picture man\!" -F "path=dudeism.md" -F "line=11" -F "line_type=new" https://gitlab.example.com/api/v3/projects/17/repository/commits/18f3e63d05582537db6d183d9d557be09e1f90c8/comments +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "note=Nice picture man\!" --form "path=dudeism.md" --form "line=11" --form "line_type=new" https://gitlab.example.com/api/v3/projects/17/repository/commits/18f3e63d05582537db6d183d9d557be09e1f90c8/comments ``` Example response: @@ -240,7 +240,7 @@ GET /projects/:id/repository/commits/:sha/statuses | `all` | boolean | no | Return all statuses, not only the latest ones ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/17/repository/commits/18f3e63d05582537db6d183d9d557be09e1f90c8/statuses +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/17/repository/commits/18f3e63d05582537db6d183d9d557be09e1f90c8/statuses ``` Example response: @@ -315,7 +315,7 @@ POST /projects/:id/statuses/:sha | `description` | string | no | The short description of the status ```bash -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/17/statuses/18f3e63d05582537db6d183d9d557be09e1f90c8?state=success" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/17/statuses/18f3e63d05582537db6d183d9d557be09e1f90c8?state=success" ``` Example response: diff --git a/doc/api/deploy_key_multiple_projects.md b/doc/api/deploy_key_multiple_projects.md index 9280f0d68b62ec..73cb4b7ea8c277 100644 --- a/doc/api/deploy_key_multiple_projects.md +++ b/doc/api/deploy_key_multiple_projects.md @@ -7,23 +7,23 @@ First, find the ID of the projects you're interested in, by either listing all projects: ``` -curl -H 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' https://gitlab.example.com/api/v3/projects +curl --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' https://gitlab.example.com/api/v3/projects ``` Or finding the ID of a group and then listing all projects in that group: ``` -curl -H 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' https://gitlab.example.com/api/v3/groups +curl --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' https://gitlab.example.com/api/v3/groups # For group 1234: -curl -H 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' https://gitlab.example.com/api/v3/groups/1234 +curl --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' https://gitlab.example.com/api/v3/groups/1234 ``` With those IDs, add the same deploy key to all: ``` for project_id in 321 456 987; do - curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -H "Content-Type: application/json" \ + curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "Content-Type: application/json" \ --data '{"title": "my key", "key": "ssh-rsa AAAA..."}' https://gitlab.example.com/api/v3/projects/${project_id}/deploy_keys done ``` diff --git a/doc/api/deploy_keys.md b/doc/api/deploy_keys.md index a288de5fc9773e..ca44afbf355bde 100644 --- a/doc/api/deploy_keys.md +++ b/doc/api/deploy_keys.md @@ -9,7 +9,7 @@ GET /deploy_keys ``` ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/deploy_keys" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/deploy_keys" ``` Example response: @@ -44,7 +44,7 @@ GET /projects/:id/deploy_keys | `id` | integer | yes | The ID of the project | ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/deploy_keys" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/deploy_keys" ``` Example response: @@ -82,7 +82,7 @@ Parameters: | `key_id` | integer | yes | The ID of the deploy key | ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/deploy_keys/11" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/deploy_keys/11" ``` Example response: @@ -114,7 +114,7 @@ POST /projects/:id/deploy_keys | `key` | string | yes | New deploy key | ```bash -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -H "Content-Type: application/json" --data '{"title": "My deploy key", "key": "ssh-rsa AAAA..."}' "https://gitlab.example.com/api/v3/projects/5/deploy_keys/" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "Content-Type: application/json" --data '{"title": "My deploy key", "key": "ssh-rsa AAAA..."}' "https://gitlab.example.com/api/v3/projects/5/deploy_keys/" ``` Example response: @@ -142,7 +142,7 @@ DELETE /projects/:id/deploy_keys/:key_id | `key_id` | integer | yes | The ID of the deploy key | ```bash -curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/deploy_keys/13" +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/deploy_keys/13" ``` Example response: @@ -165,7 +165,7 @@ Example response: Enables a deploy key for a project so this can be used. Returns the enabled key, with a status code 201 when successful. ```bash -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/deploy_keys/13/enable +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/deploy_keys/13/enable ``` | Attribute | Type | Required | Description | @@ -189,7 +189,7 @@ Example response: Disable a deploy key for a project. Returns the disabled key. ```bash -curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/deploy_keys/13/disable +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/deploy_keys/13/disable ``` | Attribute | Type | Required | Description | diff --git a/doc/api/enviroments.md b/doc/api/enviroments.md index 1e12ded448c09b..87a5fa67124996 100644 --- a/doc/api/enviroments.md +++ b/doc/api/enviroments.md @@ -13,7 +13,7 @@ GET /projects/:id/environments | `id` | integer | yes | The ID of the project | ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/1/environments +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/1/environments ``` Example response: @@ -45,7 +45,7 @@ POST /projects/:id/environment | `external_url` | string | no | Place to link to for this environment | ```bash -curl --data "name=deploy&external_url=https://deploy.example.gitlab.com" -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environments" +curl --data "name=deploy&external_url=https://deploy.example.gitlab.com" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environments" ``` Example response: @@ -76,7 +76,7 @@ PUT /projects/:id/environments/:environments_id | `external_url` | string | no | The new external_url | ```bash -curl -X PUT --data "name=staging&external_url=https://staging.example.gitlab.com" -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environment/1" +curl --request PUT --data "name=staging&external_url=https://staging.example.gitlab.com" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environment/1" ``` Example response: @@ -103,7 +103,7 @@ DELETE /projects/:id/environments/:environment_id | `environment_id` | integer | yes | The ID of the environment | ```bash -curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environment/1" +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environment/1" ``` Example response: diff --git a/doc/api/groups.md b/doc/api/groups.md index 87480bebfc43ac..f4398d5f198f59 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -104,7 +104,7 @@ Parameters: | `id` | integer/string | yes | The ID or path of a group | ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/4 +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/4 ``` Example response: @@ -319,7 +319,7 @@ PUT /groups/:id | `visibility_level` | integer | no | The visibility level of the group. 0 for private, 10 for internal, 20 for public. | ```bash -curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/groups/5?name=Experimental" +curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/groups/5?name=Experimental" ``` diff --git a/doc/api/issues.md b/doc/api/issues.md index 419fb8f85d8ace..a665645ad0ef61 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -33,7 +33,7 @@ GET /issues?labels=foo,bar&state=opened | `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` | ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/issues +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/issues ``` Example response: @@ -110,7 +110,7 @@ GET /groups/:id/issues?milestone=1.0.0&state=opened ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/4/issues +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/4/issues ``` Example response: @@ -189,7 +189,7 @@ GET /projects/:id/issues?iid=42 ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues ``` Example response: @@ -254,7 +254,7 @@ GET /projects/:id/issues/:issue_id | `issue_id`| integer | yes | The ID of a project's issue | ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/41 +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/41 ``` Example response: @@ -327,7 +327,7 @@ POST /projects/:id/issues | `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` | ```bash -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues?title=Issues%20with%20auth&labels=bug +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues?title=Issues%20with%20auth&labels=bug ``` Example response: @@ -388,7 +388,7 @@ PUT /projects/:id/issues/:issue_id | `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` | ```bash -curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85?state_event=close +curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85?state_event=close ``` Example response: @@ -438,7 +438,7 @@ DELETE /projects/:id/issues/:issue_id | `issue_id` | integer | yes | The ID of a project's issue | ```bash -curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85 +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85 ``` ## Move an issue @@ -463,7 +463,7 @@ POST /projects/:id/issues/:issue_id/move | `to_project_id` | integer | yes | The ID of the new project | ```bash -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85/move +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85/move ``` Example response: @@ -518,7 +518,7 @@ POST /projects/:id/issues/:issue_id/subscription | `issue_id` | integer | yes | The ID of a project's issue | ```bash -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/subscription +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/subscription ``` Example response: @@ -573,7 +573,7 @@ DELETE /projects/:id/issues/:issue_id/subscription | `issue_id` | integer | yes | The ID of a project's issue | ```bash -curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/subscription +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/subscription ``` Example response: @@ -628,7 +628,7 @@ POST /projects/:id/issues/:issue_id/todo | `issue_id` | integer | yes | The ID of a project's issue | ```bash -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/todo +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/todo ``` Example response: diff --git a/doc/api/labels.md b/doc/api/labels.md index a181c0f57a2764..3653ccf304acf2 100644 --- a/doc/api/labels.md +++ b/doc/api/labels.md @@ -13,7 +13,7 @@ GET /projects/:id/labels | `id` | integer | yes | The ID of the project | ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/1/labels +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/1/labels ``` Example response: @@ -82,7 +82,7 @@ POST /projects/:id/labels | `description` | string | no | The description of the label | ```bash -curl --data "name=feature&color=#5843AD" -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels" +curl --data "name=feature&color=#5843AD" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels" ``` Example response: @@ -113,7 +113,7 @@ DELETE /projects/:id/labels | `name` | string | yes | The name of the label | ```bash -curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels?name=bug" +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels?name=bug" ``` Example response: @@ -153,7 +153,7 @@ PUT /projects/:id/labels | `description` | string | no | The new description of the label | ```bash -curl -X PUT --data "name=documentation&new_name=docs&color=#8E44AD&description=Documentation" -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels" +curl --request PUT --data "name=documentation&new_name=docs&color=#8E44AD&description=Documentation" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels" ``` Example response: @@ -184,7 +184,7 @@ POST /projects/:id/labels/:label_id/subscription | `label_id` | integer or string | yes | The ID or title of a project's label | ```bash -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/labels/1/subscription +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/labels/1/subscription ``` Example response: @@ -219,7 +219,7 @@ DELETE /projects/:id/labels/:label_id/subscription | `label_id` | integer or string | yes | The ID or title of a project's label | ```bash -curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/labels/1/subscription +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/labels/1/subscription ``` Example response: diff --git a/doc/api/licenses.md b/doc/api/licenses.md index 855b0eab56fe26..ed26d1fb7fbf77 100644 --- a/doc/api/licenses.md +++ b/doc/api/licenses.md @@ -116,7 +116,7 @@ If you omit the `fullname` parameter but authenticate your request, the name of the authenticated user will be used to replace the copyright holder placeholder. ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/licenses/mit?project=My+Cool+Project +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/licenses/mit?project=My+Cool+Project ``` Example response: diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index e00882e6d5db22..3e88a758936594 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -418,7 +418,7 @@ DELETE /projects/:id/merge_requests/:merge_request_id | `merge_request_id` | integer | yes | The ID of a project's merge request | ```bash -curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/merge_request/85 +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/merge_request/85 ``` ## Accept MR @@ -587,7 +587,7 @@ GET /projects/:id/merge_requests/:merge_request_id/closes_issues | `merge_request_id` | integer | yes | The ID of the merge request | ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/76/merge_requests/1/closes_issues +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/76/merge_requests/1/closes_issues ``` Example response when the GitLab issue tracker is used: @@ -665,7 +665,7 @@ POST /projects/:id/merge_requests/:merge_request_id/subscription | `merge_request_id` | integer | yes | The ID of the merge request | ```bash -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/17/subscription +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/17/subscription ``` Example response: @@ -739,7 +739,7 @@ DELETE /projects/:id/merge_requests/:merge_request_id/subscription | `merge_request_id` | integer | yes | The ID of the merge request | ```bash -curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/17/subscription +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/17/subscription ``` Example response: @@ -812,7 +812,7 @@ POST /projects/:id/merge_requests/:merge_request_id/todo | `merge_request_id` | integer | yes | The ID of the merge request | ```bash -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/27/todo +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/27/todo ``` Example response: diff --git a/doc/api/milestones.md b/doc/api/milestones.md index e4202025f8022a..ae7d22a4be554d 100644 --- a/doc/api/milestones.md +++ b/doc/api/milestones.md @@ -20,7 +20,7 @@ Parameters: | `state` | string | optional | Return only `active` or `closed` milestones` | ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/milestones +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/milestones ``` Example Response: diff --git a/doc/api/namespaces.md b/doc/api/namespaces.md index 42d9ce3d3915d8..88cd407d792b3e 100644 --- a/doc/api/namespaces.md +++ b/doc/api/namespaces.md @@ -19,7 +19,7 @@ GET /namespaces Example request: ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/namespaces +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/namespaces ``` Example response: @@ -54,7 +54,7 @@ GET /namespaces?search=foobar Example request: ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/namespaces?search=twitter +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/namespaces?search=twitter ``` Example response: diff --git a/doc/api/notes.md b/doc/api/notes.md index 7aa1c2155bfe8d..85d140d06acfe5 100644 --- a/doc/api/notes.md +++ b/doc/api/notes.md @@ -124,7 +124,7 @@ Parameters: | `note_id` | integer | yes | The ID of a note | ```bash -curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/11/notes/636 +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/11/notes/636 ``` Example Response: @@ -248,7 +248,7 @@ Parameters: | `note_id` | integer | yes | The ID of a note | ```bash -curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/snippets/52/notes/1659 +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/snippets/52/notes/1659 ``` Example Response: @@ -376,7 +376,7 @@ Parameters: | `note_id` | integer | yes | The ID of a note | ```bash -curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/7/notes/1602 +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/7/notes/1602 ``` Example Response: diff --git a/doc/api/oauth2.md b/doc/api/oauth2.md index 7ce89adc98b763..2e6542281d2a0e 100644 --- a/doc/api/oauth2.md +++ b/doc/api/oauth2.md @@ -60,7 +60,7 @@ GET https://localhost:3000/api/v3/user?access_token=OAUTH-TOKEN Or you can put the token to the Authorization header: ``` -curl -H "Authorization: Bearer OAUTH-TOKEN" https://localhost:3000/api/v3/user +curl --header "Authorization: Bearer OAUTH-TOKEN" https://localhost:3000/api/v3/user ``` ## Resource Owner Password Credentials diff --git a/doc/api/projects.md b/doc/api/projects.md index 0ba0bffb4ac38f..727cb44f335074 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -529,7 +529,7 @@ POST /projects/:id/star | `id` | integer | yes | The ID of the project | ```bash -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star" ``` Example response: @@ -595,7 +595,7 @@ DELETE /projects/:id/star | `id` | integer | yes | The ID of the project | ```bash -curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star" +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star" ``` Example response: @@ -665,7 +665,7 @@ POST /projects/:id/archive | `id` | integer | yes | The ID of the project | ```bash -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/archive" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/archive" ``` Example response: @@ -751,7 +751,7 @@ POST /projects/:id/unarchive | `id` | integer | yes | The ID of the project | ```bash -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/unarchive" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/unarchive" ``` Example response: diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md index 1b8ee88b4edf80..fc3af5544de021 100644 --- a/doc/api/repository_files.md +++ b/doc/api/repository_files.md @@ -13,7 +13,7 @@ GET /projects/:id/repository/files ``` ```bash -curl -X GET -H 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/models/key.rb&ref=master' +curl --request GET --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/models/key.rb&ref=master' ``` Example response: @@ -44,7 +44,7 @@ POST /projects/:id/repository/files ``` ```bash -curl -X POST -H 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch_name=master&content=some%20content&commit_message=create%20a%20new%20file' +curl --request POST --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch_name=master&content=some%20content&commit_message=create%20a%20new%20file' ``` Example response: @@ -71,7 +71,7 @@ PUT /projects/:id/repository/files ``` ```bash -curl -X PUT -H 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch_name=master&content=some%20other%20content&commit_message=update%20file' +curl --request PUT --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch_name=master&content=some%20other%20content&commit_message=update%20file' ``` Example response: @@ -107,7 +107,7 @@ DELETE /projects/:id/repository/files ``` ```bash -curl -X PUT -H 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch_name=master&commit_message=delete%20file' +curl --request PUT --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch_name=master&commit_message=delete%20file' ``` Example response: diff --git a/doc/api/runners.md b/doc/api/runners.md index ddfa298f79d008..28610762dca875 100644 --- a/doc/api/runners.md +++ b/doc/api/runners.md @@ -18,7 +18,7 @@ GET /runners?scope=active | `scope` | string | no | The scope of specific runners to show, one of: `active`, `paused`, `online`; showing all runners if none provided | ``` -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners" ``` Example response: @@ -57,7 +57,7 @@ GET /runners/all?scope=online | `scope` | string | no | The scope of runners to show, one of: `specific`, `shared`, `active`, `paused`, `online`; showing all runners if none provided | ``` -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/all" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/all" ``` Example response: @@ -108,7 +108,7 @@ GET /runners/:id | `id` | integer | yes | The ID of a runner | ``` -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/6" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/6" ``` Example response: @@ -158,7 +158,7 @@ PUT /runners/:id | `tag_list` | array | no | The list of tags for a runner; put array of tags, that should be finally assigned to a runner | ``` -curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/6" -F "description=test-1-20150125-test" -F "tag_list=ruby,mysql,tag1,tag2" +curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/6" --form "description=test-1-20150125-test" --form "tag_list=ruby,mysql,tag1,tag2" ``` Example response: @@ -207,7 +207,7 @@ DELETE /runners/:id | `id` | integer | yes | The ID of a runner | ``` -curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/6" +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/6" ``` Example response: @@ -237,7 +237,7 @@ GET /projects/:id/runners | `id` | integer | yes | The ID of a project | ``` -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners" ``` Example response: @@ -275,7 +275,7 @@ POST /projects/:id/runners | `runner_id` | integer | yes | The ID of a runner | ``` -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners" -F "runner_id=9" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners" --form "runner_id=9" ``` Example response: @@ -306,7 +306,7 @@ DELETE /projects/:id/runners/:runner_id | `runner_id` | integer | yes | The ID of a runner | ``` -curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners/9" +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners/9" ``` Example response: diff --git a/doc/api/session.md b/doc/api/session.md index 066a055702df22..9076c48b899dec 100644 --- a/doc/api/session.md +++ b/doc/api/session.md @@ -21,7 +21,7 @@ POST /session | `password` | string | yes | The password of the user | ```bash -curl -X POST "https://gitlab.example.com/api/v3/session?login=john_smith&password=strongpassw0rd" +curl --request POST "https://gitlab.example.com/api/v3/session?login=john_smith&password=strongpassw0rd" ``` Example response: diff --git a/doc/api/settings.md b/doc/api/settings.md index ea39b32561c380..a76dad0ebd47b9 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -13,7 +13,7 @@ GET /application/settings ``` ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/application/settings +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/application/settings ``` Example response: @@ -75,7 +75,7 @@ PUT /application/settings | `enabled_git_access_protocol` | string | no | Enabled protocols for Git access. Allowed values are: `ssh`, `http`, and `nil` to allow both protocols. ```bash -curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/application/settings?signup_enabled=false&default_project_visibility=1 +curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/application/settings?signup_enabled=false&default_project_visibility=1 ``` Example response: diff --git a/doc/api/sidekiq_metrics.md b/doc/api/sidekiq_metrics.md index ebd131c94ca960..1ae732d40d6cf9 100644 --- a/doc/api/sidekiq_metrics.md +++ b/doc/api/sidekiq_metrics.md @@ -15,7 +15,7 @@ GET /sidekiq/queue_metrics ``` ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/queue_metrics +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/queue_metrics ``` Example response: @@ -40,7 +40,7 @@ GET /sidekiq/process_metrics ``` ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/process_metrics +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/process_metrics ``` Example response: @@ -82,7 +82,7 @@ GET /sidekiq/job_stats ``` ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/job_stats +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/job_stats ``` Example response: @@ -106,7 +106,7 @@ GET /sidekiq/compound_metrics ``` ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/compound_metrics +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/compound_metrics ``` Example response: diff --git a/doc/api/system_hooks.md b/doc/api/system_hooks.md index dc036d7e27fce4..1802fae14feb8c 100644 --- a/doc/api/system_hooks.md +++ b/doc/api/system_hooks.md @@ -20,7 +20,7 @@ GET /hooks Example request: ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/hooks +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/hooks ``` Example response: @@ -52,7 +52,7 @@ POST /hooks Example request: ```bash -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/hooks?url=https://gitlab.example.com/hook" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/hooks?url=https://gitlab.example.com/hook" ``` Example response: @@ -80,7 +80,7 @@ GET /hooks/:id Example request: ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/hooks/2 +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/hooks/2 ``` Example response: @@ -117,7 +117,7 @@ DELETE /hooks/:id Example request: ```bash -curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/hooks/2 +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/hooks/2 ``` Example response: diff --git a/doc/api/tags.md b/doc/api/tags.md index ac9fac92f4cc24..5405911745653f 100644 --- a/doc/api/tags.md +++ b/doc/api/tags.md @@ -56,7 +56,7 @@ Parameters: | `tag_name` | string | yes | The name of the tag | ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/tags/v1.0.0 +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/tags/v1.0.0 ``` Example Response: diff --git a/doc/api/todos.md b/doc/api/todos.md index c9e1e83e28a354..0cd644dfd2fe2b 100644 --- a/doc/api/todos.md +++ b/doc/api/todos.md @@ -22,7 +22,7 @@ Parameters: | `type` | string | no | The type of a todo. Can be either `Issue` or `MergeRequest` | ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos ``` Example Response: @@ -194,7 +194,7 @@ Parameters: | `id` | integer | yes | The ID of a todo | ```bash -curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos/130 +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos/130 ``` Example Response: @@ -284,7 +284,7 @@ DELETE /todos ``` ```bash -curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos ``` Example Response: diff --git a/doc/ci/examples/php.md b/doc/ci/examples/php.md index bfafcc44d66ef9..175e9d79904e19 100644 --- a/doc/ci/examples/php.md +++ b/doc/ci/examples/php.md @@ -49,7 +49,7 @@ apt-get update -yqq apt-get install git -yqq # Install phpunit, the tool that we will use for testing -curl -Lo /usr/local/bin/phpunit https://phar.phpunit.de/phpunit.phar +curl --location --output /usr/local/bin/phpunit https://phar.phpunit.de/phpunit.phar chmod +x /usr/local/bin/phpunit # Install mysql driver @@ -235,7 +235,7 @@ cache: before_script: # Install composer dependencies -- curl -sS https://getcomposer.org/installer | php +- curl --silent --show-error https://getcomposer.org/installer | php - php composer.phar install ... diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md index 57a12526363b09..6c6767fea0b14e 100644 --- a/doc/ci/triggers/README.md +++ b/doc/ci/triggers/README.md @@ -77,9 +77,9 @@ See the [Examples](#examples) section below for more details. Using cURL you can trigger a rebuild with minimal effort, for example: ```bash -curl -X POST \ - -F token=TOKEN \ - -F ref=master \ +curl --request POST \ + --form token=TOKEN \ + --form ref=master \ https://gitlab.example.com/api/v3/projects/9/trigger/builds ``` @@ -88,7 +88,7 @@ In this case, the project with ID `9` will get rebuilt on `master` branch. Alternatively, you can pass the `token` and `ref` arguments in the query string: ```bash -curl -X POST \ +curl --request POST \ "https://gitlab.example.com/api/v3/projects/9/trigger/builds?token=TOKEN&ref=master" ``` @@ -103,7 +103,7 @@ need to add in project's A `.gitlab-ci.yml`: build_docs: stage: deploy script: - - "curl -X POST -F token=TOKEN -F ref=master https://gitlab.example.com/api/v3/projects/9/trigger/builds" + - "curl --request POST --form token=TOKEN --form ref=master https://gitlab.example.com/api/v3/projects/9/trigger/builds" only: - tags ``` @@ -158,10 +158,10 @@ You can then trigger a rebuild while you pass the `UPLOAD_TO_S3` variable and the script of the `upload_package` job will run: ```bash -curl -X POST \ - -F token=TOKEN \ - -F ref=master \ - -F "variables[UPLOAD_TO_S3]=true" \ +curl --request POST \ + --form token=TOKEN \ + --form ref=master \ + --form "variables[UPLOAD_TO_S3]=true" \ https://gitlab.example.com/api/v3/projects/9/trigger/builds ``` @@ -172,7 +172,7 @@ in conjunction with cron. The example below triggers a build on the `master` branch of project with ID `9` every night at `00:30`: ```bash -30 0 * * * curl -X POST -F token=TOKEN -F ref=master https://gitlab.example.com/api/v3/projects/9/trigger/builds +30 0 * * * curl --request POST --form token=TOKEN --form ref=master https://gitlab.example.com/api/v3/projects/9/trigger/builds ``` [ci-229]: https://gitlab.com/gitlab-org/gitlab-ci/merge_requests/229 diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md index 994005f929f2ad..927a1872413541 100644 --- a/doc/development/doc_styleguide.md +++ b/doc/development/doc_styleguide.md @@ -355,7 +355,7 @@ Below is a set of [cURL][] examples that you can use in the API documentation. Get the details of a group: ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/gitlab-org +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/gitlab-org ``` #### cURL example with parameters passed in the URL @@ -363,7 +363,7 @@ curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/ Create a new project under the authenticated user's namespace: ```bash -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects?name=foo" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects?name=foo" ``` #### Post data using cURL's --data @@ -373,7 +373,7 @@ cURL's `--data` option. The example below will create a new project `foo` under the authenticated user's namespace. ```bash -curl --data "name=foo" -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects" +curl --data "name=foo" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects" ``` #### Post data using JSON content @@ -382,7 +382,7 @@ curl --data "name=foo" -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab. and double quotes. ```bash -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -H "Content-Type: application/json" --data '{"path": "my-group", "name": "My group"}' https://gitlab.example.com/api/v3/groups +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "Content-Type: application/json" --data '{"path": "my-group", "name": "My group"}' https://gitlab.example.com/api/v3/groups ``` #### Post data using form-data @@ -391,7 +391,7 @@ Instead of using JSON or urlencode you can use multipart/form-data which properly handles data encoding: ```bash -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -F "title=ssh-key" -F "key=ssh-rsa AAAAB3NzaC1yc2EA..." https://gitlab.example.com/api/v3/users/25/keys +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "title=ssh-key" --form "key=ssh-rsa AAAAB3NzaC1yc2EA..." https://gitlab.example.com/api/v3/users/25/keys ``` The above example is run by and administrator and will add an SSH public key @@ -405,7 +405,7 @@ contains spaces in its title. Observe how spaces are escaped using the `%20` ASCII code. ```bash -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/42/issues?title=Hello%20Dude" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/42/issues?title=Hello%20Dude" ``` Use `%2F` for slashes (`/`). @@ -417,7 +417,7 @@ restrict the sign-up e-mail domains of a GitLab instance to `*.example.com` and `example.net`, you would do something like this: ```bash -curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -d "domain_whitelist[]=*.example.com" -d "domain_whitelist[]=example.net" https://gitlab.example.com/api/v3/application/settings +curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "domain_whitelist[]=*.example.com" --data "domain_whitelist[]=example.net" https://gitlab.example.com/api/v3/application/settings ``` [cURL]: http://curl.haxx.se/ "cURL website" diff --git a/doc/install/installation.md b/doc/install/installation.md index af8e31a705bfeb..8f0dd3f2016097 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -89,7 +89,7 @@ Is the system packaged Git too old? Remove it and compile from source. # Download and compile from source cd /tmp - curl -O --progress https://www.kernel.org/pub/software/scm/git/git-2.7.4.tar.gz + curl --remote-name --progress https://www.kernel.org/pub/software/scm/git/git-2.7.4.tar.gz echo '7104c4f5d948a75b499a954524cb281fe30c6649d8abe20982936f75ec1f275b git-2.7.4.tar.gz' | shasum -a256 -c - && tar -xzf git-2.7.4.tar.gz cd git-2.7.4/ ./configure @@ -124,7 +124,7 @@ Remove the old Ruby 1.8 if present: Download Ruby and compile it: mkdir /tmp/ruby && cd /tmp/ruby - curl -O --progress https://cache.ruby-lang.org/pub/ruby/2.1/ruby-2.1.8.tar.gz + curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.1/ruby-2.1.8.tar.gz echo 'c7e50159357afd87b13dc5eaf4ac486a70011149 ruby-2.1.8.tar.gz' | shasum -c - && tar xzf ruby-2.1.8.tar.gz cd ruby-2.1.8 ./configure --disable-install-rdoc @@ -143,7 +143,7 @@ gitlab-workhorse we need a Go compiler. The instructions below assume you use 64-bit Linux. You can find downloads for other platforms at the [Go download page](https://golang.org/dl). - curl -O --progress https://storage.googleapis.com/golang/go1.5.3.linux-amd64.tar.gz + curl --remote-name --progress https://storage.googleapis.com/golang/go1.5.3.linux-amd64.tar.gz echo '43afe0c5017e502630b1aea4d44b8a7f059bf60d7f29dfd58db454d4e4e0ae53 go1.5.3.linux-amd64.tar.gz' | shasum -a256 -c - && \ sudo tar -C /usr/local -xzf go1.5.3.linux-amd64.tar.gz sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/ diff --git a/doc/monitoring/health_check.md b/doc/monitoring/health_check.md index 70326f1ff80da5..eac57bc3de4b9d 100644 --- a/doc/monitoring/health_check.md +++ b/doc/monitoring/health_check.md @@ -24,7 +24,7 @@ https://gitlab.example.com/health_check.json?token=ACCESS_TOKEN or as an HTTP header: ```bash -curl -H "TOKEN: ACCESS_TOKEN" https://gitlab.example.com/health_check.json +curl --header "TOKEN: ACCESS_TOKEN" https://gitlab.example.com/health_check.json ``` ## Using the Endpoint @@ -45,7 +45,7 @@ You can also ask for the status of specific services: For example, the JSON output of the following health check: ```bash -curl -H "TOKEN: ACCESS_TOKEN" https://gitlab.example.com/health_check.json +curl --header "TOKEN: ACCESS_TOKEN" https://gitlab.example.com/health_check.json ``` would be like: diff --git a/doc/update/4.0-to-4.1.md b/doc/update/4.0-to-4.1.md index c163bfd348d285..c66c6dd0fd8d9a 100644 --- a/doc/update/4.0-to-4.1.md +++ b/doc/update/4.0-to-4.1.md @@ -42,7 +42,7 @@ sudo -u gitlab -H bundle exec rake db:migrate RAILS_ENV=production sudo mv /etc/init.d/gitlab /etc/init.d/gitlab.old # get new one using sidekiq -sudo curl -L --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlab-recipes/4-1-stable/init.d/gitlab +sudo curl --location --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlab-recipes/4-1-stable/init.d/gitlab sudo chmod +x /etc/init.d/gitlab ``` diff --git a/doc/update/4.2-to-5.0.md b/doc/update/4.2-to-5.0.md index ee6de51c923320..7654f4a0131d13 100644 --- a/doc/update/4.2-to-5.0.md +++ b/doc/update/4.2-to-5.0.md @@ -126,7 +126,7 @@ sudo chmod -R u+rwX /home/git/gitlab/tmp/pids ```bash # init.d sudo rm /etc/init.d/gitlab -sudo curl -L --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlab-recipes/5-0-stable/init.d/gitlab +sudo curl --location --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlab-recipes/5-0-stable/init.d/gitlab sudo chmod +x /etc/init.d/gitlab # unicorn diff --git a/doc/update/5.0-to-5.1.md b/doc/update/5.0-to-5.1.md index f0fddcf83afd9e..c19a819ab5a3f8 100644 --- a/doc/update/5.0-to-5.1.md +++ b/doc/update/5.0-to-5.1.md @@ -63,7 +63,7 @@ sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production ```bash # init.d sudo rm /etc/init.d/gitlab -sudo curl -L --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlab-recipes/5-1-stable/init.d/gitlab +sudo curl --location --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlab-recipes/5-1-stable/init.d/gitlab sudo chmod +x /etc/init.d/gitlab ``` diff --git a/doc/update/5.2-to-5.3.md b/doc/update/5.2-to-5.3.md index c5254f6fb0c73f..fe8990b6843ccf 100644 --- a/doc/update/5.2-to-5.3.md +++ b/doc/update/5.2-to-5.3.md @@ -67,7 +67,7 @@ sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production ```bash sudo rm /etc/init.d/gitlab -sudo curl -L --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlabhq/5-3-stable/lib/support/init.d/gitlab +sudo curl --location --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlabhq/5-3-stable/lib/support/init.d/gitlab sudo chmod +x /etc/init.d/gitlab ``` diff --git a/doc/update/5.3-to-5.4.md b/doc/update/5.3-to-5.4.md index c4a6146dcda3cb..5f82ad7d444723 100644 --- a/doc/update/5.3-to-5.4.md +++ b/doc/update/5.3-to-5.4.md @@ -71,7 +71,7 @@ sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production ```bash sudo rm /etc/init.d/gitlab -sudo curl -L --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlabhq/5-4-stable/lib/support/init.d/gitlab +sudo curl --location --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlabhq/5-4-stable/lib/support/init.d/gitlab sudo chmod +x /etc/init.d/gitlab ``` diff --git a/doc/update/6.9-to-7.0.md b/doc/update/6.9-to-7.0.md index 236430b5951618..5352fd52f93ac2 100644 --- a/doc/update/6.9-to-7.0.md +++ b/doc/update/6.9-to-7.0.md @@ -33,7 +33,7 @@ Download and compile Ruby: ```bash mkdir /tmp/ruby && cd /tmp/ruby -curl -L --progress ftp://ftp.ruby-lang.org/pub/ruby/2.1/ruby-2.1.2.tar.gz | tar xz +curl --location --progress ftp://ftp.ruby-lang.org/pub/ruby/2.1/ruby-2.1.2.tar.gz | tar xz cd ruby-2.1.2 ./configure --disable-install-rdoc make diff --git a/doc/update/7.0-to-7.1.md b/doc/update/7.0-to-7.1.md index a4e9be9946e07f..71f39c44077e34 100644 --- a/doc/update/7.0-to-7.1.md +++ b/doc/update/7.0-to-7.1.md @@ -33,7 +33,7 @@ Download and compile Ruby: ```bash mkdir /tmp/ruby && cd /tmp/ruby -curl -L --progress ftp://ftp.ruby-lang.org/pub/ruby/2.1/ruby-2.1.2.tar.gz | tar xz +curl --location --progress ftp://ftp.ruby-lang.org/pub/ruby/2.1/ruby-2.1.2.tar.gz | tar xz cd ruby-2.1.2 ./configure --disable-install-rdoc make diff --git a/doc/update/7.14-to-8.0.md b/doc/update/7.14-to-8.0.md index 305017b704816c..117e2afaaa0b94 100644 --- a/doc/update/7.14-to-8.0.md +++ b/doc/update/7.14-to-8.0.md @@ -71,7 +71,7 @@ sudo -u git -H git checkout v2.6.5 First we download Go 1.5 and install it into `/usr/local/go`: ```bash -curl -O --progress https://storage.googleapis.com/golang/go1.5.linux-amd64.tar.gz +curl --remote-name --progress https://storage.googleapis.com/golang/go1.5.linux-amd64.tar.gz echo '5817fa4b2252afdb02e11e8b9dc1d9173ef3bd5a go1.5.linux-amd64.tar.gz' | shasum -c - && \ sudo tar -C /usr/local -xzf go1.5.linux-amd64.tar.gz sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/ -- GitLab From 314647459c07e9f920dfcf43a8644e2199f1132c Mon Sep 17 00:00:00 2001 From: winniehell Date: Mon, 8 Aug 2016 09:49:54 +0200 Subject: [PATCH 084/153] use Unix line endings for API documentation --- doc/api/groups.md | 1028 ++++++++++++++++++++++----------------------- 1 file changed, 514 insertions(+), 514 deletions(-) diff --git a/doc/api/groups.md b/doc/api/groups.md index f4398d5f198f59..fd665e967a9957 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -1,514 +1,514 @@ -# Groups - -## List groups - -Get a list of groups. (As user: my groups, as admin: all groups) - -``` -GET /groups -``` - -```json -[ - { - "id": 1, - "name": "Foobar Group", - "path": "foo-bar", - "description": "An interesting group" - } -] -``` - -You can search for groups by name or path, see below. - - -## List a group's projects - -Get a list of projects in this group. - -``` -GET /groups/:id/projects -``` - -Parameters: - -- `archived` (optional) - if passed, limit by archived status -- `visibility` (optional) - if passed, limit by visibility `public`, `internal`, `private` -- `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at` -- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc` -- `search` (optional) - Return list of authorized projects according to a search criteria -- `ci_enabled_first` - Return projects ordered by ci_enabled flag. Projects with enabled GitLab CI go first - -```json -[ - { - "id": 9, - "description": "foo", - "default_branch": "master", - "tag_list": [], - "public": false, - "archived": false, - "visibility_level": 10, - "ssh_url_to_repo": "git@gitlab.example.com/html5-boilerplate.git", - "http_url_to_repo": "http://gitlab.example.com/h5bp/html5-boilerplate.git", - "web_url": "http://gitlab.example.com/h5bp/html5-boilerplate", - "name": "Html5 Boilerplate", - "name_with_namespace": "Experimental / Html5 Boilerplate", - "path": "html5-boilerplate", - "path_with_namespace": "h5bp/html5-boilerplate", - "issues_enabled": true, - "merge_requests_enabled": true, - "wiki_enabled": true, - "builds_enabled": true, - "snippets_enabled": true, - "created_at": "2016-04-05T21:40:50.169Z", - "last_activity_at": "2016-04-06T16:52:08.432Z", - "shared_runners_enabled": true, - "creator_id": 1, - "namespace": { - "id": 5, - "name": "Experimental", - "path": "h5bp", - "owner_id": null, - "created_at": "2016-04-05T21:40:49.152Z", - "updated_at": "2016-04-07T08:07:48.466Z", - "description": "foo", - "avatar": { - "url": null - }, - "share_with_group_lock": false, - "visibility_level": 10 - }, - "avatar_url": null, - "star_count": 1, - "forks_count": 0, - "open_issues_count": 3, - "public_builds": true, - "shared_with_groups": [] - } -] -``` - -## Details of a group - -Get all details of a group. - -``` -GET /groups/:id -``` - -Parameters: - -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or path of a group | - -```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/4 -``` - -Example response: - -```json -{ - "id": 4, - "name": "Twitter", - "path": "twitter", - "description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.", - "visibility_level": 20, - "avatar_url": null, - "web_url": "https://gitlab.example.com/groups/twitter", - "projects": [ - { - "id": 7, - "description": "Voluptas veniam qui et beatae voluptas doloremque explicabo facilis.", - "default_branch": "master", - "tag_list": [], - "public": true, - "archived": false, - "visibility_level": 20, - "ssh_url_to_repo": "git@gitlab.example.com:twitter/typeahead-js.git", - "http_url_to_repo": "https://gitlab.example.com/twitter/typeahead-js.git", - "web_url": "https://gitlab.example.com/twitter/typeahead-js", - "name": "Typeahead.Js", - "name_with_namespace": "Twitter / Typeahead.Js", - "path": "typeahead-js", - "path_with_namespace": "twitter/typeahead-js", - "issues_enabled": true, - "merge_requests_enabled": true, - "wiki_enabled": true, - "builds_enabled": true, - "snippets_enabled": false, - "container_registry_enabled": true, - "created_at": "2016-06-17T07:47:25.578Z", - "last_activity_at": "2016-06-17T07:47:25.881Z", - "shared_runners_enabled": true, - "creator_id": 1, - "namespace": { - "id": 4, - "name": "Twitter", - "path": "twitter", - "owner_id": null, - "created_at": "2016-06-17T07:47:24.216Z", - "updated_at": "2016-06-17T07:47:24.216Z", - "description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.", - "avatar": { - "url": null - }, - "share_with_group_lock": false, - "visibility_level": 20 - }, - "avatar_url": null, - "star_count": 0, - "forks_count": 0, - "open_issues_count": 3, - "public_builds": true, - "shared_with_groups": [] - }, - { - "id": 6, - "description": "Aspernatur omnis repudiandae qui voluptatibus eaque.", - "default_branch": "master", - "tag_list": [], - "public": false, - "archived": false, - "visibility_level": 10, - "ssh_url_to_repo": "git@gitlab.example.com:twitter/flight.git", - "http_url_to_repo": "https://gitlab.example.com/twitter/flight.git", - "web_url": "https://gitlab.example.com/twitter/flight", - "name": "Flight", - "name_with_namespace": "Twitter / Flight", - "path": "flight", - "path_with_namespace": "twitter/flight", - "issues_enabled": true, - "merge_requests_enabled": true, - "wiki_enabled": true, - "builds_enabled": true, - "snippets_enabled": false, - "container_registry_enabled": true, - "created_at": "2016-06-17T07:47:24.661Z", - "last_activity_at": "2016-06-17T07:47:24.838Z", - "shared_runners_enabled": true, - "creator_id": 1, - "namespace": { - "id": 4, - "name": "Twitter", - "path": "twitter", - "owner_id": null, - "created_at": "2016-06-17T07:47:24.216Z", - "updated_at": "2016-06-17T07:47:24.216Z", - "description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.", - "avatar": { - "url": null - }, - "share_with_group_lock": false, - "visibility_level": 20 - }, - "avatar_url": null, - "star_count": 0, - "forks_count": 0, - "open_issues_count": 8, - "public_builds": true, - "shared_with_groups": [] - } - ], - "shared_projects": [ - { - "id": 8, - "description": "Velit eveniet provident fugiat saepe eligendi autem.", - "default_branch": "master", - "tag_list": [], - "public": false, - "archived": false, - "visibility_level": 0, - "ssh_url_to_repo": "git@gitlab.example.com:h5bp/html5-boilerplate.git", - "http_url_to_repo": "https://gitlab.example.com/h5bp/html5-boilerplate.git", - "web_url": "https://gitlab.example.com/h5bp/html5-boilerplate", - "name": "Html5 Boilerplate", - "name_with_namespace": "H5bp / Html5 Boilerplate", - "path": "html5-boilerplate", - "path_with_namespace": "h5bp/html5-boilerplate", - "issues_enabled": true, - "merge_requests_enabled": true, - "wiki_enabled": true, - "builds_enabled": true, - "snippets_enabled": false, - "container_registry_enabled": true, - "created_at": "2016-06-17T07:47:27.089Z", - "last_activity_at": "2016-06-17T07:47:27.310Z", - "shared_runners_enabled": true, - "creator_id": 1, - "namespace": { - "id": 5, - "name": "H5bp", - "path": "h5bp", - "owner_id": null, - "created_at": "2016-06-17T07:47:26.621Z", - "updated_at": "2016-06-17T07:47:26.621Z", - "description": "Id consequatur rem vel qui doloremque saepe.", - "avatar": { - "url": null - }, - "share_with_group_lock": false, - "visibility_level": 20 - }, - "avatar_url": null, - "star_count": 0, - "forks_count": 0, - "open_issues_count": 4, - "public_builds": true, - "shared_with_groups": [ - { - "group_id": 4, - "group_name": "Twitter", - "group_access_level": 30 - }, - { - "group_id": 3, - "group_name": "Gitlab Org", - "group_access_level": 10 - } - ] - } - ] -} -``` - -## New group - -Creates a new project group. Available only for users who can create groups. - -``` -POST /groups -``` - -Parameters: - -- `name` (required) - The name of the group -- `path` (required) - The path of the group -- `description` (optional) - The group's description -- `visibility_level` (optional) - The group's visibility. 0 for private, 10 for internal, 20 for public. - -## Transfer project to group - -Transfer a project to the Group namespace. Available only for admin - -``` -POST /groups/:id/projects/:project_id -``` - -Parameters: - -- `id` (required) - The ID or path of a group -- `project_id` (required) - The ID of a project - -## Update group - -Updates the project group. Only available to group owners and administrators. - -``` -PUT /groups/:id -``` - -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of the group | -| `name` | string | no | The name of the group | -| `path` | string | no | The path of the group | -| `description` | string | no | The description of the group | -| `visibility_level` | integer | no | The visibility level of the group. 0 for private, 10 for internal, 20 for public. | - -```bash -curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/groups/5?name=Experimental" - -``` - -Example response: - -```json -{ - "id": 5, - "name": "Experimental", - "path": "h5bp", - "description": "foo", - "visibility_level": 10, - "avatar_url": null, - "web_url": "http://gitlab.example.com/groups/h5bp", - "projects": [ - { - "id": 9, - "description": "foo", - "default_branch": "master", - "tag_list": [], - "public": false, - "archived": false, - "visibility_level": 10, - "ssh_url_to_repo": "git@gitlab.example.com/html5-boilerplate.git", - "http_url_to_repo": "http://gitlab.example.com/h5bp/html5-boilerplate.git", - "web_url": "http://gitlab.example.com/h5bp/html5-boilerplate", - "name": "Html5 Boilerplate", - "name_with_namespace": "Experimental / Html5 Boilerplate", - "path": "html5-boilerplate", - "path_with_namespace": "h5bp/html5-boilerplate", - "issues_enabled": true, - "merge_requests_enabled": true, - "wiki_enabled": true, - "builds_enabled": true, - "snippets_enabled": true, - "created_at": "2016-04-05T21:40:50.169Z", - "last_activity_at": "2016-04-06T16:52:08.432Z", - "shared_runners_enabled": true, - "creator_id": 1, - "namespace": { - "id": 5, - "name": "Experimental", - "path": "h5bp", - "owner_id": null, - "created_at": "2016-04-05T21:40:49.152Z", - "updated_at": "2016-04-07T08:07:48.466Z", - "description": "foo", - "avatar": { - "url": null - }, - "share_with_group_lock": false, - "visibility_level": 10 - }, - "avatar_url": null, - "star_count": 1, - "forks_count": 0, - "open_issues_count": 3, - "public_builds": true, - "shared_with_groups": [] - } - ] -} -``` - -## Remove group - -Removes group with all projects inside. - -``` -DELETE /groups/:id -``` - -Parameters: - -- `id` (required) - The ID or path of a user group - -## Search for group - -Get all groups that match your string in their name or path. - -``` -GET /groups?search=foobar -``` - -```json -[ - { - "id": 1, - "name": "Foobar Group", - "path": "foo-bar", - "description": "An interesting group" - } -] -``` - -## Group members - -**Group access levels** - -The group access levels are defined in the `Gitlab::Access` module. Currently, these levels are recognized: - -``` -GUEST = 10 -REPORTER = 20 -DEVELOPER = 30 -MASTER = 40 -OWNER = 50 -``` - -### List group members - -Get a list of group members viewable by the authenticated user. - -``` -GET /groups/:id/members -``` - -```json -[ - { - "id": 1, - "username": "raymond_smith", - "name": "Raymond Smith", - "state": "active", - "created_at": "2012-10-22T14:13:35Z", - "access_level": 30 - }, - { - "id": 2, - "username": "john_doe", - "name": "John Doe", - "state": "active", - "created_at": "2012-10-22T14:13:35Z", - "access_level": 30 - } -] -``` - -### Add group member - -Adds a user to the list of group members. - -``` -POST /groups/:id/members -``` - -Parameters: - -- `id` (required) - The ID or path of a group -- `user_id` (required) - The ID of a user to add -- `access_level` (required) - Project access level - -### Edit group team member - -Updates a group team member to a specified access level. - -``` -PUT /groups/:id/members/:user_id -``` - -Parameters: - -- `id` (required) - The ID of a group -- `user_id` (required) - The ID of a group member -- `access_level` (required) - Project access level - -### Remove user team member - -Removes user from user team. - -``` -DELETE /groups/:id/members/:user_id -``` - -Parameters: - -- `id` (required) - The ID or path of a user group -- `user_id` (required) - The ID of a group member - -## Namespaces in groups - -By default, groups only get 20 namespaces at a time because the API results are paginated. - -To get more (up to 100), pass the following as an argument to the API call: -``` -/groups?per_page=100 -``` - -And to switch pages add: -``` -/groups?per_page=100&page=2 -``` +# Groups + +## List groups + +Get a list of groups. (As user: my groups, as admin: all groups) + +``` +GET /groups +``` + +```json +[ + { + "id": 1, + "name": "Foobar Group", + "path": "foo-bar", + "description": "An interesting group" + } +] +``` + +You can search for groups by name or path, see below. + + +## List a group's projects + +Get a list of projects in this group. + +``` +GET /groups/:id/projects +``` + +Parameters: + +- `archived` (optional) - if passed, limit by archived status +- `visibility` (optional) - if passed, limit by visibility `public`, `internal`, `private` +- `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at` +- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc` +- `search` (optional) - Return list of authorized projects according to a search criteria +- `ci_enabled_first` - Return projects ordered by ci_enabled flag. Projects with enabled GitLab CI go first + +```json +[ + { + "id": 9, + "description": "foo", + "default_branch": "master", + "tag_list": [], + "public": false, + "archived": false, + "visibility_level": 10, + "ssh_url_to_repo": "git@gitlab.example.com/html5-boilerplate.git", + "http_url_to_repo": "http://gitlab.example.com/h5bp/html5-boilerplate.git", + "web_url": "http://gitlab.example.com/h5bp/html5-boilerplate", + "name": "Html5 Boilerplate", + "name_with_namespace": "Experimental / Html5 Boilerplate", + "path": "html5-boilerplate", + "path_with_namespace": "h5bp/html5-boilerplate", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "builds_enabled": true, + "snippets_enabled": true, + "created_at": "2016-04-05T21:40:50.169Z", + "last_activity_at": "2016-04-06T16:52:08.432Z", + "shared_runners_enabled": true, + "creator_id": 1, + "namespace": { + "id": 5, + "name": "Experimental", + "path": "h5bp", + "owner_id": null, + "created_at": "2016-04-05T21:40:49.152Z", + "updated_at": "2016-04-07T08:07:48.466Z", + "description": "foo", + "avatar": { + "url": null + }, + "share_with_group_lock": false, + "visibility_level": 10 + }, + "avatar_url": null, + "star_count": 1, + "forks_count": 0, + "open_issues_count": 3, + "public_builds": true, + "shared_with_groups": [] + } +] +``` + +## Details of a group + +Get all details of a group. + +``` +GET /groups/:id +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer/string | yes | The ID or path of a group | + +```bash +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/4 +``` + +Example response: + +```json +{ + "id": 4, + "name": "Twitter", + "path": "twitter", + "description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.", + "visibility_level": 20, + "avatar_url": null, + "web_url": "https://gitlab.example.com/groups/twitter", + "projects": [ + { + "id": 7, + "description": "Voluptas veniam qui et beatae voluptas doloremque explicabo facilis.", + "default_branch": "master", + "tag_list": [], + "public": true, + "archived": false, + "visibility_level": 20, + "ssh_url_to_repo": "git@gitlab.example.com:twitter/typeahead-js.git", + "http_url_to_repo": "https://gitlab.example.com/twitter/typeahead-js.git", + "web_url": "https://gitlab.example.com/twitter/typeahead-js", + "name": "Typeahead.Js", + "name_with_namespace": "Twitter / Typeahead.Js", + "path": "typeahead-js", + "path_with_namespace": "twitter/typeahead-js", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "builds_enabled": true, + "snippets_enabled": false, + "container_registry_enabled": true, + "created_at": "2016-06-17T07:47:25.578Z", + "last_activity_at": "2016-06-17T07:47:25.881Z", + "shared_runners_enabled": true, + "creator_id": 1, + "namespace": { + "id": 4, + "name": "Twitter", + "path": "twitter", + "owner_id": null, + "created_at": "2016-06-17T07:47:24.216Z", + "updated_at": "2016-06-17T07:47:24.216Z", + "description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.", + "avatar": { + "url": null + }, + "share_with_group_lock": false, + "visibility_level": 20 + }, + "avatar_url": null, + "star_count": 0, + "forks_count": 0, + "open_issues_count": 3, + "public_builds": true, + "shared_with_groups": [] + }, + { + "id": 6, + "description": "Aspernatur omnis repudiandae qui voluptatibus eaque.", + "default_branch": "master", + "tag_list": [], + "public": false, + "archived": false, + "visibility_level": 10, + "ssh_url_to_repo": "git@gitlab.example.com:twitter/flight.git", + "http_url_to_repo": "https://gitlab.example.com/twitter/flight.git", + "web_url": "https://gitlab.example.com/twitter/flight", + "name": "Flight", + "name_with_namespace": "Twitter / Flight", + "path": "flight", + "path_with_namespace": "twitter/flight", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "builds_enabled": true, + "snippets_enabled": false, + "container_registry_enabled": true, + "created_at": "2016-06-17T07:47:24.661Z", + "last_activity_at": "2016-06-17T07:47:24.838Z", + "shared_runners_enabled": true, + "creator_id": 1, + "namespace": { + "id": 4, + "name": "Twitter", + "path": "twitter", + "owner_id": null, + "created_at": "2016-06-17T07:47:24.216Z", + "updated_at": "2016-06-17T07:47:24.216Z", + "description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.", + "avatar": { + "url": null + }, + "share_with_group_lock": false, + "visibility_level": 20 + }, + "avatar_url": null, + "star_count": 0, + "forks_count": 0, + "open_issues_count": 8, + "public_builds": true, + "shared_with_groups": [] + } + ], + "shared_projects": [ + { + "id": 8, + "description": "Velit eveniet provident fugiat saepe eligendi autem.", + "default_branch": "master", + "tag_list": [], + "public": false, + "archived": false, + "visibility_level": 0, + "ssh_url_to_repo": "git@gitlab.example.com:h5bp/html5-boilerplate.git", + "http_url_to_repo": "https://gitlab.example.com/h5bp/html5-boilerplate.git", + "web_url": "https://gitlab.example.com/h5bp/html5-boilerplate", + "name": "Html5 Boilerplate", + "name_with_namespace": "H5bp / Html5 Boilerplate", + "path": "html5-boilerplate", + "path_with_namespace": "h5bp/html5-boilerplate", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "builds_enabled": true, + "snippets_enabled": false, + "container_registry_enabled": true, + "created_at": "2016-06-17T07:47:27.089Z", + "last_activity_at": "2016-06-17T07:47:27.310Z", + "shared_runners_enabled": true, + "creator_id": 1, + "namespace": { + "id": 5, + "name": "H5bp", + "path": "h5bp", + "owner_id": null, + "created_at": "2016-06-17T07:47:26.621Z", + "updated_at": "2016-06-17T07:47:26.621Z", + "description": "Id consequatur rem vel qui doloremque saepe.", + "avatar": { + "url": null + }, + "share_with_group_lock": false, + "visibility_level": 20 + }, + "avatar_url": null, + "star_count": 0, + "forks_count": 0, + "open_issues_count": 4, + "public_builds": true, + "shared_with_groups": [ + { + "group_id": 4, + "group_name": "Twitter", + "group_access_level": 30 + }, + { + "group_id": 3, + "group_name": "Gitlab Org", + "group_access_level": 10 + } + ] + } + ] +} +``` + +## New group + +Creates a new project group. Available only for users who can create groups. + +``` +POST /groups +``` + +Parameters: + +- `name` (required) - The name of the group +- `path` (required) - The path of the group +- `description` (optional) - The group's description +- `visibility_level` (optional) - The group's visibility. 0 for private, 10 for internal, 20 for public. + +## Transfer project to group + +Transfer a project to the Group namespace. Available only for admin + +``` +POST /groups/:id/projects/:project_id +``` + +Parameters: + +- `id` (required) - The ID or path of a group +- `project_id` (required) - The ID of a project + +## Update group + +Updates the project group. Only available to group owners and administrators. + +``` +PUT /groups/:id +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of the group | +| `name` | string | no | The name of the group | +| `path` | string | no | The path of the group | +| `description` | string | no | The description of the group | +| `visibility_level` | integer | no | The visibility level of the group. 0 for private, 10 for internal, 20 for public. | + +```bash +curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/groups/5?name=Experimental" + +``` + +Example response: + +```json +{ + "id": 5, + "name": "Experimental", + "path": "h5bp", + "description": "foo", + "visibility_level": 10, + "avatar_url": null, + "web_url": "http://gitlab.example.com/groups/h5bp", + "projects": [ + { + "id": 9, + "description": "foo", + "default_branch": "master", + "tag_list": [], + "public": false, + "archived": false, + "visibility_level": 10, + "ssh_url_to_repo": "git@gitlab.example.com/html5-boilerplate.git", + "http_url_to_repo": "http://gitlab.example.com/h5bp/html5-boilerplate.git", + "web_url": "http://gitlab.example.com/h5bp/html5-boilerplate", + "name": "Html5 Boilerplate", + "name_with_namespace": "Experimental / Html5 Boilerplate", + "path": "html5-boilerplate", + "path_with_namespace": "h5bp/html5-boilerplate", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "builds_enabled": true, + "snippets_enabled": true, + "created_at": "2016-04-05T21:40:50.169Z", + "last_activity_at": "2016-04-06T16:52:08.432Z", + "shared_runners_enabled": true, + "creator_id": 1, + "namespace": { + "id": 5, + "name": "Experimental", + "path": "h5bp", + "owner_id": null, + "created_at": "2016-04-05T21:40:49.152Z", + "updated_at": "2016-04-07T08:07:48.466Z", + "description": "foo", + "avatar": { + "url": null + }, + "share_with_group_lock": false, + "visibility_level": 10 + }, + "avatar_url": null, + "star_count": 1, + "forks_count": 0, + "open_issues_count": 3, + "public_builds": true, + "shared_with_groups": [] + } + ] +} +``` + +## Remove group + +Removes group with all projects inside. + +``` +DELETE /groups/:id +``` + +Parameters: + +- `id` (required) - The ID or path of a user group + +## Search for group + +Get all groups that match your string in their name or path. + +``` +GET /groups?search=foobar +``` + +```json +[ + { + "id": 1, + "name": "Foobar Group", + "path": "foo-bar", + "description": "An interesting group" + } +] +``` + +## Group members + +**Group access levels** + +The group access levels are defined in the `Gitlab::Access` module. Currently, these levels are recognized: + +``` +GUEST = 10 +REPORTER = 20 +DEVELOPER = 30 +MASTER = 40 +OWNER = 50 +``` + +### List group members + +Get a list of group members viewable by the authenticated user. + +``` +GET /groups/:id/members +``` + +```json +[ + { + "id": 1, + "username": "raymond_smith", + "name": "Raymond Smith", + "state": "active", + "created_at": "2012-10-22T14:13:35Z", + "access_level": 30 + }, + { + "id": 2, + "username": "john_doe", + "name": "John Doe", + "state": "active", + "created_at": "2012-10-22T14:13:35Z", + "access_level": 30 + } +] +``` + +### Add group member + +Adds a user to the list of group members. + +``` +POST /groups/:id/members +``` + +Parameters: + +- `id` (required) - The ID or path of a group +- `user_id` (required) - The ID of a user to add +- `access_level` (required) - Project access level + +### Edit group team member + +Updates a group team member to a specified access level. + +``` +PUT /groups/:id/members/:user_id +``` + +Parameters: + +- `id` (required) - The ID of a group +- `user_id` (required) - The ID of a group member +- `access_level` (required) - Project access level + +### Remove user team member + +Removes user from user team. + +``` +DELETE /groups/:id/members/:user_id +``` + +Parameters: + +- `id` (required) - The ID or path of a user group +- `user_id` (required) - The ID of a group member + +## Namespaces in groups + +By default, groups only get 20 namespaces at a time because the API results are paginated. + +To get more (up to 100), pass the following as an argument to the API call: +``` +/groups?per_page=100 +``` + +And to switch pages add: +``` +/groups?per_page=100&page=2 +``` -- GitLab From ec298011f2da1b94232e601c8e857ca180a2274d Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 9 Aug 2016 06:48:23 +0000 Subject: [PATCH 085/153] Rails prefers require_dependency so that it won't require twice: Closes #20724 --- lib/gitlab/email/receiver.rb | 2 +- lib/gitlab/request_profiler/middleware.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb index 9213cfb51e8354..a40c44eb1bc5fe 100644 --- a/lib/gitlab/email/receiver.rb +++ b/lib/gitlab/email/receiver.rb @@ -1,5 +1,5 @@ -require 'gitlab/email/handler' +require_dependency 'gitlab/email/handler' # Inspired in great part by Discourse's Email::Receiver module Gitlab diff --git a/lib/gitlab/request_profiler/middleware.rb b/lib/gitlab/request_profiler/middleware.rb index 4e787dc0656c68..786e1d49f5e048 100644 --- a/lib/gitlab/request_profiler/middleware.rb +++ b/lib/gitlab/request_profiler/middleware.rb @@ -1,5 +1,5 @@ require 'ruby-prof' -require 'gitlab/request_profiler' +require_dependency 'gitlab/request_profiler' module Gitlab module RequestProfiler -- GitLab From afd3aee387924c5af50f97b92a457e051507e662 Mon Sep 17 00:00:00 2001 From: winniehell Date: Tue, 9 Aug 2016 10:04:57 +0200 Subject: [PATCH 086/153] remove offending empty line --- .../merge_requests/merge_request_diff_cache_service_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb index 8f71d71b0f0f43..c4b874682751c5 100644 --- a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb +++ b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb @@ -1,7 +1,6 @@ require 'spec_helper' describe MergeRequests::MergeRequestDiffCacheService do - let(:subject) { MergeRequests::MergeRequestDiffCacheService.new } describe '#execute' do -- GitLab From f8e854798032d71e3b3cb2bcf49cf104e5d0e611 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 5 Aug 2016 10:56:27 +0200 Subject: [PATCH 087/153] fix MR source project assignment --- CHANGELOG | 1 + lib/gitlab/import_export/relation_factory.rb | 14 +++++++----- spec/lib/gitlab/import_export/project.json | 6 ++--- .../project_tree_restorer_spec.rb | 22 +++++++++++++++++++ 4 files changed, 34 insertions(+), 9 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 7bfeff2a4ec15a..ef4302da4be0e7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -82,6 +82,7 @@ v 8.11.0 (unreleased) - Adds support for pending invitation project members importing projects - Update devise initializer to turn on changed password notification emails. !5648 (tombell) - Avoid to show the original password field when password is automatically set. !5712 (duduribeiro) + - Fix importing GitLab projects with an invalid MR source project v 8.10.5 (unreleased) diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index 5e56b3d1aa7fd3..b0726268ca617c 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -102,17 +102,19 @@ def generate_imported_object def update_project_references project_id = @relation_hash.delete('project_id') + # If source and target are the same, populate them with the new project ID. + if @relation_hash['source_project_id'] + @relation_hash['source_project_id'] = same_source_and_target? ? project_id : -1 + end + # project_id may not be part of the export, but we always need to populate it if required. @relation_hash['project_id'] = project_id @relation_hash['gl_project_id'] = project_id if @relation_hash['gl_project_id'] @relation_hash['target_project_id'] = project_id if @relation_hash['target_project_id'] - @relation_hash['source_project_id'] = -1 if @relation_hash['source_project_id'] + end - # If source and target are the same, populate them with the new project ID. - if @relation_hash['source_project_id'] && @relation_hash['target_project_id'] && - @relation_hash['target_project_id'] == @relation_hash['source_project_id'] - @relation_hash['source_project_id'] = project_id - end + def same_source_and_target? + @relation_hash['target_project_id'] && @relation_hash['target_project_id'] == @relation_hash['source_project_id'] end def reset_ci_tokens diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index b5550ca196322a..cbbf98dca94f36 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -2393,7 +2393,7 @@ "source_project_id": 5, "author_id": 1, "assignee_id": null, - "title": "Cannot be automatically merged", + "title": "MR1", "created_at": "2016-06-14T15:02:36.568Z", "updated_at": "2016-06-14T15:02:56.815Z", "state": "opened", @@ -2827,10 +2827,10 @@ "id": 26, "target_branch": "master", "source_branch": "feature", - "source_project_id": 5, + "source_project_id": 4, "author_id": 1, "assignee_id": null, - "title": "Can be automatically merged", + "title": "MR2", "created_at": "2016-06-14T15:02:36.418Z", "updated_at": "2016-06-14T15:02:57.013Z", "state": "opened", diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index 5fdd4d5f25f15e..4d857945fdef66 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -71,6 +71,28 @@ expect(Milestone.find_by_description('test milestone').issues).not_to be_empty end + + context 'Merge requests' do + before do + restored_project_json + end + + it 'always has the new project as a target' do + expect(MergeRequest.find_by_title('MR1').target_project).to eq(project) + end + + it 'has the same source project as originally if source/target are the same' do + expect(MergeRequest.find_by_title('MR1').source_project).to eq(project) + end + + it 'has the new project as target if source/target differ' do + expect(MergeRequest.find_by_title('MR2').target_project).to eq(project) + end + + it 'has no source if source/target differ' do + expect(MergeRequest.find_by_title('MR2').source_project_id).to eq(-1) + end + end end end end -- GitLab From 57451f52cdbd527a980c0df75e1ee8bb7897d2e9 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 9 Aug 2016 11:29:32 +0200 Subject: [PATCH 088/153] Memoize CI config node validator to prevent leaks --- lib/gitlab/ci/config/node/validatable.rb | 10 ++++------ spec/lib/gitlab/ci/config/node/validatable_spec.rb | 4 ++++ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/gitlab/ci/config/node/validatable.rb b/lib/gitlab/ci/config/node/validatable.rb index f6e2896dfb23ce..085e6e988d1085 100644 --- a/lib/gitlab/ci/config/node/validatable.rb +++ b/lib/gitlab/ci/config/node/validatable.rb @@ -7,13 +7,11 @@ module Validatable class_methods do def validator - validator = Class.new(Node::Validator) - - if defined?(@validations) - @validations.each { |rules| validator.class_eval(&rules) } + @validator ||= Class.new(Node::Validator).tap do |validator| + if defined?(@validations) + @validations.each { |rules| validator.class_eval(&rules) } + end end - - validator end private diff --git a/spec/lib/gitlab/ci/config/node/validatable_spec.rb b/spec/lib/gitlab/ci/config/node/validatable_spec.rb index 10cd01afcd1da4..64b77fd6e0356f 100644 --- a/spec/lib/gitlab/ci/config/node/validatable_spec.rb +++ b/spec/lib/gitlab/ci/config/node/validatable_spec.rb @@ -23,6 +23,10 @@ .to be Gitlab::Ci::Config::Node::Validator end + it 'returns only one validator to mitigate leaks' do + expect { node.validator }.not_to change { node.validator } + end + context 'when validating node instance' do let(:node_instance) { node.new } -- GitLab From 7dff0946a7ea8c47cf531067ac752cf900927c44 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Tue, 9 Aug 2016 12:35:36 +0200 Subject: [PATCH 089/153] Remove duplicate method reintroduced by merge --- app/controllers/projects/git_http_controller.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb index a10dca6afaf8ad..b4373ef89efa65 100644 --- a/app/controllers/projects/git_http_controller.rb +++ b/app/controllers/projects/git_http_controller.rb @@ -59,10 +59,6 @@ def render_ok render json: Gitlab::Workhorse.git_http_ok(repository, user) end - def render_not_found - render plain: 'Not Found', status: :not_found - end - def render_http_not_allowed render plain: access_check.message, status: :forbidden end -- GitLab From 57df84d2fc5a03e0bf25a289dca715d6f0990d62 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 9 Aug 2016 13:33:19 +0200 Subject: [PATCH 090/153] Extend build badge specs to cover multiple pipelines --- spec/lib/gitlab/badge/build_spec.rb | 40 +++++++++++++++++++---------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/spec/lib/gitlab/badge/build_spec.rb b/spec/lib/gitlab/badge/build_spec.rb index f9abbdaf585fa9..ef9d9e7fef431d 100644 --- a/spec/lib/gitlab/badge/build_spec.rb +++ b/spec/lib/gitlab/badge/build_spec.rb @@ -53,6 +53,32 @@ end end end + + context 'when outdated pipeline for given ref exists' do + before do + build.success! + + old_build = create_build(project, '11eeffdd', branch) + old_build.drop! + end + + it 'does not take outdated pipeline into account' do + expect(badge.status).to eq 'success' + end + end + + context 'when multiple pipelines exist for given sha' do + before do + build.drop! + + new_build = create_build(project, sha, branch) + new_build.success! + end + + it 'reports the compound status' do + expect(badge.status).to eq 'failed' + end + end end context 'build does not exist' do @@ -69,20 +95,6 @@ end end - context 'when outdated pipeline for given ref exists' do - before do - build = create_build(project, sha, branch) - build.success! - - old_build = create_build(project, '11eeffdd', branch) - old_build.drop! - end - - it 'does not take outdated pipeline into account' do - expect(badge.status).to eq 'success' - end - end - def create_build(project, sha, branch) pipeline = create(:ci_pipeline, project: project, sha: sha, -- GitLab From 8abc757539454e13835073318f896796b1a85faf Mon Sep 17 00:00:00 2001 From: Adam Buckland Date: Wed, 27 Jul 2016 13:47:23 +0100 Subject: [PATCH 091/153] Update tree view to sort folders with submodules Currently trees are sorted in the fashion: - folders - files - submodules with each section sorted alphabetically This changes to this system: - folders and submodules (sorted together) - files --- CHANGELOG | 1 + app/helpers/tree_helper.rb | 18 ++---------- app/views/projects/tree/_tree_row.html.haml | 6 ++++ ...files_sort_submodules_with_folders_spec.rb | 29 +++++++++++++++++++ 4 files changed, 39 insertions(+), 15 deletions(-) create mode 100644 app/views/projects/tree/_tree_row.html.haml create mode 100644 spec/features/projects/files/files_sort_submodules_with_folders_spec.rb diff --git a/CHANGELOG b/CHANGELOG index 7bfeff2a4ec15a..73b2935968d16d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -82,6 +82,7 @@ v 8.11.0 (unreleased) - Adds support for pending invitation project members importing projects - Update devise initializer to turn on changed password notification emails. !5648 (tombell) - Avoid to show the original password field when password is automatically set. !5712 (duduribeiro) + - Sort folders with submodules in Files view !5521 v 8.10.5 (unreleased) diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index dbedf417fa5a1e..4a76c679bad5bd 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -4,23 +4,11 @@ module TreeHelper # # contents - A Grit::Tree object for the current tree def render_tree(tree) - # Render Folders before Files/Submodules + # Sort submodules and folders together by name ahead of files folders, files, submodules = tree.trees, tree.blobs, tree.submodules - tree = "" - - # Render folders if we have any - tree << render(partial: 'projects/tree/tree_item', collection: folders, - locals: { type: 'folder' }) if folders.present? - - # Render files if we have any - tree << render(partial: 'projects/tree/blob_item', collection: files, - locals: { type: 'file' }) if files.present? - - # Render submodules if we have any - tree << render(partial: 'projects/tree/submodule_item', - collection: submodules) if submodules.present? - + items = (folders + submodules).sort_by(&:name) + files + tree << render(partial: "projects/tree/tree_row", collection: items) if items.present? tree.html_safe end diff --git a/app/views/projects/tree/_tree_row.html.haml b/app/views/projects/tree/_tree_row.html.haml new file mode 100644 index 00000000000000..0a5c6f048f7202 --- /dev/null +++ b/app/views/projects/tree/_tree_row.html.haml @@ -0,0 +1,6 @@ +- if tree_row.type == :tree + = render partial: 'projects/tree/tree_item', object: tree_row, as: 'tree_item', locals: { type: 'folder' } +- elsif tree_row.type == :blob + = render partial: 'projects/tree/blob_item', object: tree_row, as: 'blob_item', locals: { type: 'file' } +- elsif tree_row.type == :commit + = render partial: 'projects/tree/submodule_item', object: tree_row, as: 'submodule_item' diff --git a/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb b/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb new file mode 100644 index 00000000000000..10b91d8990b823 --- /dev/null +++ b/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +feature 'User views files page', feature: true do + include WaitForAjax + + let(:user) { create(:user) } + let(:project) { create(:forked_project_with_submodules) } + + before do + project.team << [user, :master] + login_as user + visit namespace_project_tree_path(project.namespace, project, project.repository.root_ref) + end + + scenario 'user sees folders and submodules sorted together, followed by files' do + rows = all('td.tree-item-file-name').map(&:text) + tree = project.repository.tree + + folders = tree.trees.map(&:name) + files = tree.blobs.map(&:name) + submodules = tree.submodules.map do |submodule| + submodule.name + " @ " + submodule.id[0..7] + end + + sorted_titles = (folders + submodules).sort + files + + expect(rows).to eq(sorted_titles) + end +end -- GitLab From 2835688f89728a8b20352e8cb6d0270f451ed16a Mon Sep 17 00:00:00 2001 From: Mark Fletcher Date: Tue, 9 Aug 2016 14:12:27 +0100 Subject: [PATCH 092/153] Clarify the features for generating default label sets * Split out the information regarding the label set that GitLab can generate * Add information for the labels view in the Admin panel --- doc/user/admin_area/img/admin_labels.png | Bin 0 -> 91459 bytes doc/user/admin_area/labels.md | 9 +++++++ doc/user/project/labels.md | 32 ++++++++++++++++------- 3 files changed, 31 insertions(+), 10 deletions(-) create mode 100644 doc/user/admin_area/img/admin_labels.png create mode 100644 doc/user/admin_area/labels.md diff --git a/doc/user/admin_area/img/admin_labels.png b/doc/user/admin_area/img/admin_labels.png new file mode 100644 index 0000000000000000000000000000000000000000..1ee33a534abe4aca634869dea9330243ab3521f8 GIT binary patch literal 91459 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 IbxsLQ07+-+XaE2J literal 0 HcmV?d00001 diff --git a/doc/user/admin_area/labels.md b/doc/user/admin_area/labels.md new file mode 100644 index 00000000000000..9e2a89ebdf600f --- /dev/null +++ b/doc/user/admin_area/labels.md @@ -0,0 +1,9 @@ +# Labels + +## Default Labels + +### Define your own default Label Set + +Labels that are created within the Labels view on the Admin Dashboard will be automatically added to each new project. + +![Default label set](img/admin_labels.png) diff --git a/doc/user/project/labels.md b/doc/user/project/labels.md index 1259a16330bc61..0f7e9eede19ab5 100644 --- a/doc/user/project/labels.md +++ b/doc/user/project/labels.md @@ -22,26 +22,38 @@ created yet. ![Generate new labels](img/labels_generate.png) +Creating a new label from scratch is as easy as pressing the **New label** +button. From there on you can choose the name, give it an optional description, +a color and you are set. + +When you are ready press the **Create label** button to create the new label. + +![New label](img/labels_new_label.png) + --- -You can skip that and create a new label or click that link and GitLab will -generate a set of predefined labels for you. There 8 default generated labels +## Default Labels + +It's possible to populate the labels for your project from a set of predefined labels. + +### Generate GitLab's predefined label set + +![Generate new labels](img/labels_generate.png) + +Click the link to 'Generate a default set of labels' and GitLab will +generate a set of predefined labels for you. There are 8 default generated labels in total and you can see them in the screenshot below. ![Default generated labels](img/labels_default.png) --- -You can see that from the labels page you can have an overview of the number of -issues and merge requests assigned to each label. - -Creating a new label from scratch is as easy as pressing the **New label** -button. From there on you can choose the name, give it an optional description, -a color and you are set. +## Labels Overview -When you are ready press the **Create label** button to create the new label. +![Default generated labels](img/labels_default.png) -![New label](img/labels_new_label.png) +You can see that from the labels page you can have an overview of the number of +issues and merge requests assigned to each label. ## Prioritize labels -- GitLab From 519275c1102ad8a1d56f5807de2d8a1ae4b21dc0 Mon Sep 17 00:00:00 2001 From: tiagonbotelho Date: Mon, 25 Jul 2016 19:16:19 +0100 Subject: [PATCH 093/153] fixes part1 of files to start using active tense --- .../admin/users_controller_spec.rb | 2 +- .../application_controller_spec.rb | 6 +-- .../groups/avatars_controller_spec.rb | 2 +- .../groups/milestones_controller_spec.rb | 2 +- .../profiles/avatars_controller_spec.rb | 2 +- .../profiles/keys_controller_spec.rb | 18 ++++---- .../projects/avatars_controller_spec.rb | 2 +- .../projects/commit_controller_spec.rb | 26 +++++------ .../projects/commits_controller_spec.rb | 2 +- .../projects/compare_controller_spec.rb | 8 ++-- .../projects/forks_controller_spec.rb | 10 ++-- .../projects/issues_controller_spec.rb | 16 +++---- .../merge_requests_controller_spec.rb | 12 ++--- .../projects/milestones_controller_spec.rb | 2 +- .../project_members_controller_spec.rb | 2 +- .../protected_branches_controller_spec.rb | 2 +- .../projects/raw_controller_spec.rb | 2 +- .../projects/services_controller_spec.rb | 4 +- spec/controllers/projects_controller_spec.rb | 10 ++-- .../admin/admin_abuse_reports_spec.rb | 4 +- spec/features/admin/admin_hooks_spec.rb | 6 +-- spec/features/admin/admin_projects_spec.rb | 6 +-- spec/features/admin/admin_users_spec.rb | 28 +++++------ spec/features/atom/dashboard_spec.rb | 6 +-- spec/features/atom/issues_spec.rb | 4 +- spec/features/atom/users_spec.rb | 12 ++--- spec/features/ci_lint_spec.rb | 2 +- spec/features/commits_spec.rb | 2 +- spec/features/compare_spec.rb | 6 +-- spec/features/dashboard/label_filter_spec.rb | 2 +- spec/features/dashboard_issues_spec.rb | 6 +-- .../features/gitlab_flavored_markdown_spec.rb | 24 +++++----- spec/features/help_pages_spec.rb | 2 +- spec/features/issues/award_emoji_spec.rb | 10 ++-- spec/features/issues/award_spec.rb | 8 ++-- .../issues/bulk_assignment_labels_spec.rb | 8 ++-- spec/features/issues/filter_by_labels_spec.rb | 46 +++++++++---------- .../issues/filter_by_milestone_spec.rb | 6 +-- spec/features/issues/filter_issues_spec.rb | 38 +++++++-------- spec/features/issues/issue_sidebar_spec.rb | 8 ++-- .../features/issues/new_branch_button_spec.rb | 2 +- spec/features/issues/todo_spec.rb | 4 +- spec/features/issues/update_issues_spec.rb | 12 ++--- spec/features/issues_spec.rb | 16 +++---- spec/features/login_spec.rb | 2 +- spec/features/merge_requests/award_spec.rb | 8 ++-- spec/features/merge_requests/edit_mr_spec.rb | 2 +- .../filter_by_milestone_spec.rb | 6 +-- .../merge_when_build_succeeds_spec.rb | 2 +- spec/features/milestone_spec.rb | 4 +- spec/features/notes_on_merge_requests_spec.rb | 12 ++--- .../participants_autocomplete_spec.rb | 6 +-- spec/features/pipelines_spec.rb | 10 ++-- spec/features/profile_spec.rb | 4 +- .../labels/update_prioritization_spec.rb | 4 +- spec/features/projects_spec.rb | 8 ++-- spec/features/search_spec.rb | 18 ++++---- spec/features/todos/todos_spec.rb | 2 +- spec/features/variables_spec.rb | 8 ++-- spec/finders/merge_requests_finder_spec.rb | 4 +- spec/finders/notes_finder_spec.rb | 4 +- spec/helpers/application_helper_spec.rb | 20 ++++---- spec/helpers/blob_helper_spec.rb | 10 ++-- spec/helpers/diff_helper_spec.rb | 10 ++-- spec/helpers/emails_helper_spec.rb | 12 ++--- spec/helpers/events_helper_spec.rb | 14 +++--- spec/helpers/gitlab_markdown_helper_spec.rb | 20 ++++---- spec/helpers/graph_helper_spec.rb | 2 +- spec/helpers/groups_helper_spec.rb | 4 +- spec/helpers/issues_helper_spec.rb | 9 ++-- spec/helpers/notes_helper_spec.rb | 2 +- spec/helpers/search_helper_spec.rb | 2 +- spec/helpers/submodule_helper_spec.rb | 28 +++++------ spec/helpers/tree_helper_spec.rb | 4 +- spec/lib/ci/charts_spec.rb | 4 +- 75 files changed, 322 insertions(+), 321 deletions(-) diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index ab9aa65f7b98aa..33fe3c73822673 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -39,7 +39,7 @@ user.ldap_block end - it 'will not unblock user' do + it 'does not unblock user' do put :unblock, id: user.username user.reload expect(user.blocked?).to be_truthy diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 8bd210cbc3dd4a..98e912f000cffe 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -5,7 +5,7 @@ let(:user) { create(:user) } let(:controller) { ApplicationController.new } - it 'should redirect if the user is over their password expiry' do + it 'redirects if the user is over their password expiry' do user.password_expires_at = Time.new(2002) expect(user.ldap_user?).to be_falsey allow(controller).to receive(:current_user).and_return(user) @@ -14,7 +14,7 @@ controller.send(:check_password_expiration) end - it 'should not redirect if the user is under their password expiry' do + it 'does not redirect if the user is under their password expiry' do user.password_expires_at = Time.now + 20010101 expect(user.ldap_user?).to be_falsey allow(controller).to receive(:current_user).and_return(user) @@ -22,7 +22,7 @@ controller.send(:check_password_expiration) end - it 'should not redirect if the user is over their password expiry but they are an ldap user' do + it 'does not redirect if the user is over their password expiry but they are an ldap user' do user.password_expires_at = Time.new(2002) allow(user).to receive(:ldap_user?).and_return(true) allow(controller).to receive(:current_user).and_return(user) diff --git a/spec/controllers/groups/avatars_controller_spec.rb b/spec/controllers/groups/avatars_controller_spec.rb index 91d639218e59e2..506aeee7d2a34e 100644 --- a/spec/controllers/groups/avatars_controller_spec.rb +++ b/spec/controllers/groups/avatars_controller_spec.rb @@ -9,7 +9,7 @@ sign_in(user) end - it 'destroy should remove avatar from DB' do + it 'removes avatar from DB calling destroy' do delete :destroy, group_id: group.path @group = assigns(:group) expect(@group.avatar.present?).to be_falsey diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb index b0793cb1655280..8c52f615b8ba06 100644 --- a/spec/controllers/groups/milestones_controller_spec.rb +++ b/spec/controllers/groups/milestones_controller_spec.rb @@ -15,7 +15,7 @@ end describe "#create" do - it "should create group milestone with Chinese title" do + it "creates group milestone with Chinese title" do post :create, group_id: group.id, milestone: { project_ids: [project.id, project2.id], title: title } diff --git a/spec/controllers/profiles/avatars_controller_spec.rb b/spec/controllers/profiles/avatars_controller_spec.rb index ad5855df0a433e..4fa0462ccdfbfd 100644 --- a/spec/controllers/profiles/avatars_controller_spec.rb +++ b/spec/controllers/profiles/avatars_controller_spec.rb @@ -8,7 +8,7 @@ controller.instance_variable_set(:@user, user) end - it 'destroy should remove avatar from DB' do + it 'removes avatar from DB by calling destroy' do delete :destroy @user = assigns(:user) expect(@user.avatar.present?).to be_falsey diff --git a/spec/controllers/profiles/keys_controller_spec.rb b/spec/controllers/profiles/keys_controller_spec.rb index 3a82083717ff07..6bcfae0fc13d91 100644 --- a/spec/controllers/profiles/keys_controller_spec.rb +++ b/spec/controllers/profiles/keys_controller_spec.rb @@ -6,7 +6,7 @@ describe '#new' do before { sign_in(user) } - it 'redirect to #index' do + it 'redirects to #index' do get :new expect(response).to redirect_to(profile_keys_path) @@ -15,7 +15,7 @@ describe "#get_keys" do describe "non existant user" do - it "should generally not work" do + it "does not generally work" do get :get_keys, username: 'not-existent' expect(response).not_to be_success @@ -23,19 +23,19 @@ end describe "user with no keys" do - it "should generally work" do + it "does generally work" do get :get_keys, username: user.username expect(response).to be_success end - it "should render all keys separated with a new line" do + it "renders all keys separated with a new line" do get :get_keys, username: user.username expect(response.body).to eq("") end - it "should respond with text/plain content type" do + it "responds with text/plain content type" do get :get_keys, username: user.username expect(response.content_type).to eq("text/plain") end @@ -47,13 +47,13 @@ user.keys << create(:another_key) end - it "should generally work" do + it "does generally work" do get :get_keys, username: user.username expect(response).to be_success end - it "should render all keys separated with a new line" do + it "renders all keys separated with a new line" do get :get_keys, username: user.username expect(response.body).not_to eq("") @@ -65,13 +65,13 @@ expect(response.body).to match(/AQDmTillFzNTrrGgwaCKaSj/) end - it "should not render the comment of the key" do + it "does not render the comment of the key" do get :get_keys, username: user.username expect(response.body).not_to match(/dummy@gitlab.com/) end - it "should respond with text/plain content type" do + it "responds with text/plain content type" do get :get_keys, username: user.username expect(response.content_type).to eq("text/plain") end diff --git a/spec/controllers/projects/avatars_controller_spec.rb b/spec/controllers/projects/avatars_controller_spec.rb index 4d724ca9ed0175..f5ea097af8b5a2 100644 --- a/spec/controllers/projects/avatars_controller_spec.rb +++ b/spec/controllers/projects/avatars_controller_spec.rb @@ -10,7 +10,7 @@ controller.instance_variable_set(:@project, project) end - it 'destroy should remove avatar from DB' do + it 'removes avatar from DB by calling destroy' do delete :destroy, namespace_id: project.namespace.id, project_id: project.id expect(project.avatar.present?).to be_falsey expect(project).to be_valid diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb index 940019b708b6e1..7e440193d7be1e 100644 --- a/spec/controllers/projects/commit_controller_spec.rb +++ b/spec/controllers/projects/commit_controller_spec.rb @@ -47,25 +47,25 @@ def go(extra_params = {}) end shared_examples "export as" do |format| - it "should generally work" do + it "does generally work" do go(id: commit.id, format: format) expect(response).to be_success end - it "should generate it" do + it "generates it" do expect_any_instance_of(Commit).to receive(:"to_#{format}") go(id: commit.id, format: format) end - it "should render it" do + it "renders it" do go(id: commit.id, format: format) expect(response.body).to eq(commit.send(:"to_#{format}")) end - it "should not escape Html" do + it "does not escape Html" do allow_any_instance_of(Commit).to receive(:"to_#{format}"). and_return('HTML entities &<>" ') @@ -88,7 +88,7 @@ def go(extra_params = {}) expect(response.body).to start_with("diff --git") end - it "should really only be a git diff without whitespace changes" do + it "is only be a git diff without whitespace changes" do go(id: '66eceea0db202bb39c4e445e8ca28689645366c5', format: format, w: 1) expect(response.body).to start_with("diff --git") @@ -103,13 +103,13 @@ def go(extra_params = {}) include_examples "export as", :patch let(:format) { :patch } - it "should really be a git email patch" do + it "is a git email patch" do go(id: commit.id, format: format) expect(response.body).to start_with("From #{commit.id}") end - it "should contain a git diff" do + it "contains a git diff" do go(id: commit.id, format: format) expect(response.body).to match(/^diff --git/) @@ -147,7 +147,7 @@ def go(extra_params = {}) describe 'POST revert' do context 'when target branch is not provided' do - it 'should render the 404 page' do + it 'renders the 404 page' do post(:revert, namespace_id: project.namespace.to_param, project_id: project.to_param, @@ -159,7 +159,7 @@ def go(extra_params = {}) end context 'when the revert was successful' do - it 'should redirect to the commits page' do + it 'redirects to the commits page' do post(:revert, namespace_id: project.namespace.to_param, project_id: project.to_param, @@ -180,7 +180,7 @@ def go(extra_params = {}) id: commit.id) end - it 'should redirect to the commit page' do + it 'redirects to the commit page' do # Reverting a commit that has been already reverted. post(:revert, namespace_id: project.namespace.to_param, @@ -196,7 +196,7 @@ def go(extra_params = {}) describe 'POST cherry_pick' do context 'when target branch is not provided' do - it 'should render the 404 page' do + it 'renders the 404 page' do post(:cherry_pick, namespace_id: project.namespace.to_param, project_id: project.to_param, @@ -208,7 +208,7 @@ def go(extra_params = {}) end context 'when the cherry-pick was successful' do - it 'should redirect to the commits page' do + it 'redirects to the commits page' do post(:cherry_pick, namespace_id: project.namespace.to_param, project_id: project.to_param, @@ -229,7 +229,7 @@ def go(extra_params = {}) id: master_pickable_commit.id) end - it 'should redirect to the commit page' do + it 'redirects to the commit page' do # Cherry-picking a commit that has been already cherry-picked. post(:cherry_pick, namespace_id: project.namespace.to_param, diff --git a/spec/controllers/projects/commits_controller_spec.rb b/spec/controllers/projects/commits_controller_spec.rb index 7d8089c4bc60dc..2518a48e336429 100644 --- a/spec/controllers/projects/commits_controller_spec.rb +++ b/spec/controllers/projects/commits_controller_spec.rb @@ -11,7 +11,7 @@ describe "GET show" do context "as atom feed" do - it "should render as atom" do + it "renders as atom" do get(:show, namespace_id: project.namespace.to_param, project_id: project.to_param, diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb index ed4cc36de58d70..7a57801c437c0e 100644 --- a/spec/controllers/projects/compare_controller_spec.rb +++ b/spec/controllers/projects/compare_controller_spec.rb @@ -11,7 +11,7 @@ project.team << [user, :master] end - it 'compare should show some diffs' do + it 'compare shows some diffs' do get(:show, namespace_id: project.namespace.to_param, project_id: project.to_param, @@ -23,7 +23,7 @@ expect(assigns(:commits).length).to be >= 1 end - it 'compare should show some diffs with ignore whitespace change option' do + it 'compare shows some diffs with ignore whitespace change option' do get(:show, namespace_id: project.namespace.to_param, project_id: project.to_param, @@ -41,7 +41,7 @@ end describe 'non-existent refs' do - it 'invalid source ref' do + it 'uses invalid source ref' do get(:show, namespace_id: project.namespace.to_param, project_id: project.to_param, @@ -53,7 +53,7 @@ expect(assigns(:commits)).to eq([]) end - it 'invalid target ref' do + it 'uses invalid target ref' do get(:show, namespace_id: project.namespace.to_param, project_id: project.to_param, diff --git a/spec/controllers/projects/forks_controller_spec.rb b/spec/controllers/projects/forks_controller_spec.rb index f66bcb8099cb37..ac3469cb8a9da2 100644 --- a/spec/controllers/projects/forks_controller_spec.rb +++ b/spec/controllers/projects/forks_controller_spec.rb @@ -16,7 +16,7 @@ def get_forks context 'when fork is public' do before { forked_project.update_attribute(:visibility_level, Project::PUBLIC) } - it 'should be visible for non logged in users' do + it 'is visible for non logged in users' do get_forks expect(assigns[:forks]).to be_present @@ -28,7 +28,7 @@ def get_forks forked_project.update_attributes(visibility_level: Project::PRIVATE, group: group) end - it 'should not be visible for non logged in users' do + it 'is not be visible for non logged in users' do get_forks expect(assigns[:forks]).to be_blank @@ -38,7 +38,7 @@ def get_forks before { sign_in(project.creator) } context 'when user is not a Project member neither a group member' do - it 'should not see the Project listed' do + it 'does not see the Project listed' do get_forks expect(assigns[:forks]).to be_blank @@ -48,7 +48,7 @@ def get_forks context 'when user is a member of the Project' do before { forked_project.team << [project.creator, :developer] } - it 'should see the project listed' do + it 'sees the project listed' do get_forks expect(assigns[:forks]).to be_present @@ -58,7 +58,7 @@ def get_forks context 'when user is a member of the Group' do before { forked_project.group.add_developer(project.creator) } - it 'should see the project listed' do + it 'sees the project listed' do get_forks expect(assigns[:forks]).to be_present diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index ec820de3d09a33..b6a0276846cacb 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -30,7 +30,7 @@ expect(response).to have_http_status(200) end - it "return 301 if request path doesn't match project path" do + it "returns 301 if request path doesn't match project path" do get :index, namespace_id: project.namespace.path, project_id: project.path.upcase expect(response).to redirect_to(namespace_project_issues_path(project.namespace, project)) @@ -119,21 +119,21 @@ def move_issue let!(:request_forgery_timing_attack) { create(:issue, :confidential, project: project, assignee: assignee) } describe 'GET #index' do - it 'should not list confidential issues for guests' do + it 'does not list confidential issues for guests' do sign_out(:user) get_issues expect(assigns(:issues)).to eq [issue] end - it 'should not list confidential issues for non project members' do + it 'does not list confidential issues for non project members' do sign_in(non_member) get_issues expect(assigns(:issues)).to eq [issue] end - it 'should not list confidential issues for project members with guest role' do + it 'does not list confidential issues for project members with guest role' do sign_in(member) project.team << [member, :guest] @@ -142,7 +142,7 @@ def move_issue expect(assigns(:issues)).to eq [issue] end - it 'should list confidential issues for author' do + it 'lists confidential issues for author' do sign_in(author) get_issues @@ -150,7 +150,7 @@ def move_issue expect(assigns(:issues)).not_to include request_forgery_timing_attack end - it 'should list confidential issues for assignee' do + it 'lists confidential issues for assignee' do sign_in(assignee) get_issues @@ -158,7 +158,7 @@ def move_issue expect(assigns(:issues)).to include request_forgery_timing_attack end - it 'should list confidential issues for project members' do + it 'lists confidential issues for project members' do sign_in(member) project.team << [member, :developer] @@ -168,7 +168,7 @@ def move_issue expect(assigns(:issues)).to include request_forgery_timing_attack end - it 'should list confidential issues for admin' do + it 'lists confidential issues for admin' do sign_in(admin) get_issues diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 1f6bc84dfe8d05..69758494543085 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -36,7 +36,7 @@ describe "GET show" do shared_examples "export merge as" do |format| - it "should generally work" do + it "does generally work" do get(:show, namespace_id: project.namespace.to_param, project_id: project.to_param, @@ -46,7 +46,7 @@ expect(response).to be_success end - it "should generate it" do + it "generates it" do expect_any_instance_of(MergeRequest).to receive(:"to_#{format}") get(:show, @@ -56,7 +56,7 @@ format: format) end - it "should render it" do + it "renders it" do get(:show, namespace_id: project.namespace.to_param, project_id: project.to_param, @@ -66,7 +66,7 @@ expect(response.body).to eq(merge_request.send(:"to_#{format}").to_s) end - it "should not escape Html" do + it "does not escape Html" do allow_any_instance_of(MergeRequest).to receive(:"to_#{format}"). and_return('HTML entities &<>" ') @@ -118,7 +118,7 @@ def get_merge_requests context 'when filtering by opened state' do context 'with opened merge requests' do - it 'should list those merge requests' do + it 'lists those merge requests' do get_merge_requests expect(assigns(:merge_requests)).to include(merge_request) @@ -131,7 +131,7 @@ def get_merge_requests merge_request.reopen! end - it 'should list those merge requests' do + it 'lists those merge requests' do get_merge_requests expect(assigns(:merge_requests)).to include(merge_request) diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb index d173bb350f1c66..4e3ef5dc6fa7f3 100644 --- a/spec/controllers/projects/milestones_controller_spec.rb +++ b/spec/controllers/projects/milestones_controller_spec.rb @@ -14,7 +14,7 @@ end describe "#destroy" do - it "should remove milestone" do + it "removes milestone" do expect(issue.milestone_id).to eq(milestone.id) delete :destroy, namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid, format: :js diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb index 5e2a8cf38490ba..19d05aef0a6fcb 100644 --- a/spec/controllers/projects/project_members_controller_spec.rb +++ b/spec/controllers/projects/project_members_controller_spec.rb @@ -167,7 +167,7 @@ sign_in(user) end - it 'cannot remove himself from the project' do + it 'does not remove himself from the project' do delete :leave, namespace_id: project.namespace, project_id: project diff --git a/spec/controllers/projects/protected_branches_controller_spec.rb b/spec/controllers/projects/protected_branches_controller_spec.rb index 596d8d34b7c9a6..da6112a13f7587 100644 --- a/spec/controllers/projects/protected_branches_controller_spec.rb +++ b/spec/controllers/projects/protected_branches_controller_spec.rb @@ -3,7 +3,7 @@ describe Projects::ProtectedBranchesController do describe "GET #index" do let(:project) { create(:project_empty_repo, :public) } - it "redirect empty repo to projects page" do + it "redirects empty repo to projects page" do get(:index, namespace_id: project.namespace.to_param, project_id: project.to_param) end end diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb index 48f799d8ca1f4c..04bd9a01f7b71f 100644 --- a/spec/controllers/projects/raw_controller_spec.rb +++ b/spec/controllers/projects/raw_controller_spec.rb @@ -24,7 +24,7 @@ context 'image header' do let(:id) { 'master/files/images/6049019_460s.jpg' } - it 'set image content type header' do + it 'sets image content type header' do get(:show, namespace_id: public_project.namespace.to_param, project_id: public_project.to_param, diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb index ccd8c741c83242..cccd492ef0672c 100644 --- a/spec/controllers/projects/services_controller_spec.rb +++ b/spec/controllers/projects/services_controller_spec.rb @@ -19,7 +19,7 @@ describe "#test" do context 'success' do - it "should redirect and show success message" do + it "redirects and show success message" do expect(service).to receive(:test).and_return({ success: true, result: 'done' }) get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html expect(response.status).to redirect_to('/') @@ -28,7 +28,7 @@ end context 'failure' do - it "should redirect and show failure message" do + it "redirects and show failure message" do expect(service).to receive(:test).and_return({ success: false, result: 'Bad test' }) get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html expect(response.status).to redirect_to('/') diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 3edce4d339c539..ffe0641ddd7893 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -128,7 +128,7 @@ context "when the url contains .atom" do let(:public_project_with_dot_atom) { build(:project, :public, name: 'my.atom', path: 'my.atom') } - it 'expect an error creating the project' do + it 'expects an error creating the project' do expect(public_project_with_dot_atom).not_to be_valid end end @@ -222,7 +222,7 @@ create(:forked_project_link, forked_to_project: project_fork) end - it 'should remove fork from project' do + it 'removes fork from project' do delete(:remove_fork, namespace_id: project_fork.namespace.to_param, id: project_fork.to_param, format: :js) @@ -236,7 +236,7 @@ context 'when project not forked' do let(:unforked_project) { create(:project, namespace: user.namespace) } - it 'should do nothing if project was not forked' do + it 'does nothing if project was not forked' do delete(:remove_fork, namespace_id: unforked_project.namespace.to_param, id: unforked_project.to_param, format: :js) @@ -256,7 +256,7 @@ end describe "GET refs" do - it "should get a list of branches and tags" do + it "gets a list of branches and tags" do get :refs, namespace_id: public_project.namespace.path, id: public_project.path parsed_body = JSON.parse(response.body) @@ -265,7 +265,7 @@ expect(parsed_body["Commits"]).to be_nil end - it "should get a list of branches, tags and commits" do + it "gets a list of branches, tags and commits" do get :refs, namespace_id: public_project.namespace.path, id: public_project.path, ref: "123456" parsed_body = JSON.parse(response.body) diff --git a/spec/features/admin/admin_abuse_reports_spec.rb b/spec/features/admin/admin_abuse_reports_spec.rb index 16baf7e951625f..c1731e6414a80c 100644 --- a/spec/features/admin/admin_abuse_reports_spec.rb +++ b/spec/features/admin/admin_abuse_reports_spec.rb @@ -11,7 +11,7 @@ end describe 'in the abuse report view' do - it "should present a link to the user's profile" do + it "presents a link to the user's profile" do visit admin_abuse_reports_path expect(page).to have_link user.name, href: user_path(user) @@ -19,7 +19,7 @@ end describe 'in the profile page of the user' do - it 'should show a link to the admin view of the user' do + it 'shows a link to the admin view of the user' do visit user_path(user) expect(page).to have_link '', href: admin_user_path(user) diff --git a/spec/features/admin/admin_hooks_spec.rb b/spec/features/admin/admin_hooks_spec.rb index 7964951ae99c66..b3ce72b1452ebb 100644 --- a/spec/features/admin/admin_hooks_spec.rb +++ b/spec/features/admin/admin_hooks_spec.rb @@ -9,7 +9,7 @@ end describe "GET /admin/hooks" do - it "should be ok" do + it "is ok" do visit admin_root_path page.within ".layout-nav" do @@ -19,7 +19,7 @@ expect(current_path).to eq(admin_hooks_path) end - it "should have hooks list" do + it "has hooks list" do visit admin_hooks_path expect(page).to have_content(@system_hook.url) end @@ -33,7 +33,7 @@ expect { click_button "Add System Hook" }.to change(SystemHook, :count).by(1) end - it "should open new hook popup" do + it "opens new hook popup" do expect(current_path).to eq(admin_hooks_path) expect(page).to have_content(@url) end diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb index 101d955d693c74..30ded9202a4975 100644 --- a/spec/features/admin/admin_projects_spec.rb +++ b/spec/features/admin/admin_projects_spec.rb @@ -11,11 +11,11 @@ visit admin_namespaces_projects_path end - it "should be ok" do + it "is ok" do expect(current_path).to eq(admin_namespaces_projects_path) end - it "should have projects list" do + it "has projects list" do expect(page).to have_content(@project.name) end end @@ -26,7 +26,7 @@ click_link "#{@project.name}" end - it "should have project info" do + it "has project info" do expect(page).to have_content(@project.path) expect(page).to have_content(@project.name) end diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index 767504df251e17..cb3191dfdde4c8 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -8,11 +8,11 @@ visit admin_users_path end - it "should be ok" do + it "is ok" do expect(current_path).to eq(admin_users_path) end - it "should have users list" do + it "has users list" do expect(page).to have_content(@user.email) expect(page).to have_content(@user.name) end @@ -66,11 +66,11 @@ fill_in "user_email", with: "bigbang@mail.com" end - it "should create new user" do + it "creates new user" do expect { click_button "Create user" }.to change {User.count}.by(1) end - it "should apply defaults to user" do + it "applies defaults to user" do click_button "Create user" user = User.find_by(username: 'bang') expect(user.projects_limit). @@ -79,20 +79,20 @@ to eq(Gitlab.config.gitlab.default_can_create_group) end - it "should create user with valid data" do + it "creates user with valid data" do click_button "Create user" user = User.find_by(username: 'bang') expect(user.name).to eq('Big Bang') expect(user.email).to eq('bigbang@mail.com') end - it "should call send mail" do + it "calls send mail" do expect_any_instance_of(NotificationService).to receive(:new_user) click_button "Create user" end - it "should send valid email to user with email & password" do + it "sends valid email to user with email & password" do perform_enqueued_jobs do click_button "Create user" end @@ -106,7 +106,7 @@ end describe "GET /admin/users/:id" do - it "should have user info" do + it "has user info" do visit admin_users_path click_link @user.name @@ -123,13 +123,13 @@ expect(page).to have_content('Impersonate') end - it 'should not show impersonate button for admin itself' do + it 'does not show impersonate button for admin itself' do visit admin_user_path(@user) expect(page).not_to have_content('Impersonate') end - it 'should not show impersonate button for blocked user' do + it 'does not show impersonate button for blocked user' do another_user.block visit admin_user_path(another_user) @@ -153,7 +153,7 @@ expect(icon).not_to eql nil end - it 'can log out of impersonated user back to original user' do + it 'logs out of impersonated user back to original user' do find(:css, 'li.impersonation a').click expect(page.find(:css, '.header-user .profile-link')['data-user']).to eql(@user.username) @@ -197,7 +197,7 @@ def expect_two_factor_status(status) click_link "edit_user_#{@simple_user.id}" end - it "should have user edit page" do + it "has user edit page" do expect(page).to have_content('Name') expect(page).to have_content('Password') end @@ -212,12 +212,12 @@ def expect_two_factor_status(status) click_button "Save changes" end - it "should show page with new data" do + it "shows page with new data" do expect(page).to have_content('bigbang@mail.com') expect(page).to have_content('Big Bang') end - it "should change user entry" do + it "changes user entry" do @simple_user.reload expect(@simple_user.name).to eq('Big Bang') expect(@simple_user.is_admin?).to be_truthy diff --git a/spec/features/atom/dashboard_spec.rb b/spec/features/atom/dashboard_spec.rb index f81a3c117ff23d..746df36bb25899 100644 --- a/spec/features/atom/dashboard_spec.rb +++ b/spec/features/atom/dashboard_spec.rb @@ -5,7 +5,7 @@ let!(:user) { create(:user, name: "Jonh") } context "projects atom feed via private token" do - it "should render projects atom feed" do + it "renders projects atom feed" do visit dashboard_projects_path(:atom, private_token: user.private_token) expect(body).to have_selector('feed title') end @@ -23,11 +23,11 @@ visit dashboard_projects_path(:atom, private_token: user.private_token) end - it "should have issue opened event" do + it "has issue opened event" do expect(body).to have_content("#{user.name} opened issue ##{issue.iid}") end - it "should have issue comment event" do + it "has issue comment event" do expect(body). to have_content("#{user.name} commented on issue ##{issue.iid}") end diff --git a/spec/features/atom/issues_spec.rb b/spec/features/atom/issues_spec.rb index baa7814e96a017..09c140868fb818 100644 --- a/spec/features/atom/issues_spec.rb +++ b/spec/features/atom/issues_spec.rb @@ -9,7 +9,7 @@ before { project.team << [user, :developer] } context 'when authenticated' do - it 'should render atom feed' do + it 'renders atom feed' do login_with user visit namespace_project_issues_path(project.namespace, project, :atom) @@ -22,7 +22,7 @@ end context 'when authenticated via private token' do - it 'should render atom feed' do + it 'renders atom feed' do visit namespace_project_issues_path(project.namespace, project, :atom, private_token: user.private_token) diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb index 91704377a07a47..a8833194421fca 100644 --- a/spec/features/atom/users_spec.rb +++ b/spec/features/atom/users_spec.rb @@ -5,7 +5,7 @@ let!(:user) { create(:user) } context 'user atom feed via private token' do - it "should render user atom feed" do + it "renders user atom feed" do visit user_path(user, :atom, private_token: user.private_token) expect(body).to have_selector('feed title') end @@ -43,24 +43,24 @@ visit user_path(user, :atom, private_token: user.private_token) end - it 'should have issue opened event' do + it 'has issue opened event' do expect(body).to have_content("#{safe_name} opened issue ##{issue.iid}") end - it 'should have issue comment event' do + it 'has issue comment event' do expect(body). to have_content("#{safe_name} commented on issue ##{issue.iid}") end - it 'should have XHTML summaries in issue descriptions' do + it 'has XHTML summaries in issue descriptions' do expect(body).to match /we have a bug!<\/p>\n\n
\n\n

I guess/ end - it 'should have XHTML summaries in notes' do + it 'has XHTML summaries in notes' do expect(body).to match /Bug confirmed ]*\/>/ end - it 'should have XHTML summaries in merge request descriptions' do + it 'has XHTML summaries in merge request descriptions' do expect(body).to match /Here is the fix: <\/p>]*>]*>]*\/><\/a><\/div>/ end end diff --git a/spec/features/ci_lint_spec.rb b/spec/features/ci_lint_spec.rb index 30e29d9d552654..81077f4b00547f 100644 --- a/spec/features/ci_lint_spec.rb +++ b/spec/features/ci_lint_spec.rb @@ -17,7 +17,7 @@ File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) end - it 'Yaml parsing' do + it 'parses Yaml' do within "table" do expect(page).to have_content('Job - rspec') expect(page).to have_content('Job - spinach') diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index 45e1a157a1f1bc..5910803df51f4f 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -52,7 +52,7 @@ visit namespace_project_commits_path(project.namespace, project, :master) end - it 'should show build status' do + it 'shows build status' do page.within("//li[@id='commit-#{pipeline.short_sha}']") do expect(page).to have_css(".ci-status-link") end diff --git a/spec/features/compare_spec.rb b/spec/features/compare_spec.rb index c62556948e0faa..ca7f73e24cc864 100644 --- a/spec/features/compare_spec.rb +++ b/spec/features/compare_spec.rb @@ -11,11 +11,11 @@ end describe "branches" do - it "should pre-populate fields" do + it "pre-populates fields" do expect(page.find_field("from").value).to eq("master") end - it "should compare branches" do + it "compares branches" do fill_in "from", with: "fea" find("#from").click @@ -28,7 +28,7 @@ end describe "tags" do - it "should compare tags" do + it "compares tags" do fill_in "from", with: "v1.0" find("#from").click diff --git a/spec/features/dashboard/label_filter_spec.rb b/spec/features/dashboard/label_filter_spec.rb index 24e83d44010cb0..4cff12de854a2d 100644 --- a/spec/features/dashboard/label_filter_spec.rb +++ b/spec/features/dashboard/label_filter_spec.rb @@ -16,7 +16,7 @@ end context 'duplicate labels' do - it 'should remove duplicate labels' do + it 'removes duplicate labels' do page.within('.labels-filter') do click_button 'Label' end diff --git a/spec/features/dashboard_issues_spec.rb b/spec/features/dashboard_issues_spec.rb index 39805da9d0bb3f..3fb1cb37544717 100644 --- a/spec/features/dashboard_issues_spec.rb +++ b/spec/features/dashboard_issues_spec.rb @@ -16,7 +16,7 @@ visit_issues end - it 'should show all issues with no milestone' do + it 'shows all issues with no milestone' do show_milestone_dropdown click_link 'No Milestone' @@ -24,7 +24,7 @@ expect(page).to have_selector('.issue', count: 1) end - it 'should show all issues with any milestone' do + it 'shows all issues with any milestone' do show_milestone_dropdown click_link 'Any Milestone' @@ -32,7 +32,7 @@ expect(page).to have_selector('.issue', count: 2) end - it 'should show all issues with the selected milestone' do + it 'shows all issues with the selected milestone' do show_milestone_dropdown page.within '.dropdown-content' do diff --git a/spec/features/gitlab_flavored_markdown_spec.rb b/spec/features/gitlab_flavored_markdown_spec.rb index a89ac09f23692e..84d73d693bcfea 100644 --- a/spec/features/gitlab_flavored_markdown_spec.rb +++ b/spec/features/gitlab_flavored_markdown_spec.rb @@ -23,25 +23,25 @@ end describe "for commits" do - it "should render title in commits#index" do + it "renders title in commits#index" do visit namespace_project_commits_path(project.namespace, project, 'master', limit: 1) expect(page).to have_link(issue.to_reference) end - it "should render title in commits#show" do + it "renders title in commits#show" do visit namespace_project_commit_path(project.namespace, project, commit) expect(page).to have_link(issue.to_reference) end - it "should render description in commits#show" do + it "renders description in commits#show" do visit namespace_project_commit_path(project.namespace, project, commit) expect(page).to have_link(fred.to_reference) end - it "should render title in repositories#branches" do + it "renders title in repositories#branches" do visit namespace_project_branches_path(project.namespace, project) expect(page).to have_link(issue.to_reference) @@ -62,19 +62,19 @@ description: "ask #{fred.to_reference} for details") end - it "should render subject in issues#index" do + it "renders subject in issues#index" do visit namespace_project_issues_path(project.namespace, project) expect(page).to have_link(@other_issue.to_reference) end - it "should render subject in issues#show" do + it "renders subject in issues#show" do visit namespace_project_issue_path(project.namespace, project, @issue) expect(page).to have_link(@other_issue.to_reference) end - it "should render details in issues#show" do + it "renders details in issues#show" do visit namespace_project_issue_path(project.namespace, project, @issue) expect(page).to have_link(fred.to_reference) @@ -86,13 +86,13 @@ @merge_request = create(:merge_request, source_project: project, target_project: project, title: "fix #{issue.to_reference}") end - it "should render title in merge_requests#index" do + it "renders title in merge_requests#index" do visit namespace_project_merge_requests_path(project.namespace, project) expect(page).to have_link(issue.to_reference) end - it "should render title in merge_requests#show" do + it "renders title in merge_requests#show" do visit namespace_project_merge_request_path(project.namespace, project, @merge_request) expect(page).to have_link(issue.to_reference) @@ -107,19 +107,19 @@ description: "ask #{fred.to_reference} for details") end - it "should render title in milestones#index" do + it "renders title in milestones#index" do visit namespace_project_milestones_path(project.namespace, project) expect(page).to have_link(issue.to_reference) end - it "should render title in milestones#show" do + it "renders title in milestones#show" do visit namespace_project_milestone_path(project.namespace, project, @milestone) expect(page).to have_link(issue.to_reference) end - it "should render description in milestones#show" do + it "renders description in milestones#show" do visit namespace_project_milestone_path(project.namespace, project, @milestone) expect(page).to have_link(fred.to_reference) diff --git a/spec/features/help_pages_spec.rb b/spec/features/help_pages_spec.rb index 1e2306d7f5930b..e2101b333e2317 100644 --- a/spec/features/help_pages_spec.rb +++ b/spec/features/help_pages_spec.rb @@ -5,7 +5,7 @@ before do login_as :user end - it 'replace the variable $your_email with the email of the user' do + it 'replaces the variable $your_email with the email of the user' do visit help_page_path('ssh/README') expect(page).to have_content("ssh-keygen -t rsa -C \"#{@user.email}\"") end diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb index 07a854ea014191..6eb04cf74c523d 100644 --- a/spec/features/issues/award_emoji_spec.rb +++ b/spec/features/issues/award_emoji_spec.rb @@ -21,32 +21,32 @@ visit namespace_project_issue_path(project.namespace, project, issue) end - it 'should increment the thumbsdown emoji', js: true do + it 'increments the thumbsdown emoji', js: true do find('[data-emoji="thumbsdown"]').click sleep 2 expect(thumbsdown_emoji).to have_text("1") end context 'click the thumbsup emoji' do - it 'should increment the thumbsup emoji', js: true do + it 'increments the thumbsup emoji', js: true do find('[data-emoji="thumbsup"]').click sleep 2 expect(thumbsup_emoji).to have_text("1") end - it 'should decrement the thumbsdown emoji', js: true do + it 'decrements the thumbsdown emoji', js: true do expect(thumbsdown_emoji).to have_text("0") end end context 'click the thumbsdown emoji' do - it 'should increment the thumbsdown emoji', js: true do + it 'increments the thumbsdown emoji', js: true do find('[data-emoji="thumbsdown"]').click sleep 2 expect(thumbsdown_emoji).to have_text("1") end - it 'should decrement the thumbsup emoji', js: true do + it 'decrements the thumbsup emoji', js: true do expect(thumbsup_emoji).to have_text("0") end end diff --git a/spec/features/issues/award_spec.rb b/spec/features/issues/award_spec.rb index 63efecf87802bd..401e1ea2b893e2 100644 --- a/spec/features/issues/award_spec.rb +++ b/spec/features/issues/award_spec.rb @@ -11,7 +11,7 @@ visit namespace_project_issue_path(project.namespace, project, issue) end - it 'should add award to issue' do + it 'adds award to issue' do first('.js-emoji-btn').click expect(page).to have_selector('.js-emoji-btn.active') expect(first('.js-emoji-btn')).to have_content '1' @@ -20,7 +20,7 @@ expect(first('.js-emoji-btn')).to have_content '1' end - it 'should remove award from issue' do + it 'removes award from issue' do first('.js-emoji-btn').click find('.js-emoji-btn.active').click expect(first('.js-emoji-btn')).to have_content '0' @@ -29,7 +29,7 @@ expect(first('.js-emoji-btn')).to have_content '0' end - it 'should only have one menu on the page' do + it 'only has one menu on the page' do first('.js-add-award').click expect(page).to have_selector('.emoji-menu') @@ -42,7 +42,7 @@ visit namespace_project_issue_path(project.namespace, project, issue) end - it 'should not see award menu button' do + it 'does not see award menu button' do expect(page).not_to have_selector('.js-award-holder') end end diff --git a/spec/features/issues/bulk_assignment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb index afc093cc1f5626..bc2c087c9b9a0f 100644 --- a/spec/features/issues/bulk_assignment_labels_spec.rb +++ b/spec/features/issues/bulk_assignment_labels_spec.rb @@ -175,7 +175,7 @@ visit namespace_project_issues_path(project.namespace, project) end - it 'labels are kept' do + it 'keeps labels' do expect(find("#issue_#{issue1.id}")).to have_content 'bug' expect(find("#issue_#{issue2.id}")).to have_content 'feature' @@ -197,7 +197,7 @@ visit namespace_project_issues_path(project.namespace, project) end - it 'existing label is kept and new label is present' do + it 'keeps existing label and new label is present' do expect(find("#issue_#{issue1.id}")).to have_content 'bug' check 'check_all_issues' @@ -222,7 +222,7 @@ visit namespace_project_issues_path(project.namespace, project) end - it 'existing label is kept and new label is present' do + it 'keeps existing label and new label is present' do expect(find("#issue_#{issue1.id}")).to have_content 'bug' expect(find("#issue_#{issue1.id}")).to have_content 'bug' expect(find("#issue_#{issue2.id}")).to have_content 'feature' @@ -252,7 +252,7 @@ visit namespace_project_issues_path(project.namespace, project) end - it 'labels are kept' do + it 'keeps labels' do expect(find("#issue_#{issue1.id}")).to have_content 'bug' expect(find("#issue_#{issue1.id}")).to have_content 'First Release' expect(find("#issue_#{issue2.id}")).to have_content 'feature' diff --git a/spec/features/issues/filter_by_labels_spec.rb b/spec/features/issues/filter_by_labels_spec.rb index cb117d2476f16e..908b18e5339cdf 100644 --- a/spec/features/issues/filter_by_labels_spec.rb +++ b/spec/features/issues/filter_by_labels_spec.rb @@ -37,25 +37,25 @@ wait_for_ajax end - it 'should show issue "Bugfix1" and "Bugfix2" in issues list' do + it 'shows issue "Bugfix1" and "Bugfix2" in issues list' do expect(page).to have_content "Bugfix1" expect(page).to have_content "Bugfix2" end - it 'should not show "Feature1" in issues list' do + it 'does not show "Feature1" in issues list' do expect(page).not_to have_content "Feature1" end - it 'should show label "bug" in filtered-labels' do + it 'shows label "bug" in filtered-labels' do expect(find('.filtered-labels')).to have_content "bug" end - it 'should not show label "feature" and "enhancement" in filtered-labels' do + it 'does not show label "feature" and "enhancement" in filtered-labels' do expect(find('.filtered-labels')).not_to have_content "feature" expect(find('.filtered-labels')).not_to have_content "enhancement" end - it 'should remove label "bug"' do + it 'removes label "bug"' do find('.js-label-filter-remove').click wait_for_ajax expect(find('.filtered-labels', visible: false)).to have_no_content "bug" @@ -71,20 +71,20 @@ wait_for_ajax end - it 'should show issue "Feature1" in issues list' do + it 'shows issue "Feature1" in issues list' do expect(page).to have_content "Feature1" end - it 'should not show "Bugfix1" and "Bugfix2" in issues list' do + it 'does not show "Bugfix1" and "Bugfix2" in issues list' do expect(page).not_to have_content "Bugfix2" expect(page).not_to have_content "Bugfix1" end - it 'should show label "feature" in filtered-labels' do + it 'shows label "feature" in filtered-labels' do expect(find('.filtered-labels')).to have_content "feature" end - it 'should not show label "bug" and "enhancement" in filtered-labels' do + it 'does not show label "bug" and "enhancement" in filtered-labels' do expect(find('.filtered-labels')).not_to have_content "bug" expect(find('.filtered-labels')).not_to have_content "enhancement" end @@ -99,20 +99,20 @@ wait_for_ajax end - it 'should show issue "Bugfix2" in issues list' do + it 'shows issue "Bugfix2" in issues list' do expect(page).to have_content "Bugfix2" end - it 'should not show "Feature1" and "Bugfix1" in issues list' do + it 'does not show "Feature1" and "Bugfix1" in issues list' do expect(page).not_to have_content "Feature1" expect(page).not_to have_content "Bugfix1" end - it 'should show label "enhancement" in filtered-labels' do + it 'shows label "enhancement" in filtered-labels' do expect(find('.filtered-labels')).to have_content "enhancement" end - it 'should not show label "feature" and "bug" in filtered-labels' do + it 'does not show label "feature" and "bug" in filtered-labels' do expect(find('.filtered-labels')).not_to have_content "bug" expect(find('.filtered-labels')).not_to have_content "feature" end @@ -128,21 +128,21 @@ wait_for_ajax end - it 'should not show "Bugfix1" or "Feature1" in issues list' do + it 'does not show "Bugfix1" or "Feature1" in issues list' do expect(page).not_to have_content "Bugfix1" expect(page).not_to have_content "Feature1" end - it 'should show label "enhancement" and "feature" in filtered-labels' do + it 'shows label "enhancement" and "feature" in filtered-labels' do expect(find('.filtered-labels')).to have_content "enhancement" expect(find('.filtered-labels')).to have_content "feature" end - it 'should not show label "bug" in filtered-labels' do + it 'does not show label "bug" in filtered-labels' do expect(find('.filtered-labels')).not_to have_content "bug" end - it 'should remove label "enhancement"' do + it 'removes label "enhancement"' do find('.js-label-filter-remove', match: :first).click wait_for_ajax expect(find('.filtered-labels')).to have_no_content "enhancement" @@ -159,20 +159,20 @@ wait_for_ajax end - it 'should show issue "Bugfix2" in issues list' do + it 'shows issue "Bugfix2" in issues list' do expect(page).to have_content "Bugfix2" end - it 'should not show "Feature1"' do + it 'does not show "Feature1"' do expect(page).not_to have_content "Feature1" end - it 'should show label "bug" and "enhancement" in filtered-labels' do + it 'shows label "bug" and "enhancement" in filtered-labels' do expect(find('.filtered-labels')).to have_content "bug" expect(find('.filtered-labels')).to have_content "enhancement" end - it 'should not show label "feature" in filtered-labels' do + it 'does not show label "feature" in filtered-labels' do expect(find('.filtered-labels')).not_to have_content "feature" end end @@ -191,7 +191,7 @@ end end - it 'should allow user to remove filtered labels' do + it 'allows user to remove filtered labels' do first('.js-label-filter-remove').click wait_for_ajax @@ -201,7 +201,7 @@ end context 'dropdown filtering', js: true do - it 'should filter by label name' do + it 'filters by label name' do page.within '.labels-filter' do click_button 'Label' wait_for_ajax diff --git a/spec/features/issues/filter_by_milestone_spec.rb b/spec/features/issues/filter_by_milestone_spec.rb index 9944518589378a..485dc5600616d6 100644 --- a/spec/features/issues/filter_by_milestone_spec.rb +++ b/spec/features/issues/filter_by_milestone_spec.rb @@ -15,7 +15,7 @@ end context 'filters by upcoming milestone', js: true do - it 'should not show issues with no expiry' do + it 'does not show issues with no expiry' do create(:issue, project: project) create(:issue, project: project, milestone: milestone) @@ -25,7 +25,7 @@ expect(page).to have_css('.issue', count: 0) end - it 'should show issues in future' do + it 'shows issues in future' do milestone = create(:milestone, project: project, due_date: Date.tomorrow) create(:issue, project: project) create(:issue, project: project, milestone: milestone) @@ -36,7 +36,7 @@ expect(page).to have_css('.issue', count: 1) end - it 'should not show issues in past' do + it 'does not show issues in past' do milestone = create(:milestone, project: project, due_date: Date.yesterday) create(:issue, project: project) create(:issue, project: project, milestone: milestone) diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb index 4b9b5394b61afc..ea81ee54c908b2 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -26,17 +26,17 @@ end context 'assignee', js: true do - it 'should update to current user' do + it 'updates to current user' do expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) end - it 'should not change when closed link is clicked' do + it 'does not change when closed link is clicked' do find('.issues-state-filters a', text: "Closed").click expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) end - it 'should not change when all link is clicked' do + it 'does not change when all link is clicked' do find('.issues-state-filters a', text: "All").click expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) @@ -56,17 +56,17 @@ end context 'milestone', js: true do - it 'should update to current milestone' do + it 'updates to current milestone' do expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title) end - it 'should not change when closed link is clicked' do + it 'does not change when closed link is clicked' do find('.issues-state-filters a', text: "Closed").click expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title) end - it 'should not change when all link is clicked' do + it 'does not change when all link is clicked' do find('.issues-state-filters a', text: "All").click expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title) @@ -81,7 +81,7 @@ wait_for_ajax end - it 'should filter by any label' do + it 'filters by any label' do find('.dropdown-menu-labels a', text: 'Any Label').click page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click wait_for_ajax @@ -89,7 +89,7 @@ expect(find('.labels-filter')).to have_content 'Label' end - it 'should filter by no label' do + it 'filters by no label' do find('.dropdown-menu-labels a', text: 'No Label').click page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click wait_for_ajax @@ -100,7 +100,7 @@ expect(find('.js-label-select .dropdown-toggle-text')).to have_content('No Label') end - it 'should filter by no label' do + it 'filters by no label' do find('.dropdown-menu-labels a', text: label.title).click page.within '.labels-filter' do expect(page).to have_content label.title @@ -128,19 +128,19 @@ end context 'assignee and label', js: true do - it 'should update to current assignee and label' do + it 'updates to current assignee and label' do expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title) end - it 'should not change when closed link is clicked' do + it 'does not change when closed link is clicked' do find('.issues-state-filters a', text: "Closed").click expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title) end - it 'should not change when all link is clicked' do + it 'does not change when all link is clicked' do find('.issues-state-filters a', text: "All").click expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) @@ -168,7 +168,7 @@ end context 'only text', js: true do - it 'should filter issues by searched text' do + it 'filters issues by searched text' do fill_in 'issue_search', with: 'Bug' page.within '.issues-list' do @@ -176,7 +176,7 @@ end end - it 'should not show any issues' do + it 'does not show any issues' do fill_in 'issue_search', with: 'testing' page.within '.issues-list' do @@ -186,7 +186,7 @@ end context 'text and dropdown options', js: true do - it 'should filter by text and label' do + it 'filters by text and label' do fill_in 'issue_search', with: 'Bug' page.within '.issues-list' do @@ -204,7 +204,7 @@ end end - it 'should filter by text and milestone' do + it 'filters by text and milestone' do fill_in 'issue_search', with: 'Bug' page.within '.issues-list' do @@ -221,7 +221,7 @@ end end - it 'should filter by text and assignee' do + it 'filters by text and assignee' do fill_in 'issue_search', with: 'Bug' page.within '.issues-list' do @@ -238,7 +238,7 @@ end end - it 'should filter by text and author' do + it 'filters by text and author' do fill_in 'issue_search', with: 'Bug' page.within '.issues-list' do @@ -269,7 +269,7 @@ visit namespace_project_issues_path(project.namespace, project) end - it 'should be able to filter and sort issues' do + it 'is able to filter and sort issues' do click_button 'Label' wait_for_ajax page.within '.labels-filter' do diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb index 5739bc64dfb2b8..4b1aec8bf7164e 100644 --- a/spec/features/issues/issue_sidebar_spec.rb +++ b/spec/features/issues/issue_sidebar_spec.rb @@ -17,7 +17,7 @@ end describe 'when clicking on edit labels', js: true do - it 'dropdown has an option to create a new label' do + it 'shows dropdown option to create a new label' do find('.block.labels .edit-link').click page.within('.block.labels') do @@ -27,7 +27,7 @@ end context 'creating a new label', js: true do - it 'option to crate a new label is present' do + it 'shows option to crate a new label is present' do page.within('.block.labels') do find('.edit-link').click @@ -35,7 +35,7 @@ end end - it 'dropdown switches to "create label" section' do + it 'shows dropdown switches to "create label" section' do page.within('.block.labels') do find('.edit-link').click click_link 'Create new' @@ -44,7 +44,7 @@ end end - it 'new label is added' do + it 'adds new label' do page.within('.block.labels') do find('.edit-link').click sleep 1 diff --git a/spec/features/issues/new_branch_button_spec.rb b/spec/features/issues/new_branch_button_spec.rb index 16e188d2a8af86..e528aff4d41454 100644 --- a/spec/features/issues/new_branch_button_spec.rb +++ b/spec/features/issues/new_branch_button_spec.rb @@ -41,7 +41,7 @@ end context "for visiters" do - it 'no button is shown', js: true do + it 'shows no buttons', js: true do visit namespace_project_issue_path(project.namespace, project, issue) expect(page).not_to have_css('#new-branch') diff --git a/spec/features/issues/todo_spec.rb b/spec/features/issues/todo_spec.rb index bc0f437a8ce8d2..de8fdda388dc7a 100644 --- a/spec/features/issues/todo_spec.rb +++ b/spec/features/issues/todo_spec.rb @@ -11,7 +11,7 @@ visit namespace_project_issue_path(project.namespace, project, issue) end - it 'should create todo when clicking button' do + it 'creates todo when clicking button' do page.within '.issuable-sidebar' do click_button 'Add Todo' expect(page).to have_content 'Mark Done' @@ -28,7 +28,7 @@ end end - it 'should mark a todo as done' do + it 'marks a todo as done' do page.within '.issuable-sidebar' do click_button 'Add Todo' click_button 'Mark Done' diff --git a/spec/features/issues/update_issues_spec.rb b/spec/features/issues/update_issues_spec.rb index ddbd69b28912af..ae5da3877a89aa 100644 --- a/spec/features/issues/update_issues_spec.rb +++ b/spec/features/issues/update_issues_spec.rb @@ -13,7 +13,7 @@ end context 'status', js: true do - it 'should be set to closed' do + it 'sets to closed' do visit namespace_project_issues_path(project.namespace, project) find('#check_all_issues').click @@ -24,7 +24,7 @@ expect(page).to have_selector('.issue', count: 0) end - it 'should be set to open' do + it 'sets to open' do create_closed visit namespace_project_issues_path(project.namespace, project, state: 'closed') @@ -38,7 +38,7 @@ end context 'assignee', js: true do - it 'should update to current user' do + it 'updates to current user' do visit namespace_project_issues_path(project.namespace, project) find('#check_all_issues').click @@ -52,7 +52,7 @@ end end - it 'should update to unassigned' do + it 'updates to unassigned' do create_assigned visit namespace_project_issues_path(project.namespace, project) @@ -68,7 +68,7 @@ context 'milestone', js: true do let(:milestone) { create(:milestone, project: project) } - it 'should update milestone' do + it 'updates milestone' do visit namespace_project_issues_path(project.namespace, project) find('#check_all_issues').click @@ -80,7 +80,7 @@ expect(find('.issue')).to have_content milestone.title end - it 'should set to no milestone' do + it 'sets to no milestone' do create_with_milestone visit namespace_project_issues_path(project.namespace, project) diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index 9c92b52898c391..cb445e22af0a5a 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -26,7 +26,7 @@ find('.js-zen-enter').click end - it 'should open new issue popup' do + it 'opens new issue popup' do expect(page).to have_content("Issue ##{issue.iid}") end @@ -71,7 +71,7 @@ visit new_namespace_project_issue_path(project.namespace, project) end - it 'should save with due date' do + it 'saves with due date' do date = Date.today.at_beginning_of_month fill_in 'issue_title', with: 'bug 345' @@ -99,7 +99,7 @@ visit edit_namespace_project_issue_path(project.namespace, project, issue) end - it 'should save with due date' do + it 'saves with due date' do date = Date.today.at_beginning_of_month expect(find('#issuable-due-date').value).to eq date.to_s @@ -155,7 +155,7 @@ let(:issue) { @issue } - it 'should allow filtering by issues with no specified assignee' do + it 'allows filtering by issues with no specified assignee' do visit namespace_project_issues_path(project.namespace, project, assignee_id: IssuableFinder::NONE) expect(page).to have_content 'foobar' @@ -163,7 +163,7 @@ expect(page).not_to have_content 'gitlab' end - it 'should allow filtering by a specified assignee' do + it 'allows filtering by a specified assignee' do visit namespace_project_issues_path(project.namespace, project, assignee_id: @user.id) expect(page).not_to have_content 'foobar' @@ -514,7 +514,7 @@ visit new_namespace_project_issue_path(project.namespace, project) end - it 'should upload file when dragging into textarea' do + it 'uploads file when dragging into textarea' do drop_in_dropzone test_image_file # Wait for the file to upload @@ -562,7 +562,7 @@ visit namespace_project_issue_path(project.namespace, project, issue) end - it 'should add due date to issue' do + it 'adds due date to issue' do page.within '.due_date' do click_link 'Edit' @@ -574,7 +574,7 @@ end end - it 'should remove due date from issue' do + it 'removes due date from issue' do page.within '.due_date' do click_link 'Edit' diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb index c4e8b1da5312d0..2523b4b78982c6 100644 --- a/spec/features/login_spec.rb +++ b/spec/features/login_spec.rb @@ -131,7 +131,7 @@ def stub_omniauth_config(messages) expect_any_instance_of(Object).to receive(:omniauth_authorize_path).with(:user, "saml").and_return('/users/auth/saml') end - it 'should show 2FA prompt after OAuth login' do + it 'shows 2FA prompt after OAuth login' do stub_omniauth_config(enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'], providers: [saml_config]) user = create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: 'saml') login_via('saml', user, 'my-uid') diff --git a/spec/features/merge_requests/award_spec.rb b/spec/features/merge_requests/award_spec.rb index 007f67d60804e1..ac260e118d00c9 100644 --- a/spec/features/merge_requests/award_spec.rb +++ b/spec/features/merge_requests/award_spec.rb @@ -11,7 +11,7 @@ visit namespace_project_merge_request_path(project.namespace, project, merge_request) end - it 'should add award to merge request' do + it 'adds award to merge request' do first('.js-emoji-btn').click expect(page).to have_selector('.js-emoji-btn.active') expect(first('.js-emoji-btn')).to have_content '1' @@ -20,7 +20,7 @@ expect(first('.js-emoji-btn')).to have_content '1' end - it 'should remove award from merge request' do + it 'removes award from merge request' do first('.js-emoji-btn').click find('.js-emoji-btn.active').click expect(first('.js-emoji-btn')).to have_content '0' @@ -29,7 +29,7 @@ expect(first('.js-emoji-btn')).to have_content '0' end - it 'should only have one menu on the page' do + it 'has only one menu on the page' do first('.js-add-award').click expect(page).to have_selector('.emoji-menu') @@ -42,7 +42,7 @@ visit namespace_project_merge_request_path(project.namespace, project, merge_request) end - it 'should not see award menu button' do + it 'does not see award menu button' do expect(page).not_to have_selector('.js-award-holder') end end diff --git a/spec/features/merge_requests/edit_mr_spec.rb b/spec/features/merge_requests/edit_mr_spec.rb index 9e007ab7635f90..4109e78f56031f 100644 --- a/spec/features/merge_requests/edit_mr_spec.rb +++ b/spec/features/merge_requests/edit_mr_spec.rb @@ -14,7 +14,7 @@ end context 'editing a MR' do - it 'form should have class js-quick-submit' do + it 'has class js-quick-submit in form' do expect(page).to have_selector('.js-quick-submit') end end diff --git a/spec/features/merge_requests/filter_by_milestone_spec.rb b/spec/features/merge_requests/filter_by_milestone_spec.rb index e3ecd60a5f3e12..bb0bb590a465c2 100644 --- a/spec/features/merge_requests/filter_by_milestone_spec.rb +++ b/spec/features/merge_requests/filter_by_milestone_spec.rb @@ -21,7 +21,7 @@ end context 'filters by upcoming milestone', js: true do - it 'should not show issues with no expiry' do + it 'does not show issues with no expiry' do create(:merge_request, :with_diffs, source_project: project) create(:merge_request, :simple, source_project: project, milestone: milestone) @@ -31,7 +31,7 @@ expect(page).to have_css('.merge-request', count: 0) end - it 'should show issues in future' do + it 'shows issues in future' do milestone = create(:milestone, project: project, due_date: Date.tomorrow) create(:merge_request, :with_diffs, source_project: project) create(:merge_request, :simple, source_project: project, milestone: milestone) @@ -42,7 +42,7 @@ expect(page).to have_css('.merge-request', count: 1) end - it 'should not show issues in past' do + it 'does not show issues in past' do milestone = create(:milestone, project: project, due_date: Date.yesterday) create(:merge_request, :with_diffs, source_project: project) create(:merge_request, :simple, source_project: project, milestone: milestone) diff --git a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb index 96f7b8c993238c..60bc07bd1a0fa9 100644 --- a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb +++ b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb @@ -73,7 +73,7 @@ end context 'Build is not active' do - it "should not allow for enabling" do + it "does not allow for enabling" do visit_merge_request(merge_request) expect(page).not_to have_link "Merge When Build Succeeds" end diff --git a/spec/features/milestone_spec.rb b/spec/features/milestone_spec.rb index c2c7acff3e8dc7..c43661e56813ae 100644 --- a/spec/features/milestone_spec.rb +++ b/spec/features/milestone_spec.rb @@ -13,7 +13,7 @@ end feature 'Create a milestone' do - scenario 'should show an informative message for a new issue' do + scenario 'shows an informative message for a new issue' do visit new_namespace_project_milestone_path(project.namespace, project) page.within '.milestone-form' do fill_in "milestone_title", with: '8.7' @@ -25,7 +25,7 @@ end feature 'Open a milestone with closed issues' do - scenario 'should show an informative message' do + scenario 'shows an informative message' do create(:issue, title: "Bugfix1", project: project, milestone: milestone, state: "closed") visit namespace_project_milestone_path(project.namespace, project, milestone) diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb index 0b38c413f4401c..7a9edbbe33968f 100644 --- a/spec/features/notes_on_merge_requests_spec.rb +++ b/spec/features/notes_on_merge_requests_spec.rb @@ -23,7 +23,7 @@ subject { page } describe 'the note form' do - it 'should be valid' do + it 'is valid' do is_expected.to have_css('.js-main-target-form', visible: true, count: 1) expect(find('.js-main-target-form input[type=submit]').value). to eq('Comment') @@ -39,7 +39,7 @@ end end - it 'should have enable submit button and preview button' do + it 'has enable submit button and preview button' do page.within('.js-main-target-form') do expect(page).not_to have_css('.js-comment-button[disabled]') expect(page).to have_css('.js-md-preview-button', visible: true) @@ -57,7 +57,7 @@ end end - it 'should be added and form reset' do + it 'is added and form reset' do is_expected.to have_content('This is awsome!') page.within('.js-main-target-form') do expect(page).to have_no_field('note[note]', with: 'This is awesome!') @@ -70,7 +70,7 @@ end describe 'when editing a note', js: true do - it 'should contain the hidden edit form' do + it 'contains the hidden edit form' do page.within("#note_#{note.id}") do is_expected.to have_css('.note-edit-form', visible: false) end @@ -82,7 +82,7 @@ find(".js-note-edit").click end - it 'should show the note edit form and hide the note body' do + it 'shows the note edit form and hide the note body' do page.within("#note_#{note.id}") do expect(find('.current-note-edit-form', visible: true)).to be_visible expect(find('.note-edit-form', visible: true)).to be_visible @@ -234,7 +234,7 @@ end end - it 'should be added as discussion' do + it 'adds as discussion' do is_expected.to have_content('Another comment on line 10') is_expected.to have_css('.notes_holder') is_expected.to have_css('.notes_holder .note', count: 1) diff --git a/spec/features/participants_autocomplete_spec.rb b/spec/features/participants_autocomplete_spec.rb index c7c00a3266a32c..a78a1c9c8905ca 100644 --- a/spec/features/participants_autocomplete_spec.rb +++ b/spec/features/participants_autocomplete_spec.rb @@ -12,17 +12,17 @@ end shared_examples "open suggestions" do - it 'suggestions are displayed' do + it 'displays suggestions' do expect(page).to have_selector('.atwho-view', visible: true) end - it 'author is suggested' do + it 'suggests author' do page.within('.atwho-view', visible: true) do expect(page).to have_content(author.username) end end - it 'participant is suggested' do + it 'suggests participant' do page.within('.atwho-view', visible: true) do expect(page).to have_content(participant.username) end diff --git a/spec/features/pipelines_spec.rb b/spec/features/pipelines_spec.rb index 377a9aba60d890..eace76c370f15c 100644 --- a/spec/features/pipelines_spec.rb +++ b/spec/features/pipelines_spec.rb @@ -82,11 +82,11 @@ before { visit namespace_project_pipelines_path(project.namespace, project) } - it 'not be cancelable' do + it 'is not cancelable' do expect(page).not_to have_link('Cancel') end - it 'pipeline is running' do + it 'has pipeline running' do expect(page).to have_selector('.ci-running') end end @@ -96,11 +96,11 @@ before { visit namespace_project_pipelines_path(project.namespace, project) } - it 'not be retryable' do + it 'is not retryable' do expect(page).not_to have_link('Retry') end - it 'pipeline is failed' do + it 'has failed pipeline' do expect(page).to have_selector('.ci-failed') end end @@ -147,7 +147,7 @@ before { visit namespace_project_pipeline_path(project.namespace, project, pipeline) } - it 'showing a list of builds' do + it 'shows a list of builds' do expect(page).to have_content('Test') expect(page).to have_content(@success.id) expect(page).to have_content('Deploy') diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb index c80253fead8c7d..c3d8c349ca4c1b 100644 --- a/spec/features/profile_spec.rb +++ b/spec/features/profile_spec.rb @@ -15,7 +15,7 @@ it { expect(page).to have_content('Remove account') } - it 'should delete the account' do + it 'deletes the account' do expect { click_link 'Delete account' }.to change { User.count }.by(-1) expect(current_path).to eq(new_user_session_path) end @@ -27,7 +27,7 @@ visit profile_account_path end - it 'should not have option to remove account' do + it 'does not have option to remove account' do expect(page).not_to have_content('Remove account') expect(current_path).to eq(profile_account_path) end diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb index 98ba93b4036b3e..cb7495da8ebf4b 100644 --- a/spec/features/projects/labels/update_prioritization_spec.rb +++ b/spec/features/projects/labels/update_prioritization_spec.rb @@ -87,7 +87,7 @@ end context 'as a guest' do - it 'can not prioritize labels' do + it 'does not prioritize labels' do user = create(:user) guest = create(:user) project = create(:project, name: 'test', namespace: user.namespace) @@ -102,7 +102,7 @@ end context 'as a non signed in user' do - it 'can not prioritize labels' do + it 'does not prioritize labels' do user = create(:user) project = create(:project, name: 'test', namespace: user.namespace) diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index 6fa8298d4895b7..1b14c66fe286ec 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -44,7 +44,7 @@ visit edit_namespace_project_path(project.namespace, project) end - it 'should remove fork' do + it 'removes fork' do expect(page).to have_content 'Remove fork relationship' remove_with_confirm('Remove fork relationship', project.path) @@ -65,7 +65,7 @@ visit edit_namespace_project_path(project.namespace, project) end - it 'should remove project' do + it 'removes project' do expect { remove_with_confirm('Remove project', project.path) }.to change {Project.count}.by(-1) end end @@ -82,7 +82,7 @@ visit namespace_project_path(project.namespace, project) end - it 'click toggle and show dropdown', js: true do + it 'clicks toggle and shows dropdown', js: true do find('.js-projects-dropdown-toggle').click expect(page).to have_css('.dropdown-menu-projects .dropdown-content li', count: 1) end @@ -102,7 +102,7 @@ visit namespace_project_issue_path(project.namespace, project, issue) end - it 'click toggle and show dropdown' do + it 'clicks toggle and shows dropdown' do find('.js-projects-dropdown-toggle').click expect(page).to have_css('.dropdown-menu-projects .dropdown-content li', count: 2) diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb index 09f70cd3b002e5..b7a25d80fec90a 100644 --- a/spec/features/search_spec.rb +++ b/spec/features/search_spec.rb @@ -12,7 +12,7 @@ visit search_path end - it 'top right search form is not present' do + it 'does not show top right search form' do expect(page).not_to have_selector('.search') end @@ -76,16 +76,16 @@ visit namespace_project_path(project.namespace, project) end - it 'top right search form is present' do + it 'shows top right search form' do expect(page).to have_selector('#search') end - it 'top right search form contains location badge' do + it 'contains location badge in top right search form' do expect(page).to have_selector('.has-location-badge') end context 'clicking the search field', js: true do - it 'should show category search dropdown' do + it 'shows category search dropdown' do page.find('#search').click expect(page).to have_selector('.dropdown-header', text: /#{project.name}/i) @@ -97,7 +97,7 @@ page.find('#search').click end - it 'should take user to her issues page when issues assigned is clicked' do + it 'takes user to her issues page when issues assigned is clicked' do find('.dropdown-menu').click_link 'Issues assigned to me' sleep 2 @@ -105,7 +105,7 @@ expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) end - it 'should take user to her issues page when issues authored is clicked' do + it 'takes user to her issues page when issues authored is clicked' do find('.dropdown-menu').click_link "Issues I've created" sleep 2 @@ -113,7 +113,7 @@ expect(find('.js-author-search .dropdown-toggle-text')).to have_content(user.name) end - it 'should take user to her MR page when MR assigned is clicked' do + it 'takes user to her MR page when MR assigned is clicked' do find('.dropdown-menu').click_link 'Merge requests assigned to me' sleep 2 @@ -121,7 +121,7 @@ expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) end - it 'should take user to her MR page when MR authored is clicked' do + it 'takes user to her MR page when MR authored is clicked' do find('.dropdown-menu').click_link "Merge requests I've created" sleep 2 @@ -137,7 +137,7 @@ end end - it 'should not display the category search dropdown' do + it 'does not display the category search dropdown' do expect(page).not_to have_selector('.dropdown-header', text: /#{project.name}/i) end end diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb index 0bdb1628c74850..0342f4f1d97e31 100644 --- a/spec/features/todos/todos_spec.rb +++ b/spec/features/todos/todos_spec.rb @@ -24,7 +24,7 @@ visit dashboard_todos_path end - it 'todo is present' do + it 'has todo present' do expect(page).to have_selector('.todos-list .todo', count: 1) end diff --git a/spec/features/variables_spec.rb b/spec/features/variables_spec.rb index a2b8f7b6931ffb..61f2bc61e0ca8d 100644 --- a/spec/features/variables_spec.rb +++ b/spec/features/variables_spec.rb @@ -13,13 +13,13 @@ visit namespace_project_variables_path(project.namespace, project) end - it 'should show list of variables' do + it 'shows list of variables' do page.within('.variables-table') do expect(page).to have_content(variable.key) end end - it 'should add new variable' do + it 'adds new variable' do fill_in('variable_key', with: 'key') fill_in('variable_value', with: 'key value') click_button('Add new variable') @@ -29,7 +29,7 @@ end end - it 'should delete variable' do + it 'deletes variable' do page.within('.variables-table') do find('.btn-variable-delete').click end @@ -37,7 +37,7 @@ expect(page).not_to have_selector('variables-table') end - it 'should edit variable' do + it 'edits variable' do page.within('.variables-table') do find('.btn-variable-edit').click end diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb index bc385fd0d691c6..535aabfc18d5fd 100644 --- a/spec/finders/merge_requests_finder_spec.rb +++ b/spec/finders/merge_requests_finder_spec.rb @@ -18,13 +18,13 @@ end describe "#execute" do - it 'should filter by scope' do + it 'filters by scope' do params = { scope: 'authored', state: 'opened' } merge_requests = MergeRequestsFinder.new(user, params).execute expect(merge_requests.size).to eq(2) end - it 'should filter by project' do + it 'filters by project' do params = { project_id: project1.id, scope: 'authored', state: 'opened' } merge_requests = MergeRequestsFinder.new(user, params).execute expect(merge_requests.size).to eq(1) diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb index 8db897b16466bf..7c6860372cc2c1 100644 --- a/spec/finders/notes_finder_spec.rb +++ b/spec/finders/notes_finder_spec.rb @@ -19,12 +19,12 @@ note2 end - it 'should find all notes' do + it 'finds all notes' do notes = NotesFinder.new.execute(project, user, params) expect(notes.size).to eq(2) end - it 'should raise an exception for an invalid target_type' do + it 'raises an exception for an invalid target_type' do params.merge!(target_type: 'invalid') expect { NotesFinder.new.execute(project, user, params) }.to raise_error('invalid target_type') end diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 3e15a137e33a6c..73f5470cf358bb 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -54,7 +54,7 @@ def stub_action_name(value) describe 'project_icon' do let(:avatar_file_path) { File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') } - it 'should return an url for the avatar' do + it 'returns an url for the avatar' do project = create(:project, avatar: File.open(avatar_file_path)) avatar_url = "http://localhost/uploads/project/avatar/#{project.id}/banana_sample.gif" @@ -62,7 +62,7 @@ def stub_action_name(value) to eq "\"Banana" end - it 'should give uploaded icon when present' do + it 'gives uploaded icon when present' do project = create(:project) allow_any_instance_of(Project).to receive(:avatar_in_git).and_return(true) @@ -76,14 +76,14 @@ def stub_action_name(value) describe 'avatar_icon' do let(:avatar_file_path) { File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') } - it 'should return an url for the avatar' do + it 'returns an url for the avatar' do user = create(:user, avatar: File.open(avatar_file_path)) expect(helper.avatar_icon(user.email).to_s). to match("/uploads/user/avatar/#{user.id}/banana_sample.gif") end - it 'should return an url for the avatar with relative url' do + it 'returns an url for the avatar with relative url' do stub_config_setting(relative_url_root: '/gitlab') # Must be stubbed after the stub above, and separately stub_config_setting(url: Settings.send(:build_gitlab_url)) @@ -94,14 +94,14 @@ def stub_action_name(value) to match("/gitlab/uploads/user/avatar/#{user.id}/banana_sample.gif") end - it 'should call gravatar_icon when no User exists with the given email' do + it 'calls gravatar_icon when no User exists with the given email' do expect(helper).to receive(:gravatar_icon).with('foo@example.com', 20, 2) helper.avatar_icon('foo@example.com', 20, 2) end describe 'using a User' do - it 'should return an URL for the avatar' do + it 'returns an URL for the avatar' do user = create(:user, avatar: File.open(avatar_file_path)) expect(helper.avatar_icon(user).to_s). @@ -146,7 +146,7 @@ def stub_action_name(value) to match('https://secure.gravatar.com') end - it 'should return custom gravatar path when gravatar_url is set' do + it 'returns custom gravatar path when gravatar_url is set' do stub_gravatar_setting(plain_url: 'http://example.local/?s=%{size}&hash=%{hash}') expect(gravatar_icon(user_email, 20)). @@ -266,19 +266,19 @@ def element(*arguments) allow(helper).to receive(:current_user).and_return(user) end - it 'should preserve encoding' do + it 'preserves encoding' do expect(content.encoding.name).to eq('UTF-8') expect(helper.render_markup('foo.rst', content).encoding.name).to eq('UTF-8') end - it "should delegate to #markdown when file name corresponds to Markdown" do + it "delegates to #markdown when file name corresponds to Markdown" do expect(helper).to receive(:gitlab_markdown?).with('foo.md').and_return(true) expect(helper).to receive(:markdown).and_return('NOEL') expect(helper.render_markup('foo.md', content)).to eq('NOEL') end - it "should delegate to #asciidoc when file name corresponds to AsciiDoc" do + it "delegates to #asciidoc when file name corresponds to AsciiDoc" do expect(helper).to receive(:asciidoc?).with('foo.adoc').and_return(true) expect(helper).to receive(:asciidoc).and_return('NOEL') diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb index b2d6d59b1ee0c4..94972eed945b27 100644 --- a/spec/helpers/blob_helper_spec.rb +++ b/spec/helpers/blob_helper_spec.rb @@ -17,19 +17,19 @@ def test(input): end describe '#highlight' do - it 'should return plaintext for unknown lexer context' do + it 'returns plaintext for unknown lexer context' do result = helper.highlight(blob_name, no_context_content) expect(result).to eq(%[

:type "assem"))
]) end - it 'should highlight single block' do + it 'highlights single block' do expected = %Q[
(make-pathname :defaults name
 :type "assem"))
] expect(helper.highlight(blob_name, blob_content)).to eq(expected) end - it 'should highlight multi-line comments' do + it 'highlights multi-line comments' do result = helper.highlight(blob_name, multiline_content) html = Nokogiri::HTML(result) lines = html.search('.s') @@ -49,7 +49,7 @@ def test(input): ddd) end - it 'should highlight each line properly' do + it 'highlights each line properly' do result = helper.highlight(blob_name, blob_content) expect(result).to eq(expected) end @@ -62,7 +62,7 @@ def test(input): let(:expected_svg_path) { File.join(Rails.root, 'spec', 'fixtures', 'sanitized.svg') } let(:expected) { open(expected_svg_path).read } - it 'should retain essential elements' do + it 'retains essential elements' do blob = OpenStruct.new(data: data) expect(sanitize_svg(blob).data).to eq(expected) end diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb index b6554de1c64989..faecad744c0e72 100644 --- a/spec/helpers/diff_helper_spec.rb +++ b/spec/helpers/diff_helper_spec.rb @@ -32,16 +32,16 @@ end describe 'diff_options' do - it 'should return no collapse false' do + it 'returns no collapse false' do expect(diff_options).to include(no_collapse: false) end - it 'should return no collapse true if expand_all_diffs' do + it 'returns no collapse true if expand_all_diffs' do allow(controller).to receive(:params) { { expand_all_diffs: true } } expect(diff_options).to include(no_collapse: true) end - it 'should return no collapse true if action name diff_for_path' do + it 'returns no collapse true if action name diff_for_path' do allow(controller).to receive(:action_name) { 'diff_for_path' } expect(diff_options).to include(no_collapse: true) end @@ -60,11 +60,11 @@ end describe '#diff_line_content' do - it 'should return non breaking space when line is empty' do + it 'returns non breaking space when line is empty' do expect(diff_line_content(nil)).to eq('  ') end - it 'should return the line itself' do + it 'returns the line itself' do expect(diff_line_content(diff_file.diff_lines.first.text)). to eq('@@ -6,12 +6,18 @@ module Popen') expect(diff_line_content(diff_file.diff_lines.first.type)).to eq('match') diff --git a/spec/helpers/emails_helper_spec.rb b/spec/helpers/emails_helper_spec.rb index 7a3e38d7e63cae..3223556e1d3444 100644 --- a/spec/helpers/emails_helper_spec.rb +++ b/spec/helpers/emails_helper_spec.rb @@ -8,37 +8,37 @@ def validate_time_string(time_limit, expected_string) end context 'when time limit is less than 2 hours' do - it 'should display the time in hours using a singular unit' do + it 'displays the time in hours using a singular unit' do validate_time_string(1.hour, '1 hour') end end context 'when time limit is 2 or more hours' do - it 'should display the time in hours using a plural unit' do + it 'displays the time in hours using a plural unit' do validate_time_string(2.hours, '2 hours') end end context 'when time limit contains fractions of an hour' do - it 'should round down to the nearest hour' do + it 'rounds down to the nearest hour' do validate_time_string(96.minutes, '1 hour') end end context 'when time limit is 24 or more hours' do - it 'should display the time in days using a singular unit' do + it 'displays the time in days using a singular unit' do validate_time_string(24.hours, '1 day') end end context 'when time limit is 2 or more days' do - it 'should display the time in days using a plural unit' do + it 'displays the time in days using a plural unit' do validate_time_string(2.days, '2 days') end end context 'when time limit contains fractions of a day' do - it 'should round down to the nearest day' do + it 'rounds down to the nearest day' do validate_time_string(57.hours, '2 days') end end diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb index 6b5e3d93d48da8..022aba0c0d0799 100644 --- a/spec/helpers/events_helper_spec.rb +++ b/spec/helpers/events_helper_spec.rb @@ -6,34 +6,34 @@ allow(helper).to receive(:current_user).and_return(double) end - it 'should display one line of plain text without alteration' do + it 'displays one line of plain text without alteration' do input = 'A short, plain note' expect(helper.event_note(input)).to match(input) expect(helper.event_note(input)).not_to match(/\.\.\.\z/) end - it 'should display inline code' do + it 'displays inline code' do input = 'A note with `inline code`' expected = 'A note with inline code' expect(helper.event_note(input)).to match(expected) end - it 'should truncate a note with multiple paragraphs' do + it 'truncates a note with multiple paragraphs' do input = "Paragraph 1\n\nParagraph 2" expected = 'Paragraph 1...' expect(helper.event_note(input)).to match(expected) end - it 'should display the first line of a code block' do + it 'displays the first line of a code block' do input = "```\nCode block\nwith two lines\n```" expected = %r{Code block\.\.\.} expect(helper.event_note(input)).to match(expected) end - it 'should truncate a single long line of text' do + it 'truncates a single long line of text' do text = 'The quick brown fox jumped over the lazy dog twice' # 50 chars input = text * 4 expected = (text * 2).sub(/.{3}/, '...') @@ -41,7 +41,7 @@ expect(helper.event_note(input)).to match(expected) end - it 'should preserve a link href when link text is truncated' do + it 'preserves a link href when link text is truncated' do text = 'The quick brown fox jumped over the lazy dog' # 44 chars input = "#{text}#{text}#{text} " # 133 chars link_url = 'http://example.com/foo/bar/baz' # 30 chars @@ -52,7 +52,7 @@ expect(helper.event_note(input)).to match(expected_link_text) end - it 'should preserve code color scheme' do + it 'preserves code color scheme' do input = "```ruby\ndef test\n 'hello world'\nend\n```" expected = '
' \
         "def test\n" \
diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb
index ade5c3b02d9366..5368e5fab067fd 100644
--- a/spec/helpers/gitlab_markdown_helper_spec.rb
+++ b/spec/helpers/gitlab_markdown_helper_spec.rb
@@ -26,17 +26,17 @@
     describe "referencing multiple objects" do
       let(:actual) { "#{merge_request.to_reference} -> #{commit.to_reference} -> #{issue.to_reference}" }
 
-      it "should link to the merge request" do
+      it "links to the merge request" do
         expected = namespace_project_merge_request_path(project.namespace, project, merge_request)
         expect(helper.markdown(actual)).to match(expected)
       end
 
-      it "should link to the commit" do
+      it "links to the commit" do
         expected = namespace_project_commit_path(project.namespace, project, commit)
         expect(helper.markdown(actual)).to match(expected)
       end
 
-      it "should link to the issue" do
+      it "links to the issue" do
         expected = namespace_project_issue_path(project.namespace, project, issue)
         expect(helper.markdown(actual)).to match(expected)
       end
@@ -47,7 +47,7 @@
       let(:second_project) { create(:project, :public) }
       let(:second_issue) { create(:issue, project: second_project) }
 
-      it 'should link to the issue' do
+      it 'links to the issue' do
         expected = namespace_project_issue_path(second_project.namespace, second_project, second_issue)
         expect(markdown(actual, project: second_project)).to match(expected)
       end
@@ -58,7 +58,7 @@
     let(:commit_path) { namespace_project_commit_path(project.namespace, project, commit) }
     let(:issues)      { create_list(:issue, 2, project: project) }
 
-    it 'should handle references nested in links with all the text' do
+    it 'handles references nested in links with all the text' do
       actual = helper.link_to_gfm("This should finally fix #{issues[0].to_reference} and #{issues[1].to_reference} for real", commit_path)
       doc = Nokogiri::HTML.parse(actual)
 
@@ -88,7 +88,7 @@
       expect(doc.css('a')[4].text).to eq ' for real'
     end
 
-    it 'should forward HTML options' do
+    it 'forwards HTML options' do
       actual = helper.link_to_gfm("Fixed in #{commit.id}", commit_path, class: 'foo')
       doc = Nokogiri::HTML.parse(actual)
 
@@ -110,7 +110,7 @@
       expect(act).to eq %Q(#{issues[0].to_reference})
     end
 
-    it 'should replace commit message with emoji to link' do
+    it 'replaces commit message with emoji to link' do
       actual = link_to_gfm(':book:Book', '/foo')
       expect(actual).
         to eq %Q(:book:Book)
@@ -125,7 +125,7 @@
       helper.instance_variable_set(:@project_wiki, @wiki)
     end
 
-    it "should use Wiki pipeline for markdown files" do
+    it "uses Wiki pipeline for markdown files" do
       allow(@wiki).to receive(:format).and_return(:markdown)
 
       expect(helper).to receive(:markdown).with('wiki content', pipeline: :wiki, project_wiki: @wiki, page_slug: "nested/page")
@@ -133,7 +133,7 @@
       helper.render_wiki_content(@wiki)
     end
 
-    it "should use Asciidoctor for asciidoc files" do
+    it "uses Asciidoctor for asciidoc files" do
       allow(@wiki).to receive(:format).and_return(:asciidoc)
 
       expect(helper).to receive(:asciidoc).with('wiki content')
@@ -141,7 +141,7 @@
       helper.render_wiki_content(@wiki)
     end
 
-    it "should use the Gollum renderer for all other file types" do
+    it "uses the Gollum renderer for all other file types" do
       allow(@wiki).to receive(:format).and_return(:rdoc)
       formatted_content_stub = double('formatted_content')
       expect(formatted_content_stub).to receive(:html_safe)
diff --git a/spec/helpers/graph_helper_spec.rb b/spec/helpers/graph_helper_spec.rb
index 4acf38771b7ab8..51c49f0e5871b8 100644
--- a/spec/helpers/graph_helper_spec.rb
+++ b/spec/helpers/graph_helper_spec.rb
@@ -6,7 +6,7 @@
     let(:commit)  { project.commit("master") }
     let(:graph) { Network::Graph.new(project, 'master', commit, '') }
 
-    it 'filter our refs used by GitLab' do
+    it 'filters our refs used by GitLab' do
       allow(commit).to receive(:ref_names).and_return(['refs/merge-requests/abc', 'master', 'refs/tmp/xyz'])
       self.instance_variable_set(:@graph, graph)
       refs = get_refs(project.repository, commit)
diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb
index 4ea90a80a92664..0807534720a9b6 100644
--- a/spec/helpers/groups_helper_spec.rb
+++ b/spec/helpers/groups_helper_spec.rb
@@ -4,7 +4,7 @@
   describe 'group_icon' do
     avatar_file_path = File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif')
 
-    it 'should return an url for the avatar' do
+    it 'returns an url for the avatar' do
       group = create(:group)
       group.avatar = File.open(avatar_file_path)
       group.save!
@@ -12,7 +12,7 @@
         to match("/uploads/group/avatar/#{group.id}/banana_sample.gif")
     end
 
-    it 'should give default avatar_icon when no avatar is present' do
+    it 'gives default avatar_icon when no avatar is present' do
       group = create(:group)
       group.save!
       expect(group_icon(group.path)).to match('group_avatar.png')
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index 9ee46dd2508b24..5e4655dfc95f5d 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -10,18 +10,19 @@
     let(:ext_expected) { issues_url.gsub(':id', issue.iid.to_s).gsub(':project_id', ext_project.id.to_s) }
     let(:int_expected) { polymorphic_path([@project.namespace, project, issue]) }
 
-    it "should return internal path if used internal tracker" do
+    it "returns internal path if used internal tracker" do
       @project = project
+
       expect(url_for_issue(issue.iid)).to match(int_expected)
     end
 
-    it "should return path to external tracker" do
+    it "returns path to external tracker" do
       @project = ext_project
 
       expect(url_for_issue(issue.iid)).to match(ext_expected)
     end
 
-    it "should return empty string if project nil" do
+    it "returns empty string if project nil" do
       @project = nil
 
       expect(url_for_issue(issue.iid)).to eq ""
@@ -45,7 +46,7 @@
         allow(Gitlab.config).to receive(:issues_tracker).and_return(nil)
       end
 
-      it "should return external path" do
+      it "returns external path" do
         expect(url_for_issue(issue.iid)).to match(ext_expected)
       end
     end
diff --git a/spec/helpers/notes_helper_spec.rb b/spec/helpers/notes_helper_spec.rb
index af371248ae95ee..153f1864ceb804 100644
--- a/spec/helpers/notes_helper_spec.rb
+++ b/spec/helpers/notes_helper_spec.rb
@@ -21,7 +21,7 @@
   end
 
   describe "#notes_max_access_for_users" do
-    it 'return human access levels' do
+    it 'returns human access levels' do
       expect(helper.note_max_access_for_user(owner_note)).to eq('Owner')
       expect(helper.note_max_access_for_user(master_note)).to eq('Master')
       expect(helper.note_max_access_for_user(reporter_note)).to eq('Reporter')
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index 601b6915e27e83..b0bb991539b38c 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -42,7 +42,7 @@ def simple_sanitize(str)
         expect(search_autocomplete_opts(project.name).size).to eq(1)
       end
 
-      it "should not include the public group" do
+      it "does not include the public group" do
         group = create(:group)
         expect(search_autocomplete_opts(group.name).size).to eq(0)
       end
diff --git a/spec/helpers/submodule_helper_spec.rb b/spec/helpers/submodule_helper_spec.rb
index 101217591322d5..37ac6a2699d2d8 100644
--- a/spec/helpers/submodule_helper_spec.rb
+++ b/spec/helpers/submodule_helper_spec.rb
@@ -17,35 +17,35 @@
         allow(Gitlab.config.gitlab).to receive(:protocol).and_return('http') # set this just to be sure
       end
 
-      it 'should detect ssh on standard port' do
+      it 'detects ssh on standard port' do
         allow(Gitlab.config.gitlab_shell).to receive(:ssh_port).and_return(22) # set this just to be sure
         allow(Gitlab.config.gitlab_shell).to receive(:ssh_path_prefix).and_return(Settings.send(:build_gitlab_shell_ssh_path_prefix))
         stub_url([ config.user, '@', config.host, ':gitlab-org/gitlab-ce.git' ].join(''))
         expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ])
       end
 
-      it 'should detect ssh on non-standard port' do
+      it 'detects ssh on non-standard port' do
         allow(Gitlab.config.gitlab_shell).to receive(:ssh_port).and_return(2222)
         allow(Gitlab.config.gitlab_shell).to receive(:ssh_path_prefix).and_return(Settings.send(:build_gitlab_shell_ssh_path_prefix))
         stub_url([ 'ssh://', config.user, '@', config.host, ':2222/gitlab-org/gitlab-ce.git' ].join(''))
         expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ])
       end
 
-      it 'should detect http on standard port' do
+      it 'detects http on standard port' do
         allow(Gitlab.config.gitlab).to receive(:port).and_return(80)
         allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url))
         stub_url([ 'http://', config.host, '/gitlab-org/gitlab-ce.git' ].join(''))
         expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ])
       end
 
-      it 'should detect http on non-standard port' do
+      it 'detects http on non-standard port' do
         allow(Gitlab.config.gitlab).to receive(:port).and_return(3000)
         allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url))
         stub_url([ 'http://', config.host, ':3000/gitlab-org/gitlab-ce.git' ].join(''))
         expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ])
       end
 
-      it 'should work with relative_url_root' do
+      it 'works with relative_url_root' do
         allow(Gitlab.config.gitlab).to receive(:port).and_return(80) # set this just to be sure
         allow(Gitlab.config.gitlab).to receive(:relative_url_root).and_return('/gitlab/root')
         allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url))
@@ -55,22 +55,22 @@
     end
 
     context 'submodule on github.com' do
-      it 'should detect ssh' do
+      it 'detects ssh' do
         stub_url('git@github.com:gitlab-org/gitlab-ce.git')
         expect(submodule_links(submodule_item)).to eq([ 'https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash' ])
       end
 
-      it 'should detect http' do
+      it 'detects http' do
         stub_url('http://github.com/gitlab-org/gitlab-ce.git')
         expect(submodule_links(submodule_item)).to eq([ 'https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash' ])
       end
 
-      it 'should detect https' do
+      it 'detects https' do
         stub_url('https://github.com/gitlab-org/gitlab-ce.git')
         expect(submodule_links(submodule_item)).to eq([ 'https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash' ])
       end
 
-      it 'should return original with non-standard url' do
+      it 'returns original with non-standard url' do
         stub_url('http://github.com/gitlab-org/gitlab-ce')
         expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ])
 
@@ -80,22 +80,22 @@
     end
 
     context 'submodule on gitlab.com' do
-      it 'should detect ssh' do
+      it 'detects ssh' do
         stub_url('git@gitlab.com:gitlab-org/gitlab-ce.git')
         expect(submodule_links(submodule_item)).to eq([ 'https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash' ])
       end
 
-      it 'should detect http' do
+      it 'detects http' do
         stub_url('http://gitlab.com/gitlab-org/gitlab-ce.git')
         expect(submodule_links(submodule_item)).to eq([ 'https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash' ])
       end
 
-      it 'should detect https' do
+      it 'detects https' do
         stub_url('https://gitlab.com/gitlab-org/gitlab-ce.git')
         expect(submodule_links(submodule_item)).to eq([ 'https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash' ])
       end
 
-      it 'should return original with non-standard url' do
+      it 'returns original with non-standard url' do
         stub_url('http://gitlab.com/gitlab-org/gitlab-ce')
         expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ])
 
@@ -105,7 +105,7 @@
     end
 
     context 'submodule on unsupported' do
-      it 'should return original' do
+      it 'returns original' do
         stub_url('http://mygitserver.com/gitlab-org/gitlab-ce')
         expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ])
 
diff --git a/spec/helpers/tree_helper_spec.rb b/spec/helpers/tree_helper_spec.rb
index c70dd8076e0e6c..8d6537ba4b54b6 100644
--- a/spec/helpers/tree_helper_spec.rb
+++ b/spec/helpers/tree_helper_spec.rb
@@ -12,7 +12,7 @@
     context "on a directory containing more than one file/directory" do
       let(:tree_item) { double(name: "files", path: "files") }
 
-      it "should return the directory name" do
+      it "returns the directory name" do
         expect(flatten_tree(tree_item)).to match('files')
       end
     end
@@ -20,7 +20,7 @@
     context "on a directory containing only one directory" do
       let(:tree_item) { double(name: "foo", path: "foo") }
 
-      it "should return the flattened path" do
+      it "returns the flattened path" do
         expect(flatten_tree(tree_item)).to match('foo/bar')
       end
     end
diff --git a/spec/lib/ci/charts_spec.rb b/spec/lib/ci/charts_spec.rb
index 97f2e97b062711..034ea098193adf 100644
--- a/spec/lib/ci/charts_spec.rb
+++ b/spec/lib/ci/charts_spec.rb
@@ -7,12 +7,12 @@
       FactoryGirl.create(:ci_build, pipeline: @pipeline)
     end
 
-    it 'should return build times in minutes' do
+    it 'returns build times in minutes' do
       chart = Ci::Charts::BuildTime.new(@pipeline.project)
       expect(chart.build_times).to eq([2])
     end
 
-    it 'should handle nil build times' do
+    it 'handles nil build times' do
       create(:ci_pipeline, duration: nil, project: @pipeline.project)
 
       chart = Ci::Charts::BuildTime.new(@pipeline.project)
-- 
GitLab


From 1d268a89deef10854193db48d65cf5d519a0363d Mon Sep 17 00:00:00 2001
From: tiagonbotelho 
Date: Mon, 1 Aug 2016 16:00:44 +0100
Subject: [PATCH 094/153] adds second batch of tests changed to active tense

---
 .../project_members_controller_spec.rb        |   2 +-
 spec/factories_spec.rb                        |   2 +-
 spec/helpers/diff_helper_spec.rb              |   4 +-
 spec/lib/disable_email_interceptor_spec.rb    |   2 +-
 spec/lib/extracts_path_spec.rb                |   4 +-
 spec/lib/gitlab/asciidoc_spec.rb              |   4 +-
 spec/lib/gitlab/auth_spec.rb                  |  10 +-
 spec/lib/gitlab/ldap/access_spec.rb           |   2 +-
 spec/lib/gitlab/ldap/user_spec.rb             |   2 +-
 spec/lib/gitlab/o_auth/user_spec.rb           |   6 +-
 .../lib/gitlab/project_search_results_spec.rb |  12 +-
 spec/lib/gitlab/saml/user_spec.rb             |  10 +-
 spec/lib/gitlab/search_results_spec.rb        |  12 +-
 spec/lib/gitlab/upgrader_spec.rb              |   6 +-
 spec/mailers/emails/profile_spec.rb           |   2 +-
 spec/mailers/notify_spec.rb                   |   2 +-
 spec/models/application_setting_spec.rb       |  22 +-
 spec/models/broadcast_message_spec.rb         |   6 +-
 spec/models/build_spec.rb                     |  30 +--
 spec/models/ci/pipeline_spec.rb               |   8 +-
 spec/models/ci/trigger_spec.rb                |   4 +-
 spec/models/commit_status_spec.rb             |  12 +-
 spec/models/concerns/milestoneish_spec.rb     |  36 +--
 .../concerns/token_authenticatable_spec.rb    |   2 +-
 spec/models/forked_project_link_spec.rb       |  10 +-
 spec/models/global_milestone_spec.rb          |  12 +-
 spec/models/group_spec.rb                     |   6 +-
 spec/models/hooks/project_hook_spec.rb        |   4 +-
 spec/models/key_spec.rb                       |   4 +-
 spec/models/label_spec.rb                     |   4 +-
 spec/models/legacy_diff_note_spec.rb          |   4 +-
 spec/models/members/group_member_spec.rb      |   4 +-
 spec/models/members/project_member_spec.rb    |   2 +-
 spec/models/merge_request_spec.rb             |   6 +-
 spec/models/milestone_spec.rb                 |  20 +-
 spec/models/namespace_spec.rb                 |   6 +-
 spec/models/note_spec.rb                      |   6 +-
 spec/models/project_security_spec.rb          |  18 +-
 .../project_services/asana_service_spec.rb    |   8 +-
 .../project_services/assembla_service_spec.rb |   2 +-
 .../external_wiki_service_spec.rb             |   2 +-
 .../project_services/flowdock_service_spec.rb |   2 +-
 .../gemnasium_service_spec.rb                 |   2 +-
 .../gitlab_issue_tracker_service_spec.rb      |   4 +-
 .../project_services/hipchat_service_spec.rb  |  38 +--
 .../project_services/irker_service_spec.rb    |   2 +-
 .../project_services/jira_service_spec.rb     |  10 +-
 .../project_services/pushover_service_spec.rb |   2 +-
 .../slack_service/note_message_spec.rb        |   1 +
 .../slack_service/wiki_page_message_spec.rb   |   4 +-
 .../project_services/slack_service_spec.rb    |  20 +-
 spec/models/project_spec.rb                   |  36 +--
 spec/models/repository_spec.rb                |  40 ++--
 spec/models/service_spec.rb                   |   4 +-
 spec/models/user_spec.rb                      |  26 +--
 spec/models/wiki_page_spec.rb                 |   6 +-
 spec/requests/api/api_helpers_spec.rb         |  30 +--
 spec/requests/api/award_emoji_spec.rb         |   6 +-
 spec/requests/api/branches_spec.rb            |  38 +--
 spec/requests/api/builds_spec.rb              |  38 +--
 spec/requests/api/commit_statuses_spec.rb     |  10 +-
 spec/requests/api/commits_spec.rb             |  42 ++--
 spec/requests/api/files_spec.rb               |  22 +-
 spec/requests/api/fork_spec.rb                |  12 +-
 spec/requests/api/group_members_spec.rb       |  34 +--
 spec/requests/api/groups_spec.rb              |  56 ++---
 spec/requests/api/issues_spec.rb              | 104 ++++-----
 spec/requests/api/keys_spec.rb                |   6 +-
 spec/requests/api/labels_spec.rb              |  60 ++---
 spec/requests/api/merge_requests_spec.rb      |  88 +++----
 spec/requests/api/milestones_spec.rb          |  30 +--
 spec/requests/api/namespaces_spec.rb          |  10 +-
 spec/requests/api/notes_spec.rb               |  56 ++---
 spec/requests/api/project_hooks_spec.rb       |  36 +--
 spec/requests/api/project_members_spec.rb     |  34 +--
 spec/requests/api/project_snippets_spec.rb    |   2 +-
 spec/requests/api/projects_spec.rb            | 176 +++++++-------
 spec/requests/api/repositories_spec.rb        |  40 ++--
 spec/requests/api/runners_spec.rb             |  98 ++++----
 spec/requests/api/services_spec.rb            |  22 +-
 spec/requests/api/session_spec.rb             |  12 +-
 spec/requests/api/settings_spec.rb            |   4 +-
 spec/requests/api/system_hooks_spec.rb        |  20 +-
 spec/requests/api/tags_spec.rb                |  34 +--
 spec/requests/api/triggers_spec.rb            |  44 ++--
 spec/requests/api/users_spec.rb               | 216 +++++++++---------
 spec/requests/api/variables_spec.rb           |  38 +--
 spec/requests/ci/api/builds_spec.rb           |  36 +--
 spec/requests/ci/api/triggers_spec.rb         |  16 +-
 spec/services/create_snippet_service_spec.rb  |   4 +-
 spec/services/event_create_service_spec.rb    |  14 +-
 spec/services/git_hooks_service_spec.rb       |   6 +-
 spec/services/git_push_service_spec.rb        |   6 +-
 .../issues/bulk_update_service_spec.rb        |   2 +-
 spec/services/issues/close_service_spec.rb    |   4 +-
 spec/services/issues/update_service_spec.rb   |   8 +-
 .../merge_requests/close_service_spec.rb      |   4 +-
 .../merge_requests/create_service_spec.rb     |   2 +-
 .../merge_requests/merge_service_spec.rb      |   4 +-
 .../merge_requests/refresh_service_spec.rb    |   4 +-
 .../merge_requests/reopen_service_spec.rb     |   6 +-
 .../merge_requests/update_service_spec.rb     |   8 +-
 spec/services/notification_service_spec.rb    |   8 +-
 .../projects/autocomplete_service_spec.rb     |  14 +-
 spec/services/projects/create_service_spec.rb |   6 +-
 spec/services/projects/fork_service_spec.rb   |   8 +-
 spec/services/projects/update_service_spec.rb |  18 +-
 .../repair_ldap_blocked_user_service_spec.rb  |   4 +-
 spec/services/search_service_spec.rb          |   8 +-
 spec/services/system_note_service_spec.rb     |  12 +-
 spec/services/test_hook_service_spec.rb       |   2 +-
 spec/tasks/gitlab/backup_rake_spec.rb         |  14 +-
 spec/tasks/gitlab/db_rake_spec.rb             |   8 +-
 spec/workers/post_receive_spec.rb             |   6 +-
 114 files changed, 1084 insertions(+), 1083 deletions(-)

diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb
index 19d05aef0a6fcb..5e2a8cf38490ba 100644
--- a/spec/controllers/projects/project_members_controller_spec.rb
+++ b/spec/controllers/projects/project_members_controller_spec.rb
@@ -167,7 +167,7 @@
           sign_in(user)
         end
 
-        it 'does not remove himself from the project' do
+        it 'cannot remove himself from the project' do
           delete :leave, namespace_id: project.namespace,
                          project_id: project
 
diff --git a/spec/factories_spec.rb b/spec/factories_spec.rb
index 675d9bd18b7aa9..786e1456f5fa7e 100644
--- a/spec/factories_spec.rb
+++ b/spec/factories_spec.rb
@@ -9,7 +9,7 @@
         expect { entity }.not_to raise_error
       end
 
-      it 'should be valid', if: factory.build_class < ActiveRecord::Base do
+      it 'is valid', if: factory.build_class < ActiveRecord::Base do
         expect(entity).to be_valid
       end
     end
diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb
index faecad744c0e72..9c7c79f57c6310 100644
--- a/spec/helpers/diff_helper_spec.rb
+++ b/spec/helpers/diff_helper_spec.rb
@@ -46,13 +46,13 @@
       expect(diff_options).to include(no_collapse: true)
     end
 
-    it 'should return paths if action name diff_for_path and param old path' do
+    it 'returns paths if action name diff_for_path and param old path' do
       allow(controller).to receive(:params) { { old_path: 'lib/wadus.rb' } }
       allow(controller).to receive(:action_name) { 'diff_for_path' }
       expect(diff_options[:paths]).to include('lib/wadus.rb')
     end
 
-    it 'should return paths if action name diff_for_path and param new path' do
+    it 'returns paths if action name diff_for_path and param new path' do
       allow(controller).to receive(:params) { { new_path: 'lib/wadus.rb' } }
       allow(controller).to receive(:action_name) { 'diff_for_path' }
       expect(diff_options[:paths]).to include('lib/wadus.rb')
diff --git a/spec/lib/disable_email_interceptor_spec.rb b/spec/lib/disable_email_interceptor_spec.rb
index 309a88151cf88c..8f51474476d757 100644
--- a/spec/lib/disable_email_interceptor_spec.rb
+++ b/spec/lib/disable_email_interceptor_spec.rb
@@ -5,7 +5,7 @@
     Mail.register_interceptor(DisableEmailInterceptor)
   end
 
-  it 'should not send emails' do
+  it 'does not send emails' do
     allow(Gitlab.config.gitlab).to receive(:email_enabled).and_return(false)
     expect { deliver_mail }.not_to change(ActionMailer::Base.deliveries, :count)
   end
diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb
index 566035c60d0d24..b12a7b98d4d6bb 100644
--- a/spec/lib/extracts_path_spec.rb
+++ b/spec/lib/extracts_path_spec.rb
@@ -25,7 +25,7 @@
       @project = create(:project)
     end
 
-    it "log tree path should have no escape sequences" do
+    it "log tree path has no escape sequences" do
       assign_ref_vars
       expect(@logs_path).to eq("/#{@project.path_with_namespace}/refs/#{ref}/logs_tree/files/ruby/popen.rb")
     end
@@ -33,7 +33,7 @@
     context 'escaped sequences in ref' do
       let(:ref) { "improve%2Fawesome" }
 
-      it "id should have no escape sequences" do
+      it "id has no escape sequences" do
         assign_ref_vars
         expect(@ref).to eq('improve/awesome')
         expect(@logs_path).to eq("/#{@project.path_with_namespace}/refs/#{ref}/logs_tree/files/ruby/popen.rb")
diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb
index 32ca8239845ef2..4aba783dc334a1 100644
--- a/spec/lib/gitlab/asciidoc_spec.rb
+++ b/spec/lib/gitlab/asciidoc_spec.rb
@@ -8,7 +8,7 @@ module Gitlab
     let(:html) { 'H2O' }
 
     context "without project" do
-      it "should convert the input using Asciidoctor and default options" do
+      it "converts the input using Asciidoctor and default options" do
         expected_asciidoc_opts = {
             safe: :secure,
             backend: :html5,
@@ -24,7 +24,7 @@ module Gitlab
       context "with asciidoc_opts" do
         let(:asciidoc_opts) { { safe: :safe, attributes: ['foo'] } }
 
-        it "should merge the options with default ones" do
+        it "merges the options with default ones" do
           expected_asciidoc_opts = {
               safe: :safe,
               backend: :html5,
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index 7bec1367156e55..b0772cad3123cf 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -51,24 +51,24 @@
     let(:username) { 'John' }     # username isn't lowercase, test this
     let(:password) { 'my-secret' }
 
-    it "should find user by valid login/password" do
+    it "finds user by valid login/password" do
       expect( gl_auth.find_with_user_password(username, password) ).to eql user
     end
 
-    it 'should find user by valid email/password with case-insensitive email' do
+    it 'finds user by valid email/password with case-insensitive email' do
       expect(gl_auth.find_with_user_password(user.email.upcase, password)).to eql user
     end
 
-    it 'should find user by valid username/password with case-insensitive username' do
+    it 'finds user by valid username/password with case-insensitive username' do
       expect(gl_auth.find_with_user_password(username.upcase, password)).to eql user
     end
 
-    it "should not find user with invalid password" do
+    it "does not find user with invalid password" do
       password = 'wrong'
       expect( gl_auth.find_with_user_password(username, password) ).not_to eql user
     end
 
-    it "should not find user with invalid login" do
+    it "does not find user with invalid login" do
       user = 'wrong'
       expect( gl_auth.find_with_user_password(username, password) ).not_to eql user
     end
diff --git a/spec/lib/gitlab/ldap/access_spec.rb b/spec/lib/gitlab/ldap/access_spec.rb
index acd5394382c72a..534bcbf39febdb 100644
--- a/spec/lib/gitlab/ldap/access_spec.rb
+++ b/spec/lib/gitlab/ldap/access_spec.rb
@@ -64,7 +64,7 @@
             user.ldap_block
           end
 
-          it 'should unblock user in GitLab' do
+          it 'unblocks user in GitLab' do
             access.allowed?
             expect(user).not_to be_blocked
           end
diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb
index 949f6e2b19a3ee..89790c9e1af45d 100644
--- a/spec/lib/gitlab/ldap/user_spec.rb
+++ b/spec/lib/gitlab/ldap/user_spec.rb
@@ -36,7 +36,7 @@
       expect(ldap_user.changed?).to be_truthy
     end
 
-    it "dont marks existing ldap user as changed" do
+    it "does not mark existing ldap user as changed" do
       create(:omniauth_user, email: 'john@example.com', extern_uid: 'my-uid', provider: 'ldapmain', ldap_email: true)
       expect(ldap_user.changed?).to be_falsey
     end
diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb
index 1fca8a13037875..78c669e8fa5a57 100644
--- a/spec/lib/gitlab/o_auth/user_spec.rb
+++ b/spec/lib/gitlab/o_auth/user_spec.rb
@@ -42,7 +42,7 @@ def stub_ldap_config(messages)
     describe 'signup' do
       shared_examples 'to verify compliance with allow_single_sign_on' do
         context 'provider is marked as external' do
-          it 'should mark user as external' do
+          it 'marks user as external' do
             stub_omniauth_config(allow_single_sign_on: ['twitter'], external_providers: ['twitter'])
             oauth_user.save
             expect(gl_user).to be_valid
@@ -51,7 +51,7 @@ def stub_ldap_config(messages)
         end
 
         context 'provider was external, now has been removed' do
-          it 'should not mark external user as internal' do
+          it 'does not mark external user as internal' do
             create(:omniauth_user, extern_uid: 'my-uid', provider: 'twitter', external: true)
             stub_omniauth_config(allow_single_sign_on: ['twitter'], external_providers: ['facebook'])
             oauth_user.save
@@ -62,7 +62,7 @@ def stub_ldap_config(messages)
 
         context 'provider is not external' do
           context 'when adding a new OAuth identity' do
-            it 'should not promote an external user to internal' do
+            it 'does not promote an external user to internal' do
               user = create(:user, email: 'john@mail.com', external: true)
               user.identities.create(provider: provider, extern_uid: uid)
 
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index 270b89972d73d1..29abb4d4d077ac 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -33,7 +33,7 @@
     let!(:security_issue_1) { create(:issue, :confidential, project: project, title: 'Security issue 1', author: author) }
     let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project, assignee: assignee) }
 
-    it 'should not list project confidential issues for non project members' do
+    it 'does not list project confidential issues for non project members' do
       results = described_class.new(non_member, project, query)
       issues = results.objects('issues')
 
@@ -43,7 +43,7 @@
       expect(results.issues_count).to eq 1
     end
 
-    it 'should not list project confidential issues for project members with guest role' do
+    it 'does not list project confidential issues for project members with guest role' do
       project.team << [member, :guest]
 
       results = described_class.new(member, project, query)
@@ -55,7 +55,7 @@
       expect(results.issues_count).to eq 1
     end
 
-    it 'should list project confidential issues for author' do
+    it 'lists project confidential issues for author' do
       results = described_class.new(author, project, query)
       issues = results.objects('issues')
 
@@ -65,7 +65,7 @@
       expect(results.issues_count).to eq 2
     end
 
-    it 'should list project confidential issues for assignee' do
+    it 'lists project confidential issues for assignee' do
       results = described_class.new(assignee, project.id, query)
       issues = results.objects('issues')
 
@@ -75,7 +75,7 @@
       expect(results.issues_count).to eq 2
     end
 
-    it 'should list project confidential issues for project members' do
+    it 'lists project confidential issues for project members' do
       project.team << [member, :developer]
 
       results = described_class.new(member, project, query)
@@ -87,7 +87,7 @@
       expect(results.issues_count).to eq 3
     end
 
-    it 'should list all project issues for admin' do
+    it 'lists all project issues for admin' do
       results = described_class.new(admin, project, query)
       issues = results.objects('issues')
 
diff --git a/spec/lib/gitlab/saml/user_spec.rb b/spec/lib/gitlab/saml/user_spec.rb
index 56bf08e704170e..02c139f1a0d131 100644
--- a/spec/lib/gitlab/saml/user_spec.rb
+++ b/spec/lib/gitlab/saml/user_spec.rb
@@ -67,7 +67,7 @@ def stub_saml_group_config(groups)
         end
 
         context 'user was external, now should not be' do
-          it 'should make user internal' do
+          it 'makes user internal' do
             existing_user.update_attribute('external', true)
             saml_user.save
             expect(gl_user).to be_valid
@@ -94,14 +94,14 @@ def stub_saml_group_config(groups)
 
         context 'with allow_single_sign_on default (["saml"])' do
           before { stub_omniauth_config(allow_single_sign_on: ['saml']) }
-          it 'should not throw an error' do
+          it 'does not throw an error' do
             expect{ saml_user.save }.not_to raise_error
           end
         end
 
         context 'with allow_single_sign_on disabled' do
           before { stub_omniauth_config(allow_single_sign_on: false) }
-          it 'should throw an error' do
+          it 'throws an error' do
             expect{ saml_user.save }.to raise_error StandardError
           end
         end
@@ -223,7 +223,7 @@ def stub_saml_group_config(groups)
         context 'dont block on create' do
           before { stub_omniauth_config(block_auto_created_users: false) }
 
-          it 'should not block the user' do
+          it 'does not block the user' do
             saml_user.save
             expect(gl_user).to be_valid
             expect(gl_user).not_to be_blocked
@@ -233,7 +233,7 @@ def stub_saml_group_config(groups)
         context 'block on create' do
           before { stub_omniauth_config(block_auto_created_users: true) }
 
-          it 'should block user' do
+          it 'blocks user' do
             saml_user.save
             expect(gl_user).to be_valid
             expect(gl_user).to be_blocked
diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
index 1bb444bf34fc21..8a656ab0ee9e1d 100644
--- a/spec/lib/gitlab/search_results_spec.rb
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -73,7 +73,7 @@
     let!(:security_issue_4) { create(:issue, :confidential, project: project_3, title: 'Security issue 4', assignee: assignee) }
     let!(:security_issue_5) { create(:issue, :confidential, project: project_4, title: 'Security issue 5') }
 
-    it 'should not list confidential issues for non project members' do
+    it 'does not list confidential issues for non project members' do
       results = described_class.new(non_member, limit_projects, query)
       issues = results.objects('issues')
 
@@ -86,7 +86,7 @@
       expect(results.issues_count).to eq 1
     end
 
-    it 'should not list confidential issues for project members with guest role' do
+    it 'does not list confidential issues for project members with guest role' do
       project_1.team << [member, :guest]
       project_2.team << [member, :guest]
 
@@ -102,7 +102,7 @@
       expect(results.issues_count).to eq 1
     end
 
-    it 'should list confidential issues for author' do
+    it 'lists confidential issues for author' do
       results = described_class.new(author, limit_projects, query)
       issues = results.objects('issues')
 
@@ -115,7 +115,7 @@
       expect(results.issues_count).to eq 3
     end
 
-    it 'should list confidential issues for assignee' do
+    it 'lists confidential issues for assignee' do
       results = described_class.new(assignee, limit_projects, query)
       issues = results.objects('issues')
 
@@ -128,7 +128,7 @@
       expect(results.issues_count).to eq 3
     end
 
-    it 'should list confidential issues for project members' do
+    it 'lists confidential issues for project members' do
       project_1.team << [member, :developer]
       project_2.team << [member, :developer]
 
@@ -144,7 +144,7 @@
       expect(results.issues_count).to eq 4
     end
 
-    it 'should list all issues for admin' do
+    it 'lists all issues for admin' do
       results = described_class.new(admin, limit_projects, query)
       issues = results.objects('issues')
 
diff --git a/spec/lib/gitlab/upgrader_spec.rb b/spec/lib/gitlab/upgrader_spec.rb
index e958e087a80ff0..edadab043d7bdf 100644
--- a/spec/lib/gitlab/upgrader_spec.rb
+++ b/spec/lib/gitlab/upgrader_spec.rb
@@ -9,19 +9,19 @@
   end
 
   describe 'latest_version?' do
-    it 'should be true if newest version' do
+    it 'is true if newest version' do
       allow(upgrader).to receive(:latest_version_raw).and_return(current_version)
       expect(upgrader.latest_version?).to be_truthy
     end
   end
 
   describe 'latest_version_raw' do
-    it 'should be latest version for GitLab 5' do
+    it 'is the latest version for GitLab 5' do
       allow(upgrader).to receive(:current_version_raw).and_return("5.3.0")
       expect(upgrader.latest_version_raw).to eq("v5.4.2")
     end
 
-    it 'should get the latest version from tags' do
+    it 'gets the latest version from tags' do
       allow(upgrader).to receive(:fetch_git_tags).and_return([
         '6f0733310546402c15d3ae6128a95052f6c8ea96  refs/tags/v7.1.1',
         'facfec4b242ce151af224e20715d58e628aa5e74  refs/tags/v7.1.1^{}',
diff --git a/spec/mailers/emails/profile_spec.rb b/spec/mailers/emails/profile_spec.rb
index c6758ccad39a16..781472d0c00084 100644
--- a/spec/mailers/emails/profile_spec.rb
+++ b/spec/mailers/emails/profile_spec.rb
@@ -48,7 +48,7 @@
       it_behaves_like 'it should not have Gmail Actions links'
       it_behaves_like 'a user cannot unsubscribe through footer link'
 
-      it 'should not contain the new user\'s password' do
+      it 'does not contain the new user\'s password' do
         is_expected.not_to have_body_text /password/
       end
     end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index e2866ef160c82e..fa241867858744 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -591,7 +591,7 @@ def invite_to_project(project:, email:, inviter:)
           is_expected.to have_body_text /#{note.note}/
         end
 
-        it 'not contains note author' do
+        it 'does not contain note author' do
           is_expected.not_to have_body_text /wrote\:/
         end
 
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index fb040ba82bc729..cc215d252f9cec 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -53,59 +53,59 @@
   end
 
   context 'restricted signup domains' do
-    it 'set single domain' do
+    it 'sets single domain' do
       setting.domain_whitelist_raw = 'example.com'
       expect(setting.domain_whitelist).to eq(['example.com'])
     end
 
-    it 'set multiple domains with spaces' do
+    it 'sets multiple domains with spaces' do
       setting.domain_whitelist_raw = 'example.com *.example.com'
       expect(setting.domain_whitelist).to eq(['example.com', '*.example.com'])
     end
 
-    it 'set multiple domains with newlines and a space' do
+    it 'sets multiple domains with newlines and a space' do
       setting.domain_whitelist_raw = "example.com\n *.example.com"
       expect(setting.domain_whitelist).to eq(['example.com', '*.example.com'])
     end
 
-    it 'set multiple domains with commas' do
+    it 'sets multiple domains with commas' do
       setting.domain_whitelist_raw = "example.com, *.example.com"
       expect(setting.domain_whitelist).to eq(['example.com', '*.example.com'])
     end
   end
 
   context 'blacklisted signup domains' do
-    it 'set single domain' do
+    it 'sets single domain' do
       setting.domain_blacklist_raw = 'example.com'
       expect(setting.domain_blacklist).to contain_exactly('example.com')
     end
 
-    it 'set multiple domains with spaces' do
+    it 'sets multiple domains with spaces' do
       setting.domain_blacklist_raw = 'example.com *.example.com'
       expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com')
     end
 
-    it 'set multiple domains with newlines and a space' do
+    it 'sets multiple domains with newlines and a space' do
       setting.domain_blacklist_raw = "example.com\n *.example.com"
       expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com')
     end
 
-    it 'set multiple domains with commas' do
+    it 'sets multiple domains with commas' do
       setting.domain_blacklist_raw = "example.com, *.example.com"
       expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com')
     end
 
-    it 'set multiple domains with semicolon' do
+    it 'sets multiple domains with semicolon' do
       setting.domain_blacklist_raw = "example.com; *.example.com"
       expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com')
     end
 
-    it 'set multiple domains with mixture of everything' do
+    it 'sets multiple domains with mixture of everything' do
       setting.domain_blacklist_raw = "example.com; *.example.com\n test.com\sblock.com   yes.com"
       expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com', 'test.com', 'block.com', 'yes.com')
     end
 
-    it 'set multiple domain with file' do
+    it 'sets multiple domain with file' do
       setting.domain_blacklist_file = File.open(Rails.root.join('spec/fixtures/', 'domain_blacklist.txt'))
       expect(setting.domain_blacklist).to contain_exactly('example.com', 'test.com', 'foo.bar')
     end
diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb
index 6ad8bfef4f2220..72688137f08f53 100644
--- a/spec/models/broadcast_message_spec.rb
+++ b/spec/models/broadcast_message_spec.rb
@@ -23,19 +23,19 @@
   end
 
   describe '.current' do
-    it "should return last message if time match" do
+    it "returns last message if time match" do
       message = create(:broadcast_message)
 
       expect(BroadcastMessage.current).to eq message
     end
 
-    it "should return nil if time not come" do
+    it "returns nil if time not come" do
       create(:broadcast_message, :future)
 
       expect(BroadcastMessage.current).to be_nil
     end
 
-    it "should return nil if time has passed" do
+    it "returns nil if time has passed" do
       create(:broadcast_message, :expired)
 
       expect(BroadcastMessage.current).to be_nil
diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb
index dc88697199b3eb..9ecc9aac84bb7f 100644
--- a/spec/models/build_spec.rb
+++ b/spec/models/build_spec.rb
@@ -32,7 +32,7 @@
     end
     let(:create_from_build) { Ci::Build.create_from build }
 
-    it 'there should be a pending task' do
+    it 'exists a pending task' do
       expect(Ci::Build.pending.count(:all)).to eq 0
       create_from_build
       expect(Ci::Build.pending.count(:all)).to be > 0
@@ -573,19 +573,19 @@
     let!(:rubocop_test) { create(:ci_build, pipeline: pipeline, name: 'rubocop', stage_idx: 1, stage: 'test') }
     let!(:staging) { create(:ci_build, pipeline: pipeline, name: 'staging', stage_idx: 2, stage: 'deploy') }
 
-    it 'to have no dependents if this is first build' do
+    it 'expects to have no dependents if this is first build' do
       expect(build.depends_on_builds).to be_empty
     end
 
-    it 'to have one dependent if this is test' do
+    it 'expects to have one dependent if this is test' do
       expect(rspec_test.depends_on_builds.map(&:id)).to contain_exactly(build.id)
     end
 
-    it 'to have all builds from build and test stage if this is last' do
+    it 'expects to have all builds from build and test stage if this is last' do
       expect(staging.depends_on_builds.map(&:id)).to contain_exactly(build.id, rspec_test.id, rubocop_test.id)
     end
 
-    it 'to have retried builds instead the original ones' do
+    it 'expects to have retried builds instead the original ones' do
       retried_rspec = Ci::Build.retry(rspec_test)
       expect(staging.depends_on_builds.map(&:id)).to contain_exactly(build.id, retried_rspec.id, rubocop_test.id)
     end
@@ -655,23 +655,23 @@ def create_mr(build, pipeline, factory: :merge_request, created_at: Time.now)
 
   describe 'build erasable' do
     shared_examples 'erasable' do
-      it 'should remove artifact file' do
+      it 'removes artifact file' do
         expect(build.artifacts_file.exists?).to be_falsy
       end
 
-      it 'should remove artifact metadata file' do
+      it 'removes artifact metadata file' do
         expect(build.artifacts_metadata.exists?).to be_falsy
       end
 
-      it 'should erase build trace in trace file' do
+      it 'erases build trace in trace file' do
         expect(build.trace).to be_empty
       end
 
-      it 'should set erased to true' do
+      it 'sets erased to true' do
         expect(build.erased?).to be true
       end
 
-      it 'should set erase date' do
+      it 'sets erase date' do
         expect(build.erased_at).not_to be_falsy
       end
     end
@@ -704,7 +704,7 @@ def create_mr(build, pipeline, factory: :merge_request, created_at: Time.now)
 
           include_examples 'erasable'
 
-          it 'should record user who erased a build' do
+          it 'records user who erased a build' do
             expect(build.erased_by).to eq user
           end
         end
@@ -714,7 +714,7 @@ def create_mr(build, pipeline, factory: :merge_request, created_at: Time.now)
 
           include_examples 'erasable'
 
-          it 'should not set user who erased a build' do
+          it 'does not set user who erased a build' do
             expect(build.erased_by).to be_nil
           end
         end
@@ -750,7 +750,7 @@ def create_mr(build, pipeline, factory: :merge_request, created_at: Time.now)
         end
 
         describe '#erase' do
-          it 'should not raise error' do
+          it 'does not raise error' do
             expect { build.erase }.not_to raise_error
           end
         end
@@ -900,7 +900,7 @@ def create_mr(build, pipeline, factory: :merge_request, created_at: Time.now)
     context 'when build is running' do
       before { build.run! }
 
-      it 'should return false' do
+      it 'returns false' do
         expect(build.retryable?).to be false
       end
     end
@@ -908,7 +908,7 @@ def create_mr(build, pipeline, factory: :merge_request, created_at: Time.now)
     context 'when build is finished' do
       before { build.success! }
 
-      it 'should return true' do
+      it 'returns true' do
         expect(build.retryable?).to be true
       end
     end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 0d4c86955ceb37..ccee591cf7a50e 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -427,7 +427,7 @@ def manual_actions
   end
 
   describe '#update_state' do
-    it 'execute update_state after touching object' do
+    it 'executes update_state after touching object' do
       expect(pipeline).to receive(:update_state).and_return(true)
       pipeline.touch
     end
@@ -435,7 +435,7 @@ def manual_actions
     context 'dependent objects' do
       let(:commit_status) { build :commit_status, pipeline: pipeline }
 
-      it 'execute update_state after saving dependent object' do
+      it 'executes update_state after saving dependent object' do
         expect(pipeline).to receive(:update_state).and_return(true)
         commit_status.save
       end
@@ -513,7 +513,7 @@ def manual_actions
         create :ci_build, :success, pipeline: pipeline, name: 'rspec'
         create :ci_build, :allowed_to_fail, :failed, pipeline: pipeline, name: 'rubocop'
       end
-      
+
       it 'returns true' do
         is_expected.to be_truthy
       end
@@ -524,7 +524,7 @@ def manual_actions
         create :ci_build, :success, pipeline: pipeline, name: 'rspec'
         create :ci_build, :allowed_to_fail, :success, pipeline: pipeline, name: 'rubocop'
       end
-      
+
       it 'returns false' do
         is_expected.to be_falsey
       end
diff --git a/spec/models/ci/trigger_spec.rb b/spec/models/ci/trigger_spec.rb
index 474b0b1621de3f..3ca9231f58e3ea 100644
--- a/spec/models/ci/trigger_spec.rb
+++ b/spec/models/ci/trigger_spec.rb
@@ -4,12 +4,12 @@
   let(:project) { FactoryGirl.create :empty_project }
 
   describe 'before_validation' do
-    it 'should set an random token if none provided' do
+    it 'sets an random token if none provided' do
       trigger = FactoryGirl.create :ci_trigger_without_token, project: project
       expect(trigger.token).not_to be_nil
     end
 
-    it 'should not set an random token if one provided' do
+    it 'does not set an random token if one provided' do
       trigger = FactoryGirl.create :ci_trigger, project: project
       expect(trigger.token).to eq('token')
     end
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index ff6371ad685406..fcfa3138ce50b1 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -133,7 +133,7 @@
       @commit5 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'aa', ref: 'bb', status: 'success'
     end
 
-    it 'return unique statuses' do
+    it 'returns unique statuses' do
       is_expected.to eq([@commit4, @commit5])
     end
   end
@@ -149,7 +149,7 @@
       @commit5 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'ee', ref: nil, status: 'canceled'
     end
 
-    it 'return statuses that are running or pending' do
+    it 'returns statuses that are running or pending' do
       is_expected.to eq([@commit1, @commit2])
     end
   end
@@ -160,7 +160,7 @@
     context 'when no before_sha is set for pipeline' do
       before { pipeline.before_sha = nil }
 
-      it 'return blank sha' do
+      it 'returns blank sha' do
         is_expected.to eq(Gitlab::Git::BLANK_SHA)
       end
     end
@@ -169,7 +169,7 @@
       let(:value) { '1234' }
       before { pipeline.before_sha = value }
 
-      it 'return the set value' do
+      it 'returns the set value' do
         is_expected.to eq(value)
       end
     end
@@ -186,7 +186,7 @@
     context 'stages list' do
       subject { CommitStatus.where(pipeline: pipeline).stages }
 
-      it 'return ordered list of stages' do
+      it 'returns ordered list of stages' do
         is_expected.to eq(%w(build test deploy))
       end
     end
@@ -194,7 +194,7 @@
     context 'stages with statuses' do
       subject { CommitStatus.where(pipeline: pipeline).latest.stages_status }
 
-      it 'return list of stages with statuses' do
+      it 'returns list of stages with statuses' do
         is_expected.to eq({
           'build' => 'failed',
           'test' => 'success',
diff --git a/spec/models/concerns/milestoneish_spec.rb b/spec/models/concerns/milestoneish_spec.rb
index 7e9ab8940cfa11..b7e973798a36e7 100644
--- a/spec/models/concerns/milestoneish_spec.rb
+++ b/spec/models/concerns/milestoneish_spec.rb
@@ -26,53 +26,53 @@
   end
 
   describe '#closed_items_count' do
-    it 'should not count confidential issues for non project members' do
+    it 'does not count confidential issues for non project members' do
       expect(milestone.closed_items_count(non_member)).to eq 2
     end
 
-    it 'should not count confidential issues for project members with guest role' do
+    it 'does not count confidential issues for project members with guest role' do
       expect(milestone.closed_items_count(guest)).to eq 2
     end
 
-    it 'should count confidential issues for author' do
+    it 'counts confidential issues for author' do
       expect(milestone.closed_items_count(author)).to eq 4
     end
 
-    it 'should count confidential issues for assignee' do
+    it 'counts confidential issues for assignee' do
       expect(milestone.closed_items_count(assignee)).to eq 4
     end
 
-    it 'should count confidential issues for project members' do
+    it 'counts confidential issues for project members' do
       expect(milestone.closed_items_count(member)).to eq 6
     end
 
-    it 'should count all issues for admin' do
+    it 'counts all issues for admin' do
       expect(milestone.closed_items_count(admin)).to eq 6
     end
   end
 
   describe '#total_items_count' do
-    it 'should not count confidential issues for non project members' do
+    it 'does not count confidential issues for non project members' do
       expect(milestone.total_items_count(non_member)).to eq 4
     end
 
-    it 'should not count confidential issues for project members with guest role' do
+    it 'does not count confidential issues for project members with guest role' do
       expect(milestone.total_items_count(guest)).to eq 4
     end
 
-    it 'should count confidential issues for author' do
+    it 'counts confidential issues for author' do
       expect(milestone.total_items_count(author)).to eq 7
     end
 
-    it 'should count confidential issues for assignee' do
+    it 'counts confidential issues for assignee' do
       expect(milestone.total_items_count(assignee)).to eq 7
     end
 
-    it 'should count confidential issues for project members' do
+    it 'counts confidential issues for project members' do
       expect(milestone.total_items_count(member)).to eq 10
     end
 
-    it 'should count all issues for admin' do
+    it 'counts all issues for admin' do
       expect(milestone.total_items_count(admin)).to eq 10
     end
   end
@@ -91,27 +91,27 @@
   end
 
   describe '#percent_complete' do
-    it 'should not count confidential issues for non project members' do
+    it 'does not count confidential issues for non project members' do
       expect(milestone.percent_complete(non_member)).to eq 50
     end
 
-    it 'should not count confidential issues for project members with guest role' do
+    it 'does not count confidential issues for project members with guest role' do
       expect(milestone.percent_complete(guest)).to eq 50
     end
 
-    it 'should count confidential issues for author' do
+    it 'counts confidential issues for author' do
       expect(milestone.percent_complete(author)).to eq 57
     end
 
-    it 'should count confidential issues for assignee' do
+    it 'counts confidential issues for assignee' do
       expect(milestone.percent_complete(assignee)).to eq 57
     end
 
-    it 'should count confidential issues for project members' do
+    it 'counts confidential issues for project members' do
       expect(milestone.percent_complete(member)).to eq 60
     end
 
-    it 'should count confidential issues for admin' do
+    it 'counts confidential issues for admin' do
       expect(milestone.percent_complete(admin)).to eq 60
     end
   end
diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb
index 9e8ebc56a316f1..eb64f3d0c83291 100644
--- a/spec/models/concerns/token_authenticatable_spec.rb
+++ b/spec/models/concerns/token_authenticatable_spec.rb
@@ -41,7 +41,7 @@
       describe 'ensured! token' do
         subject { described_class.new.send("ensure_#{token_field}!") }
 
-        it 'should persist new token' do
+        it 'persists new token' do
           expect(subject).to eq described_class.current[token_field]
         end
       end
diff --git a/spec/models/forked_project_link_spec.rb b/spec/models/forked_project_link_spec.rb
index f94987dcaff81a..9c81d159cdf6da 100644
--- a/spec/models/forked_project_link_spec.rb
+++ b/spec/models/forked_project_link_spec.rb
@@ -9,11 +9,11 @@
     @project_to = fork_project(project_from, user)
   end
 
-  it "project_to should know it is forked" do
+  it "project_to knows it is forked" do
     expect(@project_to.forked?).to be_truthy
   end
 
-  it "project should know who it is forked from" do
+  it "project knows who it is forked from" do
     expect(@project_to.forked_from_project).to eq(project_from)
   end
 end
@@ -29,15 +29,15 @@
     forked_project_link.save!
   end
 
-  it "project_to should know it is forked" do
+  it "project_to knows it is forked" do
     expect(project_to.forked?).to be_truthy
   end
 
-  it "project_from should not be forked" do
+  it "project_from is not forked" do
     expect(project_from.forked?).to be_falsey
   end
 
-  it "project_to.destroy should destroy fork_link" do
+  it "project_to.destroy destroys fork_link" do
     expect(forked_project_link).to receive(:destroy)
     project_to.destroy
   end
diff --git a/spec/models/global_milestone_spec.rb b/spec/models/global_milestone_spec.rb
index ae77ec5b3489e2..92e0f7f27cecc1 100644
--- a/spec/models/global_milestone_spec.rb
+++ b/spec/models/global_milestone_spec.rb
@@ -29,15 +29,15 @@
       @global_milestones = GlobalMilestone.build_collection(milestones)
     end
 
-    it 'should have all project milestones' do
+    it 'has all project milestones' do
       expect(@global_milestones.count).to eq(2)
     end
 
-    it 'should have all project milestones titles' do
+    it 'has all project milestones titles' do
       expect(@global_milestones.map(&:title)).to match_array(['Milestone v1.2', 'VD-123'])
     end
 
-    it 'should have all project milestones' do
+    it 'has all project milestones' do
       expect(@global_milestones.map { |group_milestone| group_milestone.milestones.count }.sum).to eq(6)
     end
   end
@@ -54,11 +54,11 @@
       @global_milestone = GlobalMilestone.new(milestone1_project1.title, milestones)
     end
 
-    it 'should have exactly one group milestone' do
+    it 'has exactly one group milestone' do
       expect(@global_milestone.title).to eq('Milestone v1.2')
     end
 
-    it 'should have all project milestones with the same title' do
+    it 'has all project milestones with the same title' do
       expect(@global_milestone.milestones.count).to eq(3)
     end
   end
@@ -66,7 +66,7 @@
   describe '#safe_title' do
     let(:milestone) { create(:milestone, title: "git / test", project: project1) }
 
-    it 'should strip out slashes and spaces' do
+    it 'strips out slashes and spaces' do
       global_milestone = GlobalMilestone.new(milestone.title, [milestone])
 
       expect(global_milestone.safe_title).to eq('git-test')
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 266c46213a6956..ea4b59c26b1e6d 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -116,7 +116,7 @@
     let(:user) { create(:user) }
     before { group.add_users([user.id], GroupMember::GUEST) }
 
-    it "should update the group permission" do
+    it "updates the group permission" do
       expect(group.group_members.guests.map(&:user)).to include(user)
       group.add_users([user.id], GroupMember::DEVELOPER)
       expect(group.group_members.developers.map(&:user)).to include(user)
@@ -128,12 +128,12 @@
     let(:user) { create(:user) }
     before { group.add_user(user, GroupMember::MASTER) }
 
-    it "should be true if avatar is image" do
+    it "is true if avatar is image" do
       group.update_attribute(:avatar, 'uploads/avatar.png')
       expect(group.avatar_type).to be_truthy
     end
 
-    it "should be false if avatar is html page" do
+    it "is false if avatar is html page" do
       group.update_attribute(:avatar, 'uploads/avatar.html')
       expect(group.avatar_type).to eq(["only images allowed"])
     end
diff --git a/spec/models/hooks/project_hook_spec.rb b/spec/models/hooks/project_hook_spec.rb
index 983848392b7eb5..4a457997a4fa9d 100644
--- a/spec/models/hooks/project_hook_spec.rb
+++ b/spec/models/hooks/project_hook_spec.rb
@@ -24,7 +24,7 @@
   end
 
   describe '.push_hooks' do
-    it 'should return hooks for push events only' do
+    it 'returns hooks for push events only' do
       hook = create(:project_hook, push_events: true)
       create(:project_hook, push_events: false)
       expect(ProjectHook.push_hooks).to eq([hook])
@@ -32,7 +32,7 @@
   end
 
   describe '.tag_push_hooks' do
-    it 'should return hooks for tag push events only' do
+    it 'returns hooks for tag push events only' do
       hook = create(:project_hook, tag_push_events: true)
       create(:project_hook, tag_push_events: false)
       expect(ProjectHook.tag_push_hooks).to eq([hook])
diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb
index 6d68e52a822c97..fd4a2beff586f1 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -73,13 +73,13 @@
   end
 
   context 'callbacks' do
-    it 'should add new key to authorized_file' do
+    it 'adds new key to authorized_file' do
       @key = build(:personal_key, id: 7)
       expect(GitlabShellWorker).to receive(:perform_async).with(:add_key, @key.shell_id, @key.key)
       @key.save
     end
 
-    it 'should remove key from authorized_file' do
+    it 'removes key from authorized_file' do
       @key = create(:personal_key)
       expect(GitlabShellWorker).to receive(:perform_async).with(:remove_key, @key.shell_id, @key.key)
       @key.destroy
diff --git a/spec/models/label_spec.rb b/spec/models/label_spec.rb
index f37f44a608e53c..2a09063f85779e 100644
--- a/spec/models/label_spec.rb
+++ b/spec/models/label_spec.rb
@@ -18,7 +18,7 @@
   describe 'validation' do
     it { is_expected.to validate_presence_of(:project) }
 
-    it 'should validate color code' do
+    it 'validates color code' do
       expect(label).not_to allow_value('G-ITLAB').for(:color)
       expect(label).not_to allow_value('AABBCC').for(:color)
       expect(label).not_to allow_value('#AABBCCEE').for(:color)
@@ -30,7 +30,7 @@
       expect(label).to allow_value('#abcdef').for(:color)
     end
 
-    it 'should validate title' do
+    it 'validates title' do
       expect(label).not_to allow_value('G,ITLAB').for(:title)
       expect(label).not_to allow_value('').for(:title)
 
diff --git a/spec/models/legacy_diff_note_spec.rb b/spec/models/legacy_diff_note_spec.rb
index c8ee656fe3b439..2cfd26419ca086 100644
--- a/spec/models/legacy_diff_note_spec.rb
+++ b/spec/models/legacy_diff_note_spec.rb
@@ -5,12 +5,12 @@
     let!(:note) { create(:legacy_diff_note_on_commit, note: "+1 from me") }
     let!(:commit) { note.noteable }
 
-    it "should save a valid note" do
+    it "saves a valid note" do
       expect(note.commit_id).to eq(commit.id)
       expect(note.noteable.id).to eq(commit.id)
     end
 
-    it "should be recognized by #legacy_diff_note?" do
+    it "is recognized by #legacy_diff_note?" do
       expect(note).to be_legacy_diff_note
     end
   end
diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb
index 18439cac2a4cb6..4f875fd257a5fa 100644
--- a/spec/models/members/group_member_spec.rb
+++ b/spec/models/members/group_member_spec.rb
@@ -22,7 +22,7 @@
 describe GroupMember, models: true do
   describe 'notifications' do
     describe "#after_create" do
-      it "should send email to user" do
+      it "sends email to user" do
         membership = build(:group_member)
 
         allow(membership).to receive(:notification_service).
@@ -40,7 +40,7 @@
           and_return(double('NotificationService').as_null_object)
       end
 
-      it "should send email to user" do
+      it "sends email to user" do
         expect(@group_member).to receive(:notification_service)
         @group_member.update_attribute(:access_level, GroupMember::MASTER)
       end
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index e7c0c50646313f..28673de3189520 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -52,7 +52,7 @@
       master_todos
     end
 
-    it "destroy itself and delete associated todos" do
+    it "destroys itself and delete associated todos" do
       expect(owner.user.todos.size).to eq(2)
       expect(master.user.todos.size).to eq(3)
       expect(Todo.count).to eq(5)
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index d793cfd0bde8ba..3270b877c1a7b5 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -188,12 +188,12 @@
       create(:note, noteable: merge_request, project: merge_request.project)
     end
 
-    it "should include notes for commits" do
+    it "includes notes for commits" do
       expect(merge_request.commits).not_to be_empty
       expect(merge_request.mr_and_commit_notes.count).to eq(2)
     end
 
-    it "should include notes for commits from target project as well" do
+    it "includes notes for commits from target project as well" do
       create(:note_on_commit, commit_id: merge_request.commits.first.id,
                               project: merge_request.target_project)
 
@@ -304,7 +304,7 @@
       expect(subject.can_remove_source_branch?(user)).to be_falsey
     end
 
-    it "cant remove a root ref" do
+    it "can't remove a root ref" do
       subject.source_branch = "master"
       subject.target_branch = "feature"
 
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index d661dc0e59ab10..d64d6cde2b517b 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -28,12 +28,12 @@
   end
 
   describe "unique milestone title per project" do
-    it "shouldn't accept the same title in a project twice" do
+    it "does not accept the same title in a project twice" do
       new_milestone = Milestone.new(project: milestone.project, title: milestone.title)
       expect(new_milestone).not_to be_valid
     end
 
-    it "should accept the same title in another project" do
+    it "accepts the same title in another project" do
       project = build(:project)
       new_milestone = Milestone.new(project: project, title: milestone.title)
 
@@ -42,29 +42,29 @@
   end
 
   describe "#percent_complete" do
-    it "should not count open issues" do
+    it "does not count open issues" do
       milestone.issues << issue
       expect(milestone.percent_complete(user)).to eq(0)
     end
 
-    it "should count closed issues" do
+    it "counts closed issues" do
       issue.close
       milestone.issues << issue
       expect(milestone.percent_complete(user)).to eq(100)
     end
 
-    it "should recover from dividing by zero" do
+    it "recovers from dividing by zero" do
       expect(milestone.percent_complete(user)).to eq(0)
     end
   end
 
   describe "#expires_at" do
-    it "should be nil when due_date is unset" do
+    it "is nil when due_date is unset" do
       milestone.update_attributes(due_date: nil)
       expect(milestone.expires_at).to be_nil
     end
 
-    it "should not be nil when due_date is set" do
+    it "is not nil when due_date is set" do
       milestone.update_attributes(due_date: Date.tomorrow)
       expect(milestone.expires_at).to be_present
     end
@@ -121,7 +121,7 @@
       create :merge_request, milestone: milestone
     end
 
-    it 'Should return total count of issues and merge requests assigned to milestone' do
+    it 'returns total count of issues and merge requests assigned to milestone' do
       expect(milestone.total_items_count(user)).to eq 2
     end
   end
@@ -134,11 +134,11 @@
       create :issue
     end
 
-    it 'should be true if milestone active and all nested issues closed' do
+    it 'returns true if milestone active and all nested issues closed' do
       expect(milestone.can_be_closed?).to be_truthy
     end
 
-    it 'should be false if milestone active and not all nested issues closed' do
+    it 'returns false if milestone active and not all nested issues closed' do
       issue.milestone = milestone
       issue.save
 
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index a162da0208e96a..544920d18240b4 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -61,11 +61,11 @@
       allow(@namespace).to receive(:path_changed?).and_return(true)
     end
 
-    it "should raise error when directory exists" do
+    it "raises error when directory exists" do
       expect { @namespace.move_dir }.to raise_error("namespace directory cannot be moved")
     end
 
-    it "should move dir if path changed" do
+    it "moves dir if path changed" do
       new_path = @namespace.path + "_new"
       allow(@namespace).to receive(:path_was).and_return(@namespace.path)
       allow(@namespace).to receive(:path).and_return(new_path)
@@ -93,7 +93,7 @@
 
     before { namespace.destroy }
 
-    it "should remove its dirs when deleted" do
+    it "removes its dirs when deleted" do
       expect(File.exist?(path)).to be(false)
     end
   end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 1243f5420a74a5..53733d253f7f86 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -56,18 +56,18 @@
     let!(:note) { create(:note_on_commit, note: "+1 from me") }
     let!(:commit) { note.noteable }
 
-    it "should be accessible through #noteable" do
+    it "is accessible through #noteable" do
       expect(note.commit_id).to eq(commit.id)
       expect(note.noteable).to be_a(Commit)
       expect(note.noteable).to eq(commit)
     end
 
-    it "should save a valid note" do
+    it "saves a valid note" do
       expect(note.commit_id).to eq(commit.id)
       note.noteable == commit
     end
 
-    it "should be recognized by #for_commit?" do
+    it "is recognized by #for_commit?" do
       expect(note).to be_for_commit
     end
 
diff --git a/spec/models/project_security_spec.rb b/spec/models/project_security_spec.rb
index 2142c7c13ef9fa..36379074ea0b52 100644
--- a/spec/models/project_security_spec.rb
+++ b/spec/models/project_security_spec.rb
@@ -21,7 +21,7 @@
     let(:owner_actions) { Ability.project_owner_rules }
 
     describe "Non member rules" do
-      it "should deny for non-project users any actions" do
+      it "denies for non-project users any actions" do
         owner_actions.each do |action|
           expect(@abilities.allowed?(@u1, action, @p1)).to be_falsey
         end
@@ -33,7 +33,7 @@
         @p1.project_members.create(project: @p1, user: @u2, access_level: ProjectMember::GUEST)
       end
 
-      it "should allow for project user any guest actions" do
+      it "allows for project user any guest actions" do
         guest_actions.each do |action|
           expect(@abilities.allowed?(@u2, action, @p1)).to be_truthy
         end
@@ -45,7 +45,7 @@
         @p1.project_members.create(project: @p1, user: @u2, access_level: ProjectMember::REPORTER)
       end
 
-      it "should allow for project user any report actions" do
+      it "allows for project user any report actions" do
         report_actions.each do |action|
           expect(@abilities.allowed?(@u2, action, @p1)).to be_truthy
         end
@@ -58,13 +58,13 @@
         @p1.project_members.create(project: @p1, user: @u3, access_level: ProjectMember::DEVELOPER)
       end
 
-      it "should deny for developer master-specific actions" do
+      it "denies for developer master-specific actions" do
         [dev_actions - report_actions].each do |action|
           expect(@abilities.allowed?(@u2, action, @p1)).to be_falsey
         end
       end
 
-      it "should allow for project user any dev actions" do
+      it "allows for project user any dev actions" do
         dev_actions.each do |action|
           expect(@abilities.allowed?(@u3, action, @p1)).to be_truthy
         end
@@ -77,13 +77,13 @@
         @p1.project_members.create(project: @p1, user: @u3, access_level: ProjectMember::MASTER)
       end
 
-      it "should deny for developer master-specific actions" do
+      it "denies for developer master-specific actions" do
         [master_actions - dev_actions].each do |action|
           expect(@abilities.allowed?(@u2, action, @p1)).to be_falsey
         end
       end
 
-      it "should allow for project user any master actions" do
+      it "allows for project user any master actions" do
         master_actions.each do |action|
           expect(@abilities.allowed?(@u3, action, @p1)).to be_truthy
         end
@@ -96,13 +96,13 @@
         @p1.project_members.create(project: @p1, user: @u3, access_level: ProjectMember::MASTER)
       end
 
-      it "should deny for masters admin-specific actions" do
+      it "denies for masters admin-specific actions" do
         [owner_actions - master_actions].each do |action|
           expect(@abilities.allowed?(@u2, action, @p1)).to be_falsey
         end
       end
 
-      it "should allow for project owner any admin actions" do
+      it "allows for project owner any admin actions" do
         owner_actions.each do |action|
           expect(@abilities.allowed?(@u4, action, @p1)).to be_truthy
         end
diff --git a/spec/models/project_services/asana_service_spec.rb b/spec/models/project_services/asana_service_spec.rb
index f3d15f3c1ea709..dc702cfc42c5c8 100644
--- a/spec/models/project_services/asana_service_spec.rb
+++ b/spec/models/project_services/asana_service_spec.rb
@@ -65,7 +65,7 @@ def create_data_for_commits(*messages)
       )
     end
 
-    it 'should call Asana service to create a story' do
+    it 'calls Asana service to create a story' do
       data = create_data_for_commits('Message from commit. related to #123456')
       expected_message = "#{data[:user_name]} pushed to branch #{data[:ref]} of #{project.name_with_namespace} ( #{data[:commits][0][:url]} ): #{data[:commits][0][:message]}"
 
@@ -76,7 +76,7 @@ def create_data_for_commits(*messages)
       @asana.execute(data)
     end
 
-    it 'should call Asana service to create a story and close a task' do
+    it 'calls Asana service to create a story and close a task' do
       data = create_data_for_commits('fix #456789')
       d1 = double('Asana::Task')
       expect(d1).to receive(:add_comment)
@@ -86,7 +86,7 @@ def create_data_for_commits(*messages)
       @asana.execute(data)
     end
 
-    it 'should be able to close via url' do
+    it 'is able to close via url' do
       data = create_data_for_commits('closes https://app.asana.com/19292/956299/42')
       d1 = double('Asana::Task')
       expect(d1).to receive(:add_comment)
@@ -96,7 +96,7 @@ def create_data_for_commits(*messages)
       @asana.execute(data)
     end
 
-    it 'should allow multiple matches per line' do
+    it 'allows multiple matches per line' do
       message = <<-EOF
       minor bigfix, refactoring, fixed #123 and Closes #456 work on #789
       ref https://app.asana.com/19292/956299/42 and closing https://app.asana.com/19292/956299/12
diff --git a/spec/models/project_services/assembla_service_spec.rb b/spec/models/project_services/assembla_service_spec.rb
index 17e9361dd5cd00..00c4e0fb64c912 100644
--- a/spec/models/project_services/assembla_service_spec.rb
+++ b/spec/models/project_services/assembla_service_spec.rb
@@ -44,7 +44,7 @@
       WebMock.stub_request(:post, @api_url)
     end
 
-    it "should call Assembla API" do
+    it "calls Assembla API" do
       @assembla_service.execute(@sample_data)
       expect(WebMock).to have_requested(:post, @api_url).with(
         body: /#{@sample_data[:before]}.*#{@sample_data[:after]}.*#{project.path}/
diff --git a/spec/models/project_services/external_wiki_service_spec.rb b/spec/models/project_services/external_wiki_service_spec.rb
index 5fe5ea7d2dfc51..d7c5ea95d71af9 100644
--- a/spec/models/project_services/external_wiki_service_spec.rb
+++ b/spec/models/project_services/external_wiki_service_spec.rb
@@ -56,7 +56,7 @@
         @service.destroy!
       end
 
-      it 'should replace the wiki url' do
+      it 'replaces the wiki url' do
         wiki_path = get_project_wiki_path(project)
         expect(wiki_path).to match('https://gitlab.com')
       end
diff --git a/spec/models/project_services/flowdock_service_spec.rb b/spec/models/project_services/flowdock_service_spec.rb
index b7e627e6518cb9..6518098ceea923 100644
--- a/spec/models/project_services/flowdock_service_spec.rb
+++ b/spec/models/project_services/flowdock_service_spec.rb
@@ -57,7 +57,7 @@
       WebMock.stub_request(:post, @api_url)
     end
 
-    it "should call FlowDock API" do
+    it "calls FlowDock API" do
       @flowdock_service.execute(@sample_data)
       @sample_data[:commits].each do |commit|
         # One request to Flowdock per new commit
diff --git a/spec/models/project_services/gemnasium_service_spec.rb b/spec/models/project_services/gemnasium_service_spec.rb
index a08f1ac229f1ed..2c5583bdaa23fd 100644
--- a/spec/models/project_services/gemnasium_service_spec.rb
+++ b/spec/models/project_services/gemnasium_service_spec.rb
@@ -57,7 +57,7 @@
       )
       @sample_data = Gitlab::PushDataBuilder.build_sample(project, user)
     end
-    it "should call Gemnasium service" do
+    it "calls Gemnasium service" do
       expect(Gemnasium::GitlabService).to receive(:execute).with(an_instance_of(Hash)).once
       @gemnasium_service.execute(@sample_data)
     end
diff --git a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
index 7a1f106d6e3a3a..8ef79a17d502a0 100644
--- a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
+++ b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
@@ -54,7 +54,7 @@
         @service.destroy!
       end
 
-      it 'should give the correct path' do
+      it 'gives the correct path' do
         expect(@service.project_url).to eq("http://localhost/gitlab/root/#{project.path_with_namespace}/issues")
         expect(@service.new_issue_url).to eq("http://localhost/gitlab/root/#{project.path_with_namespace}/issues/new")
         expect(@service.issue_url(432)).to eq("http://localhost/gitlab/root/#{project.path_with_namespace}/issues/432")
@@ -71,7 +71,7 @@
         @service.destroy!
       end
 
-      it 'should give the correct path' do
+      it 'gives the correct path' do
         expect(@service.project_path).to eq("/gitlab/root/#{project.path_with_namespace}/issues")
         expect(@service.new_issue_path).to eq("/gitlab/root/#{project.path_with_namespace}/issues/new")
         expect(@service.issue_path(432)).to eq("/gitlab/root/#{project.path_with_namespace}/issues/432")
diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb
index 62ae5f6cf74745..bf438b26690948 100644
--- a/spec/models/project_services/hipchat_service_spec.rb
+++ b/spec/models/project_services/hipchat_service_spec.rb
@@ -61,7 +61,7 @@
       WebMock.stub_request(:post, api_url)
     end
 
-    it 'should test and return errors' do
+    it 'tests and return errors' do
       allow(hipchat).to receive(:execute).and_raise(StandardError, 'no such room')
       result = hipchat.test(push_sample_data)
 
@@ -69,7 +69,7 @@
       expect(result[:result].to_s).to eq('no such room')
     end
 
-    it 'should use v1 if version is provided' do
+    it 'uses v1 if version is provided' do
       allow(hipchat).to receive(:api_version).and_return('v1')
       expect(HipChat::Client).to receive(:new).with(
         token,
@@ -79,7 +79,7 @@
       hipchat.execute(push_sample_data)
     end
 
-    it 'should use v2 as the version when nothing is provided' do
+    it 'uses v2 as the version when nothing is provided' do
       allow(hipchat).to receive(:api_version).and_return('')
       expect(HipChat::Client).to receive(:new).with(
         token,
@@ -90,13 +90,13 @@
     end
 
     context 'push events' do
-      it "should call Hipchat API for push events" do
+      it "calls Hipchat API for push events" do
         hipchat.execute(push_sample_data)
 
         expect(WebMock).to have_requested(:post, api_url).once
       end
 
-      it "should create a push message" do
+      it "creates a push message" do
         message = hipchat.send(:create_push_message, push_sample_data)
 
         push_sample_data[:object_attributes]
@@ -110,13 +110,13 @@
     context 'tag_push events' do
       let(:push_sample_data) { Gitlab::PushDataBuilder.build(project, user, Gitlab::Git::BLANK_SHA, '1' * 40, 'refs/tags/test', []) }
 
-      it "should call Hipchat API for tag push events" do
+      it "calls Hipchat API for tag push events" do
         hipchat.execute(push_sample_data)
 
         expect(WebMock).to have_requested(:post, api_url).once
       end
 
-      it "should create a tag push message" do
+      it "creates a tag push message" do
         message = hipchat.send(:create_push_message, push_sample_data)
 
         push_sample_data[:object_attributes]
@@ -131,13 +131,13 @@
       let(:issue_service) { Issues::CreateService.new(project, user) }
       let(:issues_sample_data) { issue_service.hook_data(issue, 'open') }
 
-      it "should call Hipchat API for issue events" do
+      it "calls Hipchat API for issue events" do
         hipchat.execute(issues_sample_data)
 
         expect(WebMock).to have_requested(:post, api_url).once
       end
 
-      it "should create an issue message" do
+      it "creates an issue message" do
         message = hipchat.send(:create_issue_message, issues_sample_data)
 
         obj_attr = issues_sample_data[:object_attributes]
@@ -154,13 +154,13 @@
       let(:merge_service) { MergeRequests::CreateService.new(project, user) }
       let(:merge_sample_data) { merge_service.hook_data(merge_request, 'open') }
 
-      it "should call Hipchat API for merge requests events" do
+      it "calls Hipchat API for merge requests events" do
         hipchat.execute(merge_sample_data)
 
         expect(WebMock).to have_requested(:post, api_url).once
       end
 
-      it "should create a merge request message" do
+      it "creates a merge request message" do
         message = hipchat.send(:create_merge_request_message,
                                merge_sample_data)
 
@@ -184,7 +184,7 @@
                                   note: 'a comment on a commit')
         end
 
-        it "should call Hipchat API for commit comment events" do
+        it "calls Hipchat API for commit comment events" do
           data = Gitlab::NoteDataBuilder.build(commit_note, user)
           hipchat.execute(data)
 
@@ -216,7 +216,7 @@
                                          note: "merge request note")
         end
 
-        it "should call Hipchat API for merge request comment events" do
+        it "calls Hipchat API for merge request comment events" do
           data = Gitlab::NoteDataBuilder.build(merge_request_note, user)
           hipchat.execute(data)
 
@@ -243,7 +243,7 @@
                                  note: "issue note")
         end
 
-        it "should call Hipchat API for issue comment events" do
+        it "calls Hipchat API for issue comment events" do
           data = Gitlab::NoteDataBuilder.build(issue_note, user)
           hipchat.execute(data)
 
@@ -269,7 +269,7 @@
                                            note: "snippet note")
         end
 
-        it "should call Hipchat API for snippet comment events" do
+        it "calls Hipchat API for snippet comment events" do
           data = Gitlab::NoteDataBuilder.build(snippet_note, user)
           hipchat.execute(data)
 
@@ -297,13 +297,13 @@
       context 'for failed' do
         before { build.drop }
 
-        it "should call Hipchat API" do
+        it "calls Hipchat API" do
           hipchat.execute(data)
 
           expect(WebMock).to have_requested(:post, api_url).once
         end
 
-        it "should create a build message" do
+        it "creates a build message" do
           message = hipchat.send(:create_build_message, data)
 
           project_url = project.web_url
@@ -325,13 +325,13 @@
           build.success
         end
 
-        it "should call Hipchat API" do
+        it "calls Hipchat API" do
           hipchat.notify_only_broken_builds = false
           hipchat.execute(data)
           expect(WebMock).to have_requested(:post, api_url).once
         end
 
-        it "should notify only broken" do
+        it "notifies only broken" do
           hipchat.notify_only_broken_builds = true
           hipchat.execute(data)
           expect(WebMock).not_to have_requested(:post, api_url).once
diff --git a/spec/models/project_services/irker_service_spec.rb b/spec/models/project_services/irker_service_spec.rb
index 4ee022a51710ec..b528baaf15c46f 100644
--- a/spec/models/project_services/irker_service_spec.rb
+++ b/spec/models/project_services/irker_service_spec.rb
@@ -71,7 +71,7 @@
       @irker_server.close
     end
 
-    it 'should send valid JSON messages to an Irker listener' do
+    it 'sends valid JSON messages to an Irker listener' do
       irker.execute(sample_data)
 
       conn = @irker_server.accept
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index 5a97cf370dae20..342403f63544b7 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -75,7 +75,7 @@
       WebMock.stub_request(:post, @comment_url)
     end
 
-    it "should call JIRA API" do
+    it "calls JIRA API" do
       @jira_service.execute(merge_request,
                             ExternalIssue.new("JIRA-123", project))
       expect(WebMock).to have_requested(:post, @comment_url).with(
@@ -128,7 +128,7 @@
         expect(@jira_service.api_url).to eq("http://jira_edited.example.com/rest/api/2")
       end
 
-      it "should reset password if url changed, even if setter called multiple times" do
+      it "resets password if url changed, even if setter called multiple times" do
         @jira_service.api_url = 'http://jira1.example.com/rest/api/2'
         @jira_service.api_url = 'http://jira1.example.com/rest/api/2'
         @jira_service.save
@@ -181,7 +181,7 @@
         @service.destroy!
       end
 
-      it 'should be initialized' do
+      it 'is initialized' do
         expect(@service.title).to eq('JIRA')
         expect(@service.description).to eq("Jira issue tracker")
       end
@@ -197,7 +197,7 @@
         @service.destroy!
       end
 
-      it "should be correct" do
+      it "is correct" do
         expect(@service.title).to eq('Jira One')
         expect(@service.description).to eq('Jira One issue tracker')
       end
@@ -225,7 +225,7 @@
         @service.destroy!
       end
 
-      it 'should be prepopulated with the settings' do
+      it 'is prepopulated with the settings' do
         expect(@service.properties["project_url"]).to eq('http://jira.sample/projects/project_a')
         expect(@service.properties["issues_url"]).to eq("http://jira.sample/issues/:id")
         expect(@service.properties["new_issue_url"]).to eq("http://jira.sample/projects/project_a/issues/new")
diff --git a/spec/models/project_services/pushover_service_spec.rb b/spec/models/project_services/pushover_service_spec.rb
index 555d9757b47a7b..19c0270a4938d6 100644
--- a/spec/models/project_services/pushover_service_spec.rb
+++ b/spec/models/project_services/pushover_service_spec.rb
@@ -72,7 +72,7 @@
       WebMock.stub_request(:post, api_url)
     end
 
-    it 'should call Pushover API' do
+    it 'calls Pushover API' do
       pushover.execute(sample_data)
 
       expect(WebMock).to have_requested(:post, api_url).once
diff --git a/spec/models/project_services/slack_service/note_message_spec.rb b/spec/models/project_services/slack_service/note_message_spec.rb
index 379c3e1219c3b9..41b93f08050833 100644
--- a/spec/models/project_services/slack_service/note_message_spec.rb
+++ b/spec/models/project_services/slack_service/note_message_spec.rb
@@ -60,6 +60,7 @@
           title: "merge request title\ndetails\n"
       }
     end
+
     it 'returns a message regarding notes on a merge request' do
       message = SlackService::NoteMessage.new(@args)
       expect(message.pretext).to eq("Test User commented on " \
diff --git a/spec/models/project_services/slack_service/wiki_page_message_spec.rb b/spec/models/project_services/slack_service/wiki_page_message_spec.rb
index 46dedb66c7c112..13aea0b0600675 100644
--- a/spec/models/project_services/slack_service/wiki_page_message_spec.rb
+++ b/spec/models/project_services/slack_service/wiki_page_message_spec.rb
@@ -47,7 +47,7 @@
     context 'when :action == "create"' do
       before { args[:object_attributes][:action] = 'create' }
 
-      it 'it returns the attachment for a new wiki page' do
+      it 'returns the attachment for a new wiki page' do
         expect(subject.attachments).to eq([
           {
             text: "Wiki page description",
@@ -60,7 +60,7 @@
     context 'when :action == "update"' do
       before { args[:object_attributes][:action] = 'update' }
 
-      it 'it returns the attachment for an updated wiki page' do
+      it 'returns the attachment for an updated wiki page' do
         expect(subject.attachments).to eq([
           {
             text: "Wiki page description",
diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/models/project_services/slack_service_spec.rb
index df511b1bc4c36e..45a5f4ef12ada0 100644
--- a/spec/models/project_services/slack_service_spec.rb
+++ b/spec/models/project_services/slack_service_spec.rb
@@ -93,31 +93,31 @@
       @wiki_page_sample_data = wiki_page_service.hook_data(@wiki_page, 'create')
     end
 
-    it "should call Slack API for push events" do
+    it "calls Slack API for push events" do
       slack.execute(push_sample_data)
 
       expect(WebMock).to have_requested(:post, webhook_url).once
     end
 
-    it "should call Slack API for issue events" do
+    it "calls Slack API for issue events" do
       slack.execute(@issues_sample_data)
 
       expect(WebMock).to have_requested(:post, webhook_url).once
     end
 
-    it "should call Slack API for merge requests events" do
+    it "calls Slack API for merge requests events" do
       slack.execute(@merge_sample_data)
 
       expect(WebMock).to have_requested(:post, webhook_url).once
     end
 
-    it "should call Slack API for wiki page events" do
+    it "calls Slack API for wiki page events" do
       slack.execute(@wiki_page_sample_data)
 
       expect(WebMock).to have_requested(:post, webhook_url).once
     end
 
-    it 'should use the username as an option for slack when configured' do
+    it 'uses the username as an option for slack when configured' do
       allow(slack).to receive(:username).and_return(username)
       expect(Slack::Notifier).to receive(:new).
        with(webhook_url, username: username).
@@ -128,7 +128,7 @@
       slack.execute(push_sample_data)
     end
 
-    it 'should use the channel as an option when it is configured' do
+    it 'uses the channel as an option when it is configured' do
       allow(slack).to receive(:channel).and_return(channel)
       expect(Slack::Notifier).to receive(:new).
         with(webhook_url, channel: channel).
@@ -234,7 +234,7 @@
                                 note: 'a comment on a commit')
       end
 
-      it "should call Slack API for commit comment events" do
+      it "calls Slack API for commit comment events" do
         data = Gitlab::NoteDataBuilder.build(commit_note, user)
         slack.execute(data)
 
@@ -248,7 +248,7 @@
                                        note: "merge request note")
       end
 
-      it "should call Slack API for merge request comment events" do
+      it "calls Slack API for merge request comment events" do
         data = Gitlab::NoteDataBuilder.build(merge_request_note, user)
         slack.execute(data)
 
@@ -261,7 +261,7 @@
         create(:note_on_issue, project: project, note: "issue note")
       end
 
-      it "should call Slack API for issue comment events" do
+      it "calls Slack API for issue comment events" do
         data = Gitlab::NoteDataBuilder.build(issue_note, user)
         slack.execute(data)
 
@@ -275,7 +275,7 @@
                                          note: "snippet note")
       end
 
-      it "should call Slack API for snippet comment events" do
+      it "calls Slack API for snippet comment events" do
         data = Gitlab::NoteDataBuilder.build(snippet_note, user)
         slack.execute(data)
 
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 567f87b9970c64..1c3d694075af2e 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -89,7 +89,7 @@
     it { is_expected.to validate_presence_of(:namespace) }
     it { is_expected.to validate_presence_of(:repository_storage) }
 
-    it 'should not allow new projects beyond user limits' do
+    it 'does not allow new projects beyond user limits' do
       project2 = build(:project)
       allow(project2).to receive(:creator).and_return(double(can_create_project?: false, projects_limit: 0).as_null_object)
       expect(project2).not_to be_valid
@@ -98,7 +98,7 @@
 
     describe 'wiki path conflict' do
       context "when the new path has been used by the wiki of other Project" do
-        it 'should have an error on the name attribute' do
+        it 'has an error on the name attribute' do
           new_project = build_stubbed(:project, namespace_id: project.namespace_id, path: "#{project.path}.wiki")
 
           expect(new_project).not_to be_valid
@@ -107,7 +107,7 @@
       end
 
       context "when the new wiki path has been used by the path of other Project" do
-        it 'should have an error on the name attribute' do
+        it 'has an error on the name attribute' do
           project_with_wiki_suffix = create(:project, path: 'foo.wiki')
           new_project = build_stubbed(:project, namespace_id: project_with_wiki_suffix.namespace_id, path: 'foo')
 
@@ -125,7 +125,7 @@
         allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
       end
 
-      it "should not allow repository storages that don't match a label in the configuration" do
+      it "does not allow repository storages that don't match a label in the configuration" do
         expect(project2).not_to be_valid
         expect(project2.errors[:repository_storage].first).to match(/is not included in the list/)
       end
@@ -172,12 +172,12 @@
   end
 
   describe 'project token' do
-    it 'should set an random token if none provided' do
+    it 'sets an random token if none provided' do
       project = FactoryGirl.create :empty_project, runners_token: ''
       expect(project.runners_token).not_to eq('')
     end
 
-    it 'should not set an random toke if one provided' do
+    it 'does not set an random toke if one provided' do
       project = FactoryGirl.create :empty_project, runners_token: 'my-token'
       expect(project.runners_token).to eq('my-token')
     end
@@ -225,7 +225,7 @@
     end
   end
 
-  it 'should return valid url to repo' do
+  it 'returns valid url to repo' do
     project = Project.new(path: 'somewhere')
     expect(project.url_to_repo).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + 'somewhere.git')
   end
@@ -279,7 +279,7 @@
     let(:last_event) { double(created_at: Time.now) }
 
     describe 'last_activity' do
-      it 'should alias last_activity to last_event' do
+      it 'alias last_activity to last_event' do
         allow(project).to receive(:last_event).and_return(last_event)
         expect(project.last_activity).to eq(last_event)
       end
@@ -350,13 +350,13 @@
     let(:prev_commit_id) { merge_request.commits.last.id }
     let(:commit_id) { merge_request.commits.first.id }
 
-    it 'should close merge request if last commit from source branch was pushed to target branch' do
+    it 'closes merge request if last commit from source branch was pushed to target branch' do
       project.update_merge_requests(prev_commit_id, commit_id, "refs/heads/#{merge_request.target_branch}", key.user)
       merge_request.reload
       expect(merge_request.merged?).to be_truthy
     end
 
-    it 'should update merge request commits with new one if pushed to source branch' do
+    it 'updates merge request commits with new one if pushed to source branch' do
       project.update_merge_requests(prev_commit_id, commit_id, "refs/heads/#{merge_request.source_branch}", key.user)
       merge_request.reload
       expect(merge_request.diff_head_sha).to eq(commit_id)
@@ -433,11 +433,11 @@
     let(:project) { create(:project) }
     let(:ext_project) { create(:redmine_project) }
 
-    it "should be true if used internal tracker" do
+    it "is true if used internal tracker" do
       expect(project.default_issues_tracker?).to be_truthy
     end
 
-    it "should be false if used other tracker" do
+    it "is false if used other tracker" do
       expect(ext_project.default_issues_tracker?).to be_falsey
     end
   end
@@ -636,12 +636,12 @@
   describe '#avatar_type' do
     let(:project) { create(:project) }
 
-    it 'should be true if avatar is image' do
+    it 'is true if avatar is image' do
       project.update_attribute(:avatar, 'uploads/avatar.png')
       expect(project.avatar_type).to be_truthy
     end
 
-    it 'should be false if avatar is html page' do
+    it 'is false if avatar is html page' do
       project.update_attribute(:avatar, 'uploads/avatar.html')
       expect(project.avatar_type).to eq(['only images allowed'])
     end
@@ -814,16 +814,16 @@
     context 'for shared runners disabled' do
       let(:shared_runners_enabled) { false }
 
-      it 'there are no runners available' do
+      it 'has no runners available' do
         expect(project.any_runners?).to be_falsey
       end
 
-      it 'there is a specific runner' do
+      it 'has a specific runner' do
         project.runners << specific_runner
         expect(project.any_runners?).to be_truthy
       end
 
-      it 'there is a shared runner, but they are prohibited to use' do
+      it 'has a shared runner, but they are prohibited to use' do
         shared_runner
         expect(project.any_runners?).to be_falsey
       end
@@ -837,7 +837,7 @@
     context 'for shared runners enabled' do
       let(:shared_runners_enabled) { true }
 
-      it 'there is a shared runner' do
+      it 'has a shared runner' do
         shared_runner
         expect(project.any_runners?).to be_truthy
       end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 2a053b1804f0f7..f7dbfd712cc452 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -340,14 +340,14 @@
 
   describe '#add_branch' do
     context 'when pre hooks were successful' do
-      it 'should run without errors' do
+      it 'runs without errors' do
         hook = double(trigger: [true, nil])
         expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook)
 
         expect { repository.add_branch(user, 'new_feature', 'master') }.not_to raise_error
       end
 
-      it 'should create the branch' do
+      it 'creates the branch' do
         allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil])
 
         branch = repository.add_branch(user, 'new_feature', 'master')
@@ -363,7 +363,7 @@
     end
 
     context 'when pre hooks failed' do
-      it 'should get an error' do
+      it 'gets an error' do
         allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
 
         expect do
@@ -371,7 +371,7 @@
         end.to raise_error(GitHooksService::PreReceiveError)
       end
 
-      it 'should not create the branch' do
+      it 'does not create the branch' do
         allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
 
         expect do
@@ -387,14 +387,14 @@
     let(:blank_sha) { '0000000000000000000000000000000000000000' }
 
     context 'when pre hooks were successful' do
-      it 'should run without errors' do
+      it 'runs without errors' do
         expect_any_instance_of(GitHooksService).to receive(:execute).
           with(user, project.repository.path_to_repo, old_rev, blank_sha, 'refs/heads/feature')
 
         expect { repository.rm_branch(user, 'feature') }.not_to raise_error
       end
 
-      it 'should delete the branch' do
+      it 'deletes the branch' do
         allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil])
 
         expect { repository.rm_branch(user, 'feature') }.not_to raise_error
@@ -404,7 +404,7 @@
     end
 
     context 'when pre hooks failed' do
-      it 'should get an error' do
+      it 'gets an error' do
         allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
 
         expect do
@@ -412,7 +412,7 @@
         end.to raise_error(GitHooksService::PreReceiveError)
       end
 
-      it 'should not delete the branch' do
+      it 'does not delete the branch' do
         allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
 
         expect do
@@ -433,13 +433,13 @@
           and_yield.and_return(true)
       end
 
-      it 'should run without errors' do
+      it 'runs without errors' do
         expect do
           repository.commit_with_hooks(user, 'feature') { sample_commit.id }
         end.not_to raise_error
       end
 
-      it 'should ensure the autocrlf Git option is set to :input' do
+      it 'ensures the autocrlf Git option is set to :input' do
         expect(repository).to receive(:update_autocrlf_option)
 
         repository.commit_with_hooks(user, 'feature') { sample_commit.id }
@@ -455,7 +455,7 @@
     end
 
     context 'when pre hooks failed' do
-      it 'should get an error' do
+      it 'gets an error' do
         allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
 
         expect do
@@ -715,7 +715,7 @@
   end
 
   describe '#merge' do
-    it 'should merge the code and return the commit id' do
+    it 'merges the code and return the commit id' do
       expect(merge_commit).to be_present
       expect(repository.blob_at(merge_commit.id, 'files/ruby/feature.rb')).to be_present
     end
@@ -726,13 +726,13 @@
     let(:update_image_commit) { repository.commit('2f63565e7aac07bcdadb654e253078b727143ec4') }
 
     context 'when there is a conflict' do
-      it 'should abort the operation' do
+      it 'aborts the operation' do
         expect(repository.revert(user, new_image_commit, 'master')).to eq(false)
       end
     end
 
     context 'when commit was already reverted' do
-      it 'should abort the operation' do
+      it 'aborts the operation' do
         repository.revert(user, update_image_commit, 'master')
 
         expect(repository.revert(user, update_image_commit, 'master')).to eq(false)
@@ -740,13 +740,13 @@
     end
 
     context 'when commit can be reverted' do
-      it 'should revert the changes' do
+      it 'reverts the changes' do
         expect(repository.revert(user, update_image_commit, 'master')).to be_truthy
       end
     end
 
     context 'reverting a merge commit' do
-      it 'should revert the changes' do
+      it 'reverts the changes' do
         merge_commit
         expect(repository.blob_at_branch('master', 'files/ruby/feature.rb')).to be_present
 
@@ -762,13 +762,13 @@
     let(:pickable_merge) { repository.commit('e56497bb5f03a90a51293fc6d516788730953899') }
 
     context 'when there is a conflict' do
-      it 'should abort the operation' do
+      it 'aborts the operation' do
         expect(repository.cherry_pick(user, conflict_commit, 'master')).to eq(false)
       end
     end
 
     context 'when commit was already cherry-picked' do
-      it 'should abort the operation' do
+      it 'aborts the operation' do
         repository.cherry_pick(user, pickable_commit, 'master')
 
         expect(repository.cherry_pick(user, pickable_commit, 'master')).to eq(false)
@@ -776,13 +776,13 @@
     end
 
     context 'when commit can be cherry-picked' do
-      it 'should cherry-pick the changes' do
+      it 'cherry-picks the changes' do
         expect(repository.cherry_pick(user, pickable_commit, 'master')).to be_truthy
       end
     end
 
     context 'cherry-picking a merge commit' do
-      it 'should cherry-pick the changes' do
+      it 'cherry-picks the changes' do
         expect(repository.blob_at_branch('master', 'foo/bar/.gitkeep')).to be_nil
 
         repository.cherry_pick(user, pickable_merge, 'master')
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index 67b3783d514583..05056a4bb4759d 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -65,13 +65,13 @@
       end
       let(:project) { create(:project) }
 
-      describe 'should be prefilled for projects pushover service' do
+      describe 'is prefilled for projects pushover service' do
         before do
           service_template
           project.build_missing_services
         end
 
-        it "should have all fields prefilled" do
+        it "has all fields prefilled" do
           service = project.pushover_service
           expect(service.template).to eq(false)
           expect(service.device).to eq('MyDevice')
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 9f432501c59ede..f67acbbef375b9 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -166,7 +166,7 @@
             allow_any_instance_of(ApplicationSetting).to receive(:domain_whitelist).and_return(['*.example.com'])
           end
 
-          it 'should give priority to whitelist and allow info@test.example.com' do
+          it 'gives priority to whitelist and allow info@test.example.com' do
             user = build(:user, email: 'info@test.example.com')
             expect(user).to be_valid
           end
@@ -304,18 +304,18 @@
   end
 
   describe '#generate_password' do
-    it "should execute callback when force_random_password specified" do
+    it "executes callback when force_random_password specified" do
       user = build(:user, force_random_password: true)
       expect(user).to receive(:generate_password)
       user.save
     end
 
-    it "should not generate password by default" do
+    it "does not generate password by default" do
       user = create(:user, password: 'abcdefghe')
       expect(user.password).to eq('abcdefghe')
     end
 
-    it "should generate password when forcing random password" do
+    it "generates password when forcing random password" do
       allow(Devise).to receive(:friendly_token).and_return('123456789')
       user = create(:user, password: 'abcdefg', force_random_password: true)
       expect(user.password).to eq('12345678')
@@ -323,7 +323,7 @@
   end
 
   describe 'authentication token' do
-    it "should have authentication token" do
+    it "has authentication token" do
       user = create(:user)
       expect(user.authentication_token).not_to be_blank
     end
@@ -430,7 +430,7 @@
   describe 'blocking user' do
     let(:user) { create(:user, name: 'John Smith') }
 
-    it "should block user" do
+    it "blocks user" do
       user.block
       expect(user.blocked?).to be_truthy
     end
@@ -501,7 +501,7 @@
     describe 'with defaults' do
       let(:user) { User.new }
 
-      it "should apply defaults to user" do
+      it "applies defaults to user" do
         expect(user.projects_limit).to eq(Gitlab.config.gitlab.default_projects_limit)
         expect(user.can_create_group).to eq(Gitlab.config.gitlab.default_can_create_group)
         expect(user.theme_id).to eq(Gitlab.config.gitlab.default_theme)
@@ -512,7 +512,7 @@
     describe 'with default overrides' do
       let(:user) { User.new(projects_limit: 123, can_create_group: false, can_create_team: true, theme_id: 1) }
 
-      it "should apply defaults to user" do
+      it "applies defaults to user" do
         expect(user.projects_limit).to eq(123)
         expect(user.can_create_group).to be_falsey
         expect(user.theme_id).to eq(1)
@@ -602,7 +602,7 @@
   describe 'by_username_or_id' do
     let(:user1) { create(:user, username: 'foo') }
 
-    it "should get the correct user" do
+    it "gets the correct user" do
       expect(User.by_username_or_id(user1.id)).to eq(user1)
       expect(User.by_username_or_id('foo')).to eq(user1)
       expect(User.by_username_or_id(-1)).to be_nil
@@ -614,7 +614,7 @@
     let(:username) { 'John' }
     let!(:user) { create(:user, username: username) }
 
-    it 'should get the correct user' do
+    it 'gets the correct user' do
       expect(User.by_login(user.email.upcase)).to eq user
       expect(User.by_login(user.email)).to eq user
       expect(User.by_login(username.downcase)).to eq user
@@ -639,7 +639,7 @@
   describe 'all_ssh_keys' do
     it { is_expected.to have_many(:keys).dependent(:destroy) }
 
-    it "should have all ssh keys" do
+    it "has all ssh keys" do
       user = create :user
       key = create :key, key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD33bWLBxu48Sev9Fert1yzEO4WGcWglWF7K/AwblIUFselOt/QdOL9DSjpQGxLagO1s9wl53STIO8qGS4Ms0EJZyIXOEFMjFJ5xmjSy+S37By4sG7SsltQEHMxtbtFOaW5LV2wCrX+rUsRNqLMamZjgjcPO0/EgGCXIGMAYW4O7cwGZdXWYIhQ1Vwy+CsVMDdPkPgBXqK7nR/ey8KMs8ho5fMNgB5hBw/AL9fNGhRw3QTD6Q12Nkhl4VZES2EsZqlpNnJttnPdp847DUsT6yuLRlfiQfz5Cn9ysHFdXObMN5VYIiPFwHeYCZp1X2S4fDZooRE8uOLTfxWHPXwrhqSH", user_id: user.id
 
@@ -650,12 +650,12 @@
   describe '#avatar_type' do
     let(:user) { create(:user) }
 
-    it "should be true if avatar is image" do
+    it "is true if avatar is image" do
       user.update_attribute(:avatar, 'uploads/avatar.png')
       expect(user.avatar_type).to be_truthy
     end
 
-    it "should be false if avatar is html page" do
+    it "is false if avatar is html page" do
       user.update_attribute(:avatar, 'uploads/avatar.html')
       expect(user.avatar_type).to eq(["only images allowed"])
     end
diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb
index ddc49495eda485..5c34b1b0a30796 100644
--- a/spec/models/wiki_page_spec.rb
+++ b/spec/models/wiki_page_spec.rb
@@ -147,12 +147,12 @@
       @page = wiki.find_page("Delete Page")
     end
 
-    it "should delete the page" do
+    it "deletes the page" do
       @page.delete
       expect(wiki.pages).to be_empty
     end
 
-    it "should return true" do
+    it "returns true" do
       expect(@page.delete).to eq(true)
     end
   end
@@ -183,7 +183,7 @@
       destroy_page("Title")
     end
 
-    it "should be replace a hyphen to a space" do
+    it "replaces a hyphen to a space" do
       @page.title = "Import-existing-repositories-into-GitLab"
       expect(@page.title).to eq("Import existing repositories into GitLab")
     end
diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/api_helpers_spec.rb
index 831889afb6c894..c65510fadecf0d 100644
--- a/spec/requests/api/api_helpers_spec.rb
+++ b/spec/requests/api/api_helpers_spec.rb
@@ -41,19 +41,19 @@ def error!(message, status)
 
   describe ".current_user" do
     describe "when authenticating using a user's private token" do
-      it "should return nil for an invalid token" do
+      it "returns nil for an invalid token" do
         env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token'
         allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
         expect(current_user).to be_nil
       end
 
-      it "should return nil for a user without access" do
+      it "returns nil for a user without access" do
         env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token
         allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
         expect(current_user).to be_nil
       end
 
-      it "should leave user as is when sudo not specified" do
+      it "leaves user as is when sudo not specified" do
         env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token
         expect(current_user).to eq(user)
         clear_env
@@ -65,19 +65,19 @@ def error!(message, status)
     describe "when authenticating using a user's personal access tokens" do
       let(:personal_access_token) { create(:personal_access_token, user: user) }
 
-      it "should return nil for an invalid token" do
+      it "returns nil for an invalid token" do
         env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token'
         allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
         expect(current_user).to be_nil
       end
 
-      it "should return nil for a user without access" do
+      it "returns nil for a user without access" do
         env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token
         allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
         expect(current_user).to be_nil
       end
 
-      it "should leave user as is when sudo not specified" do
+      it "leaves user as is when sudo not specified" do
         env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token
         expect(current_user).to eq(user)
         clear_env
@@ -100,7 +100,7 @@ def error!(message, status)
       end
     end
 
-    it "should change current user to sudo when admin" do
+    it "changes current user to sudo when admin" do
       set_env(admin, user.id)
       expect(current_user).to eq(user)
       set_param(admin, user.id)
@@ -111,7 +111,7 @@ def error!(message, status)
       expect(current_user).to eq(user)
     end
 
-    it "should throw an error when the current user is not an admin and attempting to sudo" do
+    it "throws an error when the current user is not an admin and attempting to sudo" do
       set_env(user, admin.id)
       expect { current_user }.to raise_error(Exception)
       set_param(user, admin.id)
@@ -122,7 +122,7 @@ def error!(message, status)
       expect { current_user }.to raise_error(Exception)
     end
 
-    it "should throw an error when the user cannot be found for a given id" do
+    it "throws an error when the user cannot be found for a given id" do
       id = user.id + admin.id
       expect(user.id).not_to eq(id)
       expect(admin.id).not_to eq(id)
@@ -133,7 +133,7 @@ def error!(message, status)
       expect { current_user }.to raise_error(Exception)
     end
 
-    it "should throw an error when the user cannot be found for a given username" do
+    it "throws an error when the user cannot be found for a given username" do
       username = "#{user.username}#{admin.username}"
       expect(user.username).not_to eq(username)
       expect(admin.username).not_to eq(username)
@@ -144,7 +144,7 @@ def error!(message, status)
       expect { current_user }.to raise_error(Exception)
     end
 
-    it "should handle sudo's to oneself" do
+    it "handles sudo's to oneself" do
       set_env(admin, admin.id)
       expect(current_user).to eq(admin)
       set_param(admin, admin.id)
@@ -155,7 +155,7 @@ def error!(message, status)
       expect(current_user).to eq(admin)
     end
 
-    it "should handle multiple sudo's to oneself" do
+    it "handles multiple sudo's to oneself" do
       set_env(admin, user.id)
       expect(current_user).to eq(user)
       expect(current_user).to eq(user)
@@ -171,7 +171,7 @@ def error!(message, status)
       expect(current_user).to eq(user)
     end
 
-    it "should handle multiple sudo's to oneself using string ids" do
+    it "handles multiple sudo's to oneself using string ids" do
       set_env(admin, user.id.to_s)
       expect(current_user).to eq(user)
       expect(current_user).to eq(user)
@@ -183,7 +183,7 @@ def error!(message, status)
   end
 
   describe '.sudo_identifier' do
-    it "should return integers when input is an int" do
+    it "returns integers when input is an int" do
       set_env(admin, '123')
       expect(sudo_identifier).to eq(123)
       set_env(admin, '0001234567890')
@@ -195,7 +195,7 @@ def error!(message, status)
       expect(sudo_identifier).to eq(1234567890)
     end
 
-    it "should return string when input is an is not an int" do
+    it "returns string when input is an is not an int" do
       set_env(admin, '12.30')
       expect(sudo_identifier).to eq("12.30")
       set_env(admin, 'hello')
diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb
index 2b74dd4bbb0c9c..73c268c0d1ef48 100644
--- a/spec/requests/api/award_emoji_spec.rb
+++ b/spec/requests/api/award_emoji_spec.rb
@@ -22,7 +22,7 @@
         expect(json_response.first['name']).to eq(award_emoji.name)
       end
 
-      it "should return a 404 error when issue id not found" do
+      it "returns a 404 error when issue id not found" do
         get api("/projects/#{project.id}/issues/12345/award_emoji", user)
 
         expect(response).to have_http_status(404)
@@ -124,13 +124,13 @@
         expect(json_response['user']['username']).to eq(user.username)
       end
 
-      it "should return a 400 bad request error if the name is not given" do
+      it "returns a 400 bad request error if the name is not given" do
         post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user)
 
         expect(response).to have_http_status(400)
       end
 
-      it "should return a 401 unauthorized error if the user is not authenticated" do
+      it "returns a 401 unauthorized error if the user is not authenticated" do
         post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji"), name: 'thumbsup'
 
         expect(response).to have_http_status(401)
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index e8fd697965f3c9..9444138f93d7dc 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -13,7 +13,7 @@
   let!(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' }
 
   describe "GET /projects/:id/repository/branches" do
-    it "should return an array of project branches" do
+    it "returns an array of project branches" do
       project.repository.expire_cache
 
       get api("/projects/#{project.id}/repository/branches", user)
@@ -25,7 +25,7 @@
   end
 
   describe "GET /projects/:id/repository/branches/:branch" do
-    it "should return the branch information for a single branch" do
+    it "returns the branch information for a single branch" do
       get api("/projects/#{project.id}/repository/branches/#{branch_name}", user)
       expect(response).to have_http_status(200)
 
@@ -36,12 +36,12 @@
       expect(json_response['developers_can_merge']).to eq(false)
     end
 
-    it "should return a 403 error if guest" do
+    it "returns a 403 error if guest" do
       get api("/projects/#{project.id}/repository/branches", user2)
       expect(response).to have_http_status(403)
     end
 
-    it "should return a 404 error if branch is not available" do
+    it "returns a 404 error if branch is not available" do
       get api("/projects/#{project.id}/repository/branches/unknown", user)
       expect(response).to have_http_status(404)
     end
@@ -138,17 +138,17 @@
       end
     end
 
-    it "should return a 404 error if branch not found" do
+    it "returns a 404 error if branch not found" do
       put api("/projects/#{project.id}/repository/branches/unknown/protect", user)
       expect(response).to have_http_status(404)
     end
 
-    it "should return a 403 error if guest" do
+    it "returns a 403 error if guest" do
       put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user2)
       expect(response).to have_http_status(403)
     end
 
-    it "should return success when protect branch again" do
+    it "returns success when protect branch again" do
       put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user)
       put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user)
       expect(response).to have_http_status(200)
@@ -156,7 +156,7 @@
   end
 
   describe "PUT /projects/:id/repository/branches/:branch/unprotect" do
-    it "should unprotect a single branch" do
+    it "unprotects a single branch" do
       put api("/projects/#{project.id}/repository/branches/#{branch_name}/unprotect", user)
       expect(response).to have_http_status(200)
 
@@ -165,12 +165,12 @@
       expect(json_response['protected']).to eq(false)
     end
 
-    it "should return success when unprotect branch" do
+    it "returns success when unprotect branch" do
       put api("/projects/#{project.id}/repository/branches/unknown/unprotect", user)
       expect(response).to have_http_status(404)
     end
 
-    it "should return success when unprotect branch again" do
+    it "returns success when unprotect branch again" do
       put api("/projects/#{project.id}/repository/branches/#{branch_name}/unprotect", user)
       put api("/projects/#{project.id}/repository/branches/#{branch_name}/unprotect", user)
       expect(response).to have_http_status(200)
@@ -178,7 +178,7 @@
   end
 
   describe "POST /projects/:id/repository/branches" do
-    it "should create a new branch" do
+    it "creates a new branch" do
       post api("/projects/#{project.id}/repository/branches", user),
            branch_name: 'feature1',
            ref: branch_sha
@@ -189,14 +189,14 @@
       expect(json_response['commit']['id']).to eq(branch_sha)
     end
 
-    it "should deny for user without push access" do
+    it "denies for user without push access" do
       post api("/projects/#{project.id}/repository/branches", user2),
            branch_name: branch_name,
            ref: branch_sha
       expect(response).to have_http_status(403)
     end
 
-    it 'should return 400 if branch name is invalid' do
+    it 'returns 400 if branch name is invalid' do
       post api("/projects/#{project.id}/repository/branches", user),
            branch_name: 'new design',
            ref: branch_sha
@@ -204,7 +204,7 @@
       expect(json_response['message']).to eq('Branch name is invalid')
     end
 
-    it 'should return 400 if branch already exists' do
+    it 'returns 400 if branch already exists' do
       post api("/projects/#{project.id}/repository/branches", user),
            branch_name: 'new_design1',
            ref: branch_sha
@@ -217,7 +217,7 @@
       expect(json_response['message']).to eq('Branch already exists')
     end
 
-    it 'should return 400 if ref name is invalid' do
+    it 'returns 400 if ref name is invalid' do
       post api("/projects/#{project.id}/repository/branches", user),
            branch_name: 'new_design3',
            ref: 'foo'
@@ -231,25 +231,25 @@
       allow_any_instance_of(Repository).to receive(:rm_branch).and_return(true)
     end
 
-    it "should remove branch" do
+    it "removes branch" do
       delete api("/projects/#{project.id}/repository/branches/#{branch_name}", user)
       expect(response).to have_http_status(200)
       expect(json_response['branch_name']).to eq(branch_name)
     end
 
-    it 'should return 404 if branch not exists' do
+    it 'returns 404 if branch not exists' do
       delete api("/projects/#{project.id}/repository/branches/foobar", user)
       expect(response).to have_http_status(404)
     end
 
-    it "should remove protected branch" do
+    it "removes protected branch" do
       project.protected_branches.create(name: branch_name)
       delete api("/projects/#{project.id}/repository/branches/#{branch_name}", user)
       expect(response).to have_http_status(405)
       expect(json_response['message']).to eq('Protected branch cant be removed')
     end
 
-    it "should not remove HEAD branch" do
+    it "does not remove HEAD branch" do
       delete api("/projects/#{project.id}/repository/branches/master", user)
       expect(response).to have_http_status(405)
       expect(json_response['message']).to eq('Cannot remove HEAD branch')
diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb
index 86a7b242fbe2ec..966d302dfd3acf 100644
--- a/spec/requests/api/builds_spec.rb
+++ b/spec/requests/api/builds_spec.rb
@@ -18,7 +18,7 @@
     before { get api("/projects/#{project.id}/builds?#{query}", api_user) }
 
     context 'authorized user' do
-      it 'should return project builds' do
+      it 'returns project builds' do
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
       end
@@ -84,7 +84,7 @@
             get api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", api_user)
           end
 
-          it 'should return project builds for specific commit' do
+          it 'returns project builds for specific commit' do
             expect(response).to have_http_status(200)
             expect(json_response).to be_an Array
             expect(json_response.size).to eq 2
@@ -113,7 +113,7 @@
           get api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", nil)
         end
 
-        it 'should not return project builds' do
+        it 'does not return project builds' do
           expect(response).to have_http_status(401)
           expect(json_response.except('message')).to be_empty
         end
@@ -125,7 +125,7 @@
     before { get api("/projects/#{project.id}/builds/#{build.id}", api_user) }
 
     context 'authorized user' do
-      it 'should return specific build data' do
+      it 'returns specific build data' do
         expect(response).to have_http_status(200)
         expect(json_response['name']).to eq('test')
       end
@@ -134,7 +134,7 @@
     context 'unauthorized user' do
       let(:api_user) { nil }
 
-      it 'should not return specific build data' do
+      it 'does not return specific build data' do
         expect(response).to have_http_status(401)
       end
     end
@@ -152,7 +152,7 @@
             'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' }
         end
 
-        it 'should return specific build artifacts' do
+        it 'returns specific build artifacts' do
           expect(response).to have_http_status(200)
           expect(response.headers).to include(download_headers)
         end
@@ -161,13 +161,13 @@
       context 'unauthorized user' do
         let(:api_user) { nil }
 
-        it 'should not return specific build artifacts' do
+        it 'does not return specific build artifacts' do
           expect(response).to have_http_status(401)
         end
       end
     end
 
-    it 'should not return build artifacts if not uploaded' do
+    it 'does not return build artifacts if not uploaded' do
       expect(response).to have_http_status(404)
     end
   end
@@ -272,7 +272,7 @@ def path_for_ref(ref = pipeline.ref, job = build.name)
     end
 
     context 'authorized user' do
-      it 'should return specific build trace' do
+      it 'returns specific build trace' do
         expect(response).to have_http_status(200)
         expect(response.body).to eq(build.trace)
       end
@@ -281,7 +281,7 @@ def path_for_ref(ref = pipeline.ref, job = build.name)
     context 'unauthorized user' do
       let(:api_user) { nil }
 
-      it 'should not return specific build trace' do
+      it 'does not return specific build trace' do
         expect(response).to have_http_status(401)
       end
     end
@@ -292,7 +292,7 @@ def path_for_ref(ref = pipeline.ref, job = build.name)
 
     context 'authorized user' do
       context 'user with :update_build persmission' do
-        it 'should cancel running or pending build' do
+        it 'cancels running or pending build' do
           expect(response).to have_http_status(201)
           expect(project.builds.first.status).to eq('canceled')
         end
@@ -301,7 +301,7 @@ def path_for_ref(ref = pipeline.ref, job = build.name)
       context 'user without :update_build permission' do
         let(:api_user) { reporter.user }
 
-        it 'should not cancel build' do
+        it 'does not cancel build' do
           expect(response).to have_http_status(403)
         end
       end
@@ -310,7 +310,7 @@ def path_for_ref(ref = pipeline.ref, job = build.name)
     context 'unauthorized user' do
       let(:api_user) { nil }
 
-      it 'should not cancel build' do
+      it 'does not cancel build' do
         expect(response).to have_http_status(401)
       end
     end
@@ -323,7 +323,7 @@ def path_for_ref(ref = pipeline.ref, job = build.name)
 
     context 'authorized user' do
       context 'user with :update_build permission' do
-        it 'should retry non-running build' do
+        it 'retries non-running build' do
           expect(response).to have_http_status(201)
           expect(project.builds.first.status).to eq('canceled')
           expect(json_response['status']).to eq('pending')
@@ -333,7 +333,7 @@ def path_for_ref(ref = pipeline.ref, job = build.name)
       context 'user without :update_build permission' do
         let(:api_user) { reporter.user }
 
-        it 'should not retry build' do
+        it 'does not retry build' do
           expect(response).to have_http_status(403)
         end
       end
@@ -342,7 +342,7 @@ def path_for_ref(ref = pipeline.ref, job = build.name)
     context 'unauthorized user' do
       let(:api_user) { nil }
 
-      it 'should not retry build' do
+      it 'does not retry build' do
         expect(response).to have_http_status(401)
       end
     end
@@ -356,14 +356,14 @@ def path_for_ref(ref = pipeline.ref, job = build.name)
     context 'build is erasable' do
       let(:build) { create(:ci_build, :trace, :artifacts, :success, project: project, pipeline: pipeline) }
 
-      it 'should erase build content' do
+      it 'erases build content' do
         expect(response.status).to eq 201
         expect(build.trace).to be_empty
         expect(build.artifacts_file.exists?).to be_falsy
         expect(build.artifacts_metadata.exists?).to be_falsy
       end
 
-      it 'should update build' do
+      it 'updates build' do
         expect(build.reload.erased_at).to be_truthy
         expect(build.reload.erased_by).to eq user
       end
@@ -372,7 +372,7 @@ def path_for_ref(ref = pipeline.ref, job = build.name)
     context 'build is not erasable' do
       let(:build) { create(:ci_build, :trace, project: project, pipeline: pipeline) }
 
-      it 'should respond with forbidden' do
+      it 'responds with forbidden' do
         expect(response.status).to eq 403
       end
     end
diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb
index 2da01da7fa1201..2d6093fec7a467 100644
--- a/spec/requests/api/commit_statuses_spec.rb
+++ b/spec/requests/api/commit_statuses_spec.rb
@@ -99,7 +99,7 @@ def create_status(commit, opts = {})
     context "guest user" do
       before { get api(get_url, guest) }
 
-      it "should not return project commits" do
+      it "does not return project commits" do
         expect(response).to have_http_status(403)
       end
     end
@@ -107,7 +107,7 @@ def create_status(commit, opts = {})
     context "unauthorized user" do
       before { get api(get_url) }
 
-      it "should not return project commits" do
+      it "does not return project commits" do
         expect(response).to have_http_status(401)
       end
     end
@@ -179,7 +179,7 @@ def create_status(commit, opts = {})
     context 'reporter user' do
       before { post api(post_url, reporter) }
 
-      it 'should not create commit status' do
+      it 'does not create commit status' do
         expect(response).to have_http_status(403)
       end
     end
@@ -187,7 +187,7 @@ def create_status(commit, opts = {})
     context 'guest user' do
       before { post api(post_url, guest) }
 
-      it 'should not create commit status' do
+      it 'does not create commit status' do
         expect(response).to have_http_status(403)
       end
     end
@@ -195,7 +195,7 @@ def create_status(commit, opts = {})
     context 'unauthorized user' do
       before { post api(post_url) }
 
-      it 'should not create commit status' do
+      it 'does not create commit status' do
         expect(response).to have_http_status(401)
       end
     end
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 51ee2167d475ba..4379fcb3c1ee1c 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -17,7 +17,7 @@
     context "authorized user" do
       before { project.team << [user2, :reporter] }
 
-      it "should return project commits" do
+      it "returns project commits" do
         get api("/projects/#{project.id}/repository/commits", user)
         expect(response).to have_http_status(200)
 
@@ -27,14 +27,14 @@
     end
 
     context "unauthorized user" do
-      it "should not return project commits" do
+      it "does not return project commits" do
         get api("/projects/#{project.id}/repository/commits")
         expect(response).to have_http_status(401)
       end
     end
 
     context "since optional parameter" do
-      it "should return project commits since provided parameter" do
+      it "returns project commits since provided parameter" do
         commits = project.repository.commits("master")
         since = commits.second.created_at
 
@@ -47,7 +47,7 @@
     end
 
     context "until optional parameter" do
-      it "should return project commits until provided parameter" do
+      it "returns project commits until provided parameter" do
         commits = project.repository.commits("master")
         before = commits.second.created_at
 
@@ -60,7 +60,7 @@
     end
 
     context "invalid xmlschema date parameters" do
-      it "should return an invalid parameter error message" do
+      it "returns an invalid parameter error message" do
         get api("/projects/#{project.id}/repository/commits?since=invalid-date", user)
 
         expect(response).to have_http_status(400)
@@ -71,7 +71,7 @@
 
   describe "GET /projects:id/repository/commits/:sha" do
     context "authorized user" do
-      it "should return a commit by sha" do
+      it "returns a commit by sha" do
         get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
 
         expect(response).to have_http_status(200)
@@ -82,18 +82,18 @@
         expect(json_response['stats']['total']).to eq(project.repository.commit.stats.total)
       end
 
-      it "should return a 404 error if not found" do
+      it "returns a 404 error if not found" do
         get api("/projects/#{project.id}/repository/commits/invalid_sha", user)
         expect(response).to have_http_status(404)
       end
 
-      it "should return nil for commit without CI" do
+      it "returns nil for commit without CI" do
         get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
         expect(response).to have_http_status(200)
         expect(json_response['status']).to be_nil
       end
 
-      it "should return status for CI" do
+      it "returns status for CI" do
         pipeline = project.ensure_pipeline(project.repository.commit.sha, 'master')
         get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
         expect(response).to have_http_status(200)
@@ -102,7 +102,7 @@
     end
 
     context "unauthorized user" do
-      it "should not return the selected commit" do
+      it "does not return the selected commit" do
         get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}")
         expect(response).to have_http_status(401)
       end
@@ -113,7 +113,7 @@
     context "authorized user" do
       before { project.team << [user2, :reporter] }
 
-      it "should return the diff of the selected commit" do
+      it "returns the diff of the selected commit" do
         get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/diff", user)
         expect(response).to have_http_status(200)
 
@@ -122,14 +122,14 @@
         expect(json_response.first.keys).to include "diff"
       end
 
-      it "should return a 404 error if invalid commit" do
+      it "returns a 404 error if invalid commit" do
         get api("/projects/#{project.id}/repository/commits/invalid_sha/diff", user)
         expect(response).to have_http_status(404)
       end
     end
 
     context "unauthorized user" do
-      it "should not return the diff of the selected commit" do
+      it "does not return the diff of the selected commit" do
         get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/diff")
         expect(response).to have_http_status(401)
       end
@@ -138,7 +138,7 @@
 
   describe 'GET /projects:id/repository/commits/:sha/comments' do
     context 'authorized user' do
-      it 'should return merge_request comments' do
+      it 'returns merge_request comments' do
         get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -147,14 +147,14 @@
         expect(json_response.first['author']['id']).to eq(user.id)
       end
 
-      it 'should return a 404 error if merge_request_id not found' do
+      it 'returns a 404 error if merge_request_id not found' do
         get api("/projects/#{project.id}/repository/commits/1234ab/comments", user)
         expect(response).to have_http_status(404)
       end
     end
 
     context 'unauthorized user' do
-      it 'should not return the diff of the selected commit' do
+      it 'does not return the diff of the selected commit' do
         get api("/projects/#{project.id}/repository/commits/1234ab/comments")
         expect(response).to have_http_status(401)
       end
@@ -163,7 +163,7 @@
 
   describe 'POST /projects:id/repository/commits/:sha/comments' do
     context 'authorized user' do
-      it 'should return comment' do
+      it 'returns comment' do
         post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user), note: 'My comment'
         expect(response).to have_http_status(201)
         expect(json_response['note']).to eq('My comment')
@@ -172,7 +172,7 @@
         expect(json_response['line_type']).to be_nil
       end
 
-      it 'should return the inline comment' do
+      it 'returns the inline comment' do
         post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user), note: 'My comment', path: project.repository.commit.raw_diffs.first.new_path, line: 7, line_type: 'new'
         expect(response).to have_http_status(201)
         expect(json_response['note']).to eq('My comment')
@@ -181,19 +181,19 @@
         expect(json_response['line_type']).to eq('new')
       end
 
-      it 'should return 400 if note is missing' do
+      it 'returns 400 if note is missing' do
         post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user)
         expect(response).to have_http_status(400)
       end
 
-      it 'should return 404 if note is attached to non existent commit' do
+      it 'returns 404 if note is attached to non existent commit' do
         post api("/projects/#{project.id}/repository/commits/1234ab/comments", user), note: 'My comment'
         expect(response).to have_http_status(404)
       end
     end
 
     context 'unauthorized user' do
-      it 'should not return the diff of the selected commit' do
+      it 'does not return the diff of the selected commit' do
         post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments")
         expect(response).to have_http_status(401)
       end
diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb
index 2e5448143d5ad1..2d1213df8a7b42 100644
--- a/spec/requests/api/files_spec.rb
+++ b/spec/requests/api/files_spec.rb
@@ -9,7 +9,7 @@
   before { project.team << [user, :developer] }
 
   describe "GET /projects/:id/repository/files" do
-    it "should return file info" do
+    it "returns file info" do
       params = {
         file_path: file_path,
         ref: 'master',
@@ -23,12 +23,12 @@
       expect(Base64.decode64(json_response['content']).lines.first).to eq("require 'fileutils'\n")
     end
 
-    it "should return a 400 bad request if no params given" do
+    it "returns a 400 bad request if no params given" do
       get api("/projects/#{project.id}/repository/files", user)
       expect(response).to have_http_status(400)
     end
 
-    it "should return a 404 if such file does not exist" do
+    it "returns a 404 if such file does not exist" do
       params = {
         file_path: 'app/models/application.rb',
         ref: 'master',
@@ -49,18 +49,18 @@
       }
     end
 
-    it "should create a new file in project repo" do
+    it "creates a new file in project repo" do
       post api("/projects/#{project.id}/repository/files", user), valid_params
       expect(response).to have_http_status(201)
       expect(json_response['file_path']).to eq('newfile.rb')
     end
 
-    it "should return a 400 bad request if no params given" do
+    it "returns a 400 bad request if no params given" do
       post api("/projects/#{project.id}/repository/files", user)
       expect(response).to have_http_status(400)
     end
 
-    it "should return a 400 if editor fails to create file" do
+    it "returns a 400 if editor fails to create file" do
       allow_any_instance_of(Repository).to receive(:commit_file).
         and_return(false)
 
@@ -79,13 +79,13 @@
       }
     end
 
-    it "should update existing file in project repo" do
+    it "updates existing file in project repo" do
       put api("/projects/#{project.id}/repository/files", user), valid_params
       expect(response).to have_http_status(200)
       expect(json_response['file_path']).to eq(file_path)
     end
 
-    it "should return a 400 bad request if no params given" do
+    it "returns a 400 bad request if no params given" do
       put api("/projects/#{project.id}/repository/files", user)
       expect(response).to have_http_status(400)
     end
@@ -100,18 +100,18 @@
       }
     end
 
-    it "should delete existing file in project repo" do
+    it "deletes existing file in project repo" do
       delete api("/projects/#{project.id}/repository/files", user), valid_params
       expect(response).to have_http_status(200)
       expect(json_response['file_path']).to eq(file_path)
     end
 
-    it "should return a 400 bad request if no params given" do
+    it "returns a 400 bad request if no params given" do
       delete api("/projects/#{project.id}/repository/files", user)
       expect(response).to have_http_status(400)
     end
 
-    it "should return a 400 if fails to create file" do
+    it "returns a 400 if fails to create file" do
       allow_any_instance_of(Repository).to receive(:remove_file).and_return(false)
 
       delete api("/projects/#{project.id}/repository/files", user), valid_params
diff --git a/spec/requests/api/fork_spec.rb b/spec/requests/api/fork_spec.rb
index a9f5aa924b71a7..f802fcd2d2e590 100644
--- a/spec/requests/api/fork_spec.rb
+++ b/spec/requests/api/fork_spec.rb
@@ -20,7 +20,7 @@
     before { user3 }
 
     context 'when authenticated' do
-      it 'should fork if user has sufficient access to project' do
+      it 'forks if user has sufficient access to project' do
         post api("/projects/fork/#{project.id}", user2)
         expect(response).to have_http_status(201)
         expect(json_response['name']).to eq(project.name)
@@ -30,7 +30,7 @@
         expect(json_response['forked_from_project']['id']).to eq(project.id)
       end
 
-      it 'should fork if user is admin' do
+      it 'forks if user is admin' do
         post api("/projects/fork/#{project.id}", admin)
         expect(response).to have_http_status(201)
         expect(json_response['name']).to eq(project.name)
@@ -40,20 +40,20 @@
         expect(json_response['forked_from_project']['id']).to eq(project.id)
       end
 
-      it 'should fail on missing project access for the project to fork' do
+      it 'fails on missing project access for the project to fork' do
         post api("/projects/fork/#{project.id}", user3)
         expect(response).to have_http_status(404)
         expect(json_response['message']).to eq('404 Project Not Found')
       end
 
-      it 'should fail if forked project exists in the user namespace' do
+      it 'fails if forked project exists in the user namespace' do
         post api("/projects/fork/#{project.id}", user)
         expect(response).to have_http_status(409)
         expect(json_response['message']['name']).to eq(['has already been taken'])
         expect(json_response['message']['path']).to eq(['has already been taken'])
       end
 
-      it 'should fail if project to fork from does not exist' do
+      it 'fails if project to fork from does not exist' do
         post api('/projects/fork/424242', user)
         expect(response).to have_http_status(404)
         expect(json_response['message']).to eq('404 Project Not Found')
@@ -61,7 +61,7 @@
     end
 
     context 'when unauthenticated' do
-      it 'should return authentication error' do
+      it 'returns authentication error' do
         post api("/projects/fork/#{project.id}")
         expect(response).to have_http_status(401)
         expect(json_response['message']).to eq('401 Unauthorized')
diff --git a/spec/requests/api/group_members_spec.rb b/spec/requests/api/group_members_spec.rb
index 52f9e7d4681cc7..8bd6a8062ae774 100644
--- a/spec/requests/api/group_members_spec.rb
+++ b/spec/requests/api/group_members_spec.rb
@@ -28,7 +28,7 @@
 
   describe "GET /groups/:id/members" do
     context "when authenticated as user that is part or the group" do
-      it "each user: should return an array of members groups of group3" do
+      it "each user: returns an array of members groups of group3" do
         [owner, master, developer, reporter, guest].each do |user|
           get api("/groups/#{group_with_members.id}/members", user)
           expect(response).to have_http_status(200)
@@ -52,14 +52,14 @@
 
   describe "POST /groups/:id/members" do
     context "when not a member of the group" do
-      it "should not add guest as member of group_no_members when adding being done by person outside the group" do
+      it "does not add guest as member of group_no_members when adding being done by person outside the group" do
         post api("/groups/#{group_no_members.id}/members", reporter), user_id: guest.id, access_level: GroupMember::MASTER
         expect(response).to have_http_status(403)
       end
     end
 
     context "when a member of the group" do
-      it "should return ok and add new member" do
+      it "returns ok and add new member" do
         new_user = create(:user)
 
         expect do
@@ -71,7 +71,7 @@
         expect(json_response['access_level']).to eq(GroupMember::MASTER)
       end
 
-      it "should not allow guest to modify group members" do
+      it "does not allow guest to modify group members" do
         new_user = create(:user)
 
         expect do
@@ -81,22 +81,22 @@
         expect(response).to have_http_status(403)
       end
 
-      it "should return error if member already exists" do
+      it "returns error if member already exists" do
         post api("/groups/#{group_with_members.id}/members", owner), user_id: master.id, access_level: GroupMember::MASTER
         expect(response).to have_http_status(409)
       end
 
-      it "should return a 400 error when user id is not given" do
+      it "returns a 400 error when user id is not given" do
         post api("/groups/#{group_no_members.id}/members", owner), access_level: GroupMember::MASTER
         expect(response).to have_http_status(400)
       end
 
-      it "should return a 400 error when access level is not given" do
+      it "returns a 400 error when access level is not given" do
         post api("/groups/#{group_no_members.id}/members", owner), user_id: master.id
         expect(response).to have_http_status(400)
       end
 
-      it "should return a 422 error when access level is not known" do
+      it "returns a 422 error when access level is not known" do
         post api("/groups/#{group_no_members.id}/members", owner), user_id: master.id, access_level: 1234
         expect(response).to have_http_status(422)
       end
@@ -105,7 +105,7 @@
 
   describe 'PUT /groups/:id/members/:user_id' do
     context 'when not a member of the group' do
-      it 'should return a 409 error if the user is not a group member' do
+      it 'returns a 409 error if the user is not a group member' do
         put(
           api("/groups/#{group_no_members.id}/members/#{developer.id}",
               owner), access_level: GroupMember::MASTER
@@ -115,7 +115,7 @@
     end
 
     context 'when a member of the group' do
-      it 'should return ok and update member access level' do
+      it 'returns ok and update member access level' do
         put(
           api("/groups/#{group_with_members.id}/members/#{reporter.id}",
               owner),
@@ -132,7 +132,7 @@
         expect(json_reporter['access_level']).to eq(GroupMember::MASTER)
       end
 
-      it 'should not allow guest to modify group members' do
+      it 'does not allow guest to modify group members' do
         put(
           api("/groups/#{group_with_members.id}/members/#{developer.id}",
               guest),
@@ -149,14 +149,14 @@
         expect(json_developer['access_level']).to eq(GroupMember::DEVELOPER)
       end
 
-      it 'should return a 400 error when access level is not given' do
+      it 'returns a 400 error when access level is not given' do
         put(
           api("/groups/#{group_with_members.id}/members/#{master.id}", owner)
         )
         expect(response).to have_http_status(400)
       end
 
-      it 'should return a 422 error when access level is not known' do
+      it 'returns a 422 error when access level is not known' do
         put(
           api("/groups/#{group_with_members.id}/members/#{master.id}", owner),
           access_level: 1234
@@ -168,7 +168,7 @@
 
   describe 'DELETE /groups/:id/members/:user_id' do
     context 'when not a member of the group' do
-      it "should not delete guest's membership of group_with_members" do
+      it "does not delete guest's membership of group_with_members" do
         random_user = create(:user)
         delete api("/groups/#{group_with_members.id}/members/#{owner.id}", random_user)
 
@@ -177,7 +177,7 @@
     end
 
     context "when a member of the group" do
-      it "should delete guest's membership of group" do
+      it "deletes guest's membership of group" do
         expect do
           delete api("/groups/#{group_with_members.id}/members/#{guest.id}", owner)
         end.to change { group_with_members.members.count }.by(-1)
@@ -185,12 +185,12 @@
         expect(response).to have_http_status(200)
       end
 
-      it "should return a 404 error when user id is not known" do
+      it "returns a 404 error when user id is not known" do
         delete api("/groups/#{group_with_members.id}/members/1328", owner)
         expect(response).to have_http_status(404)
       end
 
-      it "should not allow guest to modify group members" do
+      it "does not allow guest to modify group members" do
         delete api("/groups/#{group_with_members.id}/members/#{master.id}", guest)
         expect(response).to have_http_status(403)
       end
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index c2c94040eceb55..4860b23c2ed7bf 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -21,14 +21,14 @@
 
   describe "GET /groups" do
     context "when unauthenticated" do
-      it "should return authentication error" do
+      it "returns authentication error" do
         get api("/groups")
         expect(response).to have_http_status(401)
       end
     end
 
     context "when authenticated as user" do
-      it "normal user: should return an array of groups of user1" do
+      it "normal user: returns an array of groups of user1" do
         get api("/groups", user1)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -38,7 +38,7 @@
     end
 
     context "when authenticated as  admin" do
-      it "admin: should return an array of all groups" do
+      it "admin: returns an array of all groups" do
         get api("/groups", admin)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -70,12 +70,12 @@
         expect(json_response['shared_projects'][0]['id']).to eq(project.id)
       end
 
-      it "should not return a non existing group" do
+      it "does not return a non existing group" do
         get api("/groups/1328", user1)
         expect(response).to have_http_status(404)
       end
 
-      it "should not return a group not attached to user1" do
+      it "does not return a group not attached to user1" do
         get api("/groups/#{group2.id}", user1)
 
         expect(response).to have_http_status(404)
@@ -83,31 +83,31 @@
     end
 
     context "when authenticated as admin" do
-      it "should return any existing group" do
+      it "returns any existing group" do
         get api("/groups/#{group2.id}", admin)
         expect(response).to have_http_status(200)
         expect(json_response['name']).to eq(group2.name)
       end
 
-      it "should not return a non existing group" do
+      it "does not return a non existing group" do
         get api("/groups/1328", admin)
         expect(response).to have_http_status(404)
       end
     end
 
     context 'when using group path in URL' do
-      it 'should return any existing group' do
+      it 'returns any existing group' do
         get api("/groups/#{group1.path}", admin)
         expect(response).to have_http_status(200)
         expect(json_response['name']).to eq(group1.name)
       end
 
-      it 'should not return a non existing group' do
+      it 'does not return a non existing group' do
         get api('/groups/unknown', admin)
         expect(response).to have_http_status(404)
       end
 
-      it 'should not return a group not attached to user1' do
+      it 'does not return a group not attached to user1' do
         get api("/groups/#{group2.path}", user1)
 
         expect(response).to have_http_status(404)
@@ -161,7 +161,7 @@
 
   describe "GET /groups/:id/projects" do
     context "when authenticated as user" do
-      it "should return the group's projects" do
+      it "returns the group's projects" do
         get api("/groups/#{group1.id}/projects", user1)
 
         expect(response).to have_http_status(200)
@@ -170,12 +170,12 @@
         expect(project_names).to match_array([project1.name, project3.name])
       end
 
-      it "should not return a non existing group" do
+      it "does not return a non existing group" do
         get api("/groups/1328/projects", user1)
         expect(response).to have_http_status(404)
       end
 
-      it "should not return a group not attached to user1" do
+      it "does not return a group not attached to user1" do
         get api("/groups/#{group2.id}/projects", user1)
 
         expect(response).to have_http_status(404)
@@ -215,12 +215,12 @@
         expect(project_names).to match_array([project1.name, project3.name])
       end
 
-      it 'should not return a non existing group' do
+      it 'does not return a non existing group' do
         get api('/groups/unknown/projects', admin)
         expect(response).to have_http_status(404)
       end
 
-      it 'should not return a group not attached to user1' do
+      it 'does not return a group not attached to user1' do
         get api("/groups/#{group2.path}/projects", user1)
 
         expect(response).to have_http_status(404)
@@ -230,30 +230,30 @@
 
   describe "POST /groups" do
     context "when authenticated as user without group permissions" do
-      it "should not create group" do
+      it "does not create group" do
         post api("/groups", user1), attributes_for(:group)
         expect(response).to have_http_status(403)
       end
     end
 
     context "when authenticated as user with group permissions" do
-      it "should create group" do
+      it "creates group" do
         post api("/groups", user3), attributes_for(:group)
         expect(response).to have_http_status(201)
       end
 
-      it "should not create group, duplicate" do
+      it "does not create group, duplicate" do
         post api("/groups", user3), { name: 'Duplicate Test', path: group2.path }
         expect(response).to have_http_status(400)
         expect(response.message).to eq("Bad Request")
       end
 
-      it "should return 400 bad request error if name not given" do
+      it "returns 400 bad request error if name not given" do
         post api("/groups", user3), { path: group2.path }
         expect(response).to have_http_status(400)
       end
 
-      it "should return 400 bad request error if path not given" do
+      it "returns 400 bad request error if path not given" do
         post api("/groups", user3), { name: 'test' }
         expect(response).to have_http_status(400)
       end
@@ -262,24 +262,24 @@
 
   describe "DELETE /groups/:id" do
     context "when authenticated as user" do
-      it "should remove group" do
+      it "removes group" do
         delete api("/groups/#{group1.id}", user1)
         expect(response).to have_http_status(200)
       end
 
-      it "should not remove a group if not an owner" do
+      it "does not remove a group if not an owner" do
         user4 = create(:user)
         group1.add_master(user4)
         delete api("/groups/#{group1.id}", user3)
         expect(response).to have_http_status(403)
       end
 
-      it "should not remove a non existing group" do
+      it "does not remove a non existing group" do
         delete api("/groups/1328", user1)
         expect(response).to have_http_status(404)
       end
 
-      it "should not remove a group not attached to user1" do
+      it "does not remove a group not attached to user1" do
         delete api("/groups/#{group2.id}", user1)
 
         expect(response).to have_http_status(404)
@@ -287,12 +287,12 @@
     end
 
     context "when authenticated as admin" do
-      it "should remove any existing group" do
+      it "removes any existing group" do
         delete api("/groups/#{group2.id}", admin)
         expect(response).to have_http_status(200)
       end
 
-      it "should not remove a non existing group" do
+      it "does not remove a non existing group" do
         delete api("/groups/1328", admin)
         expect(response).to have_http_status(404)
       end
@@ -308,14 +308,14 @@
     end
 
     context "when authenticated as user" do
-      it "should not transfer project to group" do
+      it "does not transfer project to group" do
         post api("/groups/#{group1.id}/projects/#{project.id}", user2)
         expect(response).to have_http_status(403)
       end
     end
 
     context "when authenticated as admin" do
-      it "should transfer project to group" do
+      it "transfers project to group" do
         post api("/groups/#{group1.id}/projects/#{project.id}", admin)
         expect(response).to have_http_status(201)
       end
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 9d3d28e0b9151a..3cd4e981fb270d 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -49,28 +49,28 @@
 
   describe "GET /issues" do
     context "when unauthenticated" do
-      it "should return authentication error" do
+      it "returns authentication error" do
         get api("/issues")
         expect(response).to have_http_status(401)
       end
     end
 
     context "when authenticated" do
-      it "should return an array of issues" do
+      it "returns an array of issues" do
         get api("/issues", user)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
         expect(json_response.first['title']).to eq(issue.title)
       end
 
-      it "should add pagination headers and keep query params" do
+      it "adds pagination headers and keep query params" do
         get api("/issues?state=closed&per_page=3", user)
         expect(response.headers['Link']).to eq(
           '; rel="first", ; rel="last"' % [user.private_token, user.private_token]
         )
       end
 
-      it 'should return an array of closed issues' do
+      it 'returns an array of closed issues' do
         get api('/issues?state=closed', user)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -78,7 +78,7 @@
         expect(json_response.first['id']).to eq(closed_issue.id)
       end
 
-      it 'should return an array of opened issues' do
+      it 'returns an array of opened issues' do
         get api('/issues?state=opened', user)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -86,7 +86,7 @@
         expect(json_response.first['id']).to eq(issue.id)
       end
 
-      it 'should return an array of all issues' do
+      it 'returns an array of all issues' do
         get api('/issues?state=all', user)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -95,7 +95,7 @@
         expect(json_response.second['id']).to eq(closed_issue.id)
       end
 
-      it 'should return an array of labeled issues' do
+      it 'returns an array of labeled issues' do
         get api("/issues?labels=#{label.title}", user)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -103,7 +103,7 @@
         expect(json_response.first['labels']).to eq([label.title])
       end
 
-      it 'should return an array of labeled issues when at least one label matches' do
+      it 'returns an array of labeled issues when at least one label matches' do
         get api("/issues?labels=#{label.title},foo,bar", user)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -111,14 +111,14 @@
         expect(json_response.first['labels']).to eq([label.title])
       end
 
-      it 'should return an empty array if no issue matches labels' do
+      it 'returns an empty array if no issue matches labels' do
         get api('/issues?labels=foo,bar', user)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
         expect(json_response.length).to eq(0)
       end
 
-      it 'should return an array of labeled issues matching given state' do
+      it 'returns an array of labeled issues matching given state' do
         get api("/issues?labels=#{label.title}&state=opened", user)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -127,7 +127,7 @@
         expect(json_response.first['state']).to eq('opened')
       end
 
-      it 'should return an empty array if no issue matches labels and state filters' do
+      it 'returns an empty array if no issue matches labels and state filters' do
         get api("/issues?labels=#{label.title}&state=closed", user)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -282,7 +282,7 @@
     let(:base_url) { "/projects/#{project.id}" }
     let(:title) { milestone.title }
 
-    it 'should return project issues without confidential issues for non project members' do
+    it 'returns project issues without confidential issues for non project members' do
       get api("#{base_url}/issues", non_member)
       expect(response).to have_http_status(200)
       expect(json_response).to be_an Array
@@ -290,7 +290,7 @@
       expect(json_response.first['title']).to eq(issue.title)
     end
 
-    it 'should return project issues without confidential issues for project members with guest role' do
+    it 'returns project issues without confidential issues for project members with guest role' do
       get api("#{base_url}/issues", guest)
       expect(response).to have_http_status(200)
       expect(json_response).to be_an Array
@@ -298,7 +298,7 @@
       expect(json_response.first['title']).to eq(issue.title)
     end
 
-    it 'should return project confidential issues for author' do
+    it 'returns project confidential issues for author' do
       get api("#{base_url}/issues", author)
       expect(response).to have_http_status(200)
       expect(json_response).to be_an Array
@@ -306,7 +306,7 @@
       expect(json_response.first['title']).to eq(issue.title)
     end
 
-    it 'should return project confidential issues for assignee' do
+    it 'returns project confidential issues for assignee' do
       get api("#{base_url}/issues", assignee)
       expect(response).to have_http_status(200)
       expect(json_response).to be_an Array
@@ -314,7 +314,7 @@
       expect(json_response.first['title']).to eq(issue.title)
     end
 
-    it 'should return project issues with confidential issues for project members' do
+    it 'returns project issues with confidential issues for project members' do
       get api("#{base_url}/issues", user)
       expect(response).to have_http_status(200)
       expect(json_response).to be_an Array
@@ -322,7 +322,7 @@
       expect(json_response.first['title']).to eq(issue.title)
     end
 
-    it 'should return project confidential issues for admin' do
+    it 'returns project confidential issues for admin' do
       get api("#{base_url}/issues", admin)
       expect(response).to have_http_status(200)
       expect(json_response).to be_an Array
@@ -330,7 +330,7 @@
       expect(json_response.first['title']).to eq(issue.title)
     end
 
-    it 'should return an array of labeled project issues' do
+    it 'returns an array of labeled project issues' do
       get api("#{base_url}/issues?labels=#{label.title}", user)
       expect(response).to have_http_status(200)
       expect(json_response).to be_an Array
@@ -338,7 +338,7 @@
       expect(json_response.first['labels']).to eq([label.title])
     end
 
-    it 'should return an array of labeled project issues when at least one label matches' do
+    it 'returns an array of labeled project issues when at least one label matches' do
       get api("#{base_url}/issues?labels=#{label.title},foo,bar", user)
       expect(response).to have_http_status(200)
       expect(json_response).to be_an Array
@@ -346,28 +346,28 @@
       expect(json_response.first['labels']).to eq([label.title])
     end
 
-    it 'should return an empty array if no project issue matches labels' do
+    it 'returns an empty array if no project issue matches labels' do
       get api("#{base_url}/issues?labels=foo,bar", user)
       expect(response).to have_http_status(200)
       expect(json_response).to be_an Array
       expect(json_response.length).to eq(0)
     end
 
-    it 'should return an empty array if no issue matches milestone' do
+    it 'returns an empty array if no issue matches milestone' do
       get api("#{base_url}/issues?milestone=#{empty_milestone.title}", user)
       expect(response).to have_http_status(200)
       expect(json_response).to be_an Array
       expect(json_response.length).to eq(0)
     end
 
-    it 'should return an empty array if milestone does not exist' do
+    it 'returns an empty array if milestone does not exist' do
       get api("#{base_url}/issues?milestone=foo", user)
       expect(response).to have_http_status(200)
       expect(json_response).to be_an Array
       expect(json_response.length).to eq(0)
     end
 
-    it 'should return an array of issues in given milestone' do
+    it 'returns an array of issues in given milestone' do
       get api("#{base_url}/issues?milestone=#{title}", user)
       expect(response).to have_http_status(200)
       expect(json_response).to be_an Array
@@ -376,7 +376,7 @@
       expect(json_response.second['id']).to eq(closed_issue.id)
     end
 
-    it 'should return an array of issues matching state in milestone' do
+    it 'returns an array of issues matching state in milestone' do
       get api("#{base_url}/issues?milestone=#{milestone.title}"\
               '&state=closed', user)
       expect(response).to have_http_status(200)
@@ -405,7 +405,7 @@
       expect(json_response['author']).to be_a Hash
     end
 
-    it "should return a project issue by id" do
+    it "returns a project issue by id" do
       get api("/projects/#{project.id}/issues/#{issue.id}", user)
 
       expect(response).to have_http_status(200)
@@ -413,7 +413,7 @@
       expect(json_response['iid']).to eq(issue.iid)
     end
 
-    it 'should return a project issue by iid' do
+    it 'returns a project issue by iid' do
       get api("/projects/#{project.id}/issues?iid=#{issue.iid}", user)
       expect(response.status).to eq 200
       expect(json_response.first['title']).to eq issue.title
@@ -421,44 +421,44 @@
       expect(json_response.first['iid']).to eq issue.iid
     end
 
-    it "should return 404 if issue id not found" do
+    it "returns 404 if issue id not found" do
       get api("/projects/#{project.id}/issues/54321", user)
       expect(response).to have_http_status(404)
     end
 
     context 'confidential issues' do
-      it "should return 404 for non project members" do
+      it "returns 404 for non project members" do
         get api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member)
         expect(response).to have_http_status(404)
       end
 
-      it "should return 404 for project members with guest role" do
+      it "returns 404 for project members with guest role" do
         get api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest)
         expect(response).to have_http_status(404)
       end
 
-      it "should return confidential issue for project members" do
+      it "returns confidential issue for project members" do
         get api("/projects/#{project.id}/issues/#{confidential_issue.id}", user)
         expect(response).to have_http_status(200)
         expect(json_response['title']).to eq(confidential_issue.title)
         expect(json_response['iid']).to eq(confidential_issue.iid)
       end
 
-      it "should return confidential issue for author" do
+      it "returns confidential issue for author" do
         get api("/projects/#{project.id}/issues/#{confidential_issue.id}", author)
         expect(response).to have_http_status(200)
         expect(json_response['title']).to eq(confidential_issue.title)
         expect(json_response['iid']).to eq(confidential_issue.iid)
       end
 
-      it "should return confidential issue for assignee" do
+      it "returns confidential issue for assignee" do
         get api("/projects/#{project.id}/issues/#{confidential_issue.id}", assignee)
         expect(response).to have_http_status(200)
         expect(json_response['title']).to eq(confidential_issue.title)
         expect(json_response['iid']).to eq(confidential_issue.iid)
       end
 
-      it "should return confidential issue for admin" do
+      it "returns confidential issue for admin" do
         get api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin)
         expect(response).to have_http_status(200)
         expect(json_response['title']).to eq(confidential_issue.title)
@@ -468,7 +468,7 @@
   end
 
   describe "POST /projects/:id/issues" do
-    it "should create a new project issue" do
+    it "creates a new project issue" do
       post api("/projects/#{project.id}/issues", user),
         title: 'new issue', labels: 'label, label2'
       expect(response).to have_http_status(201)
@@ -477,12 +477,12 @@
       expect(json_response['labels']).to eq(['label', 'label2'])
     end
 
-    it "should return a 400 bad request if title not given" do
+    it "returns a 400 bad request if title not given" do
       post api("/projects/#{project.id}/issues", user), labels: 'label, label2'
       expect(response).to have_http_status(400)
     end
 
-    it 'should allow special label names' do
+    it 'allows special label names' do
       post api("/projects/#{project.id}/issues", user),
            title: 'new issue',
            labels: 'label, label?, label&foo, ?, &'
@@ -494,7 +494,7 @@
       expect(json_response['labels']).to include '&'
     end
 
-    it 'should return 400 if title is too long' do
+    it 'returns 400 if title is too long' do
       post api("/projects/#{project.id}/issues", user),
            title: 'g' * 256
       expect(response).to have_http_status(400)
@@ -543,7 +543,7 @@
       }
     end
 
-    it "should not create a new project issue" do
+    it "does not create a new project issue" do
       expect { post api("/projects/#{project.id}/issues", user), params }.not_to change(Issue, :count)
       expect(response).to have_http_status(400)
       expect(json_response['message']).to eq({ "error" => "Spam detected" })
@@ -559,7 +559,7 @@
   end
 
   describe "PUT /projects/:id/issues/:issue_id to update only title" do
-    it "should update a project issue" do
+    it "updates a project issue" do
       put api("/projects/#{project.id}/issues/#{issue.id}", user),
         title: 'updated title'
       expect(response).to have_http_status(200)
@@ -567,13 +567,13 @@
       expect(json_response['title']).to eq('updated title')
     end
 
-    it "should return 404 error if issue id not found" do
+    it "returns 404 error if issue id not found" do
       put api("/projects/#{project.id}/issues/44444", user),
         title: 'updated title'
       expect(response).to have_http_status(404)
     end
 
-    it 'should allow special label names' do
+    it 'allows special label names' do
       put api("/projects/#{project.id}/issues/#{issue.id}", user),
           title: 'updated title',
           labels: 'label, label?, label&foo, ?, &'
@@ -587,33 +587,33 @@
     end
 
     context 'confidential issues' do
-      it "should return 403 for non project members" do
+      it "returns 403 for non project members" do
         put api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member),
           title: 'updated title'
         expect(response).to have_http_status(403)
       end
 
-      it "should return 403 for project members with guest role" do
+      it "returns 403 for project members with guest role" do
         put api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest),
           title: 'updated title'
         expect(response).to have_http_status(403)
       end
 
-      it "should update a confidential issue for project members" do
+      it "updates a confidential issue for project members" do
         put api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
           title: 'updated title'
         expect(response).to have_http_status(200)
         expect(json_response['title']).to eq('updated title')
       end
 
-      it "should update a confidential issue for author" do
+      it "updates a confidential issue for author" do
         put api("/projects/#{project.id}/issues/#{confidential_issue.id}", author),
           title: 'updated title'
         expect(response).to have_http_status(200)
         expect(json_response['title']).to eq('updated title')
       end
 
-      it "should update a confidential issue for admin" do
+      it "updates a confidential issue for admin" do
         put api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin),
           title: 'updated title'
         expect(response).to have_http_status(200)
@@ -626,21 +626,21 @@
     let!(:label) { create(:label, title: 'dummy', project: project) }
     let!(:label_link) { create(:label_link, label: label, target: issue) }
 
-    it 'should not update labels if not present' do
+    it 'does not update labels if not present' do
       put api("/projects/#{project.id}/issues/#{issue.id}", user),
           title: 'updated title'
       expect(response).to have_http_status(200)
       expect(json_response['labels']).to eq([label.title])
     end
 
-    it 'should remove all labels' do
+    it 'removes all labels' do
       put api("/projects/#{project.id}/issues/#{issue.id}", user),
           labels: ''
       expect(response).to have_http_status(200)
       expect(json_response['labels']).to eq([])
     end
 
-    it 'should update labels' do
+    it 'updates labels' do
       put api("/projects/#{project.id}/issues/#{issue.id}", user),
           labels: 'foo,bar'
       expect(response).to have_http_status(200)
@@ -648,7 +648,7 @@
       expect(json_response['labels']).to include 'bar'
     end
 
-    it 'should allow special label names' do
+    it 'allows special label names' do
       put api("/projects/#{project.id}/issues/#{issue.id}", user),
           labels: 'label:foo, label-bar,label_bar,label/bar,label?bar,label&bar,?,&'
       expect(response.status).to eq(200)
@@ -662,7 +662,7 @@
       expect(json_response['labels']).to include '&'
     end
 
-    it 'should return 400 if title is too long' do
+    it 'returns 400 if title is too long' do
       put api("/projects/#{project.id}/issues/#{issue.id}", user),
           title: 'g' * 256
       expect(response).to have_http_status(400)
@@ -673,7 +673,7 @@
   end
 
   describe "PUT /projects/:id/issues/:issue_id to update state and label" do
-    it "should update a project issue" do
+    it "updates a project issue" do
       put api("/projects/#{project.id}/issues/#{issue.id}", user),
         labels: 'label2', state_event: "close"
       expect(response).to have_http_status(200)
diff --git a/spec/requests/api/keys_spec.rb b/spec/requests/api/keys_spec.rb
index 1861882d59ecb8..893ed5c2b10d97 100644
--- a/spec/requests/api/keys_spec.rb
+++ b/spec/requests/api/keys_spec.rb
@@ -12,20 +12,20 @@
     before { admin }
 
     context 'when unauthenticated' do
-      it 'should return authentication error' do
+      it 'returns authentication error' do
         get api("/keys/#{key.id}")
         expect(response).to have_http_status(401)
       end
     end
 
     context 'when authenticated' do
-      it 'should return 404 for non-existing key' do
+      it 'returns 404 for non-existing key' do
         get api('/keys/999999', admin)
         expect(response).to have_http_status(404)
         expect(json_response['message']).to eq('404 Not found')
       end
 
-      it 'should return single ssh key with user information' do
+      it 'returns single ssh key with user information' do
         user.keys << key
         user.save
         get api("/keys/#{key.id}", admin)
diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb
index 63636b4a1b61a8..83789223019c76 100644
--- a/spec/requests/api/labels_spec.rb
+++ b/spec/requests/api/labels_spec.rb
@@ -12,7 +12,7 @@
   end
 
   describe 'GET /projects/:id/labels' do
-    it 'should return project labels' do
+    it 'returns project labels' do
       get api("/projects/#{project.id}/labels", user)
       expect(response).to have_http_status(200)
       expect(json_response).to be_an Array
@@ -22,7 +22,7 @@
   end
 
   describe 'POST /projects/:id/labels' do
-    it 'should return created label when all params' do
+    it 'returns created label when all params' do
       post api("/projects/#{project.id}/labels", user),
            name: 'Foo',
            color: '#FFAABB',
@@ -33,7 +33,7 @@
       expect(json_response['description']).to eq('test')
     end
 
-    it 'should return created label when only required params' do
+    it 'returns created label when only required params' do
       post api("/projects/#{project.id}/labels", user),
            name: 'Foo & Bar',
            color: '#FFAABB'
@@ -43,17 +43,17 @@
       expect(json_response['description']).to be_nil
     end
 
-    it 'should return a 400 bad request if name not given' do
+    it 'returns a 400 bad request if name not given' do
       post api("/projects/#{project.id}/labels", user), color: '#FFAABB'
       expect(response).to have_http_status(400)
     end
 
-    it 'should return a 400 bad request if color not given' do
+    it 'returns a 400 bad request if color not given' do
       post api("/projects/#{project.id}/labels", user), name: 'Foobar'
       expect(response).to have_http_status(400)
     end
 
-    it 'should return 400 for invalid color' do
+    it 'returns 400 for invalid color' do
       post api("/projects/#{project.id}/labels", user),
            name: 'Foo',
            color: '#FFAA'
@@ -61,7 +61,7 @@
       expect(json_response['message']['color']).to eq(['must be a valid color code'])
     end
 
-    it 'should return 400 for too long color code' do
+    it 'returns 400 for too long color code' do
       post api("/projects/#{project.id}/labels", user),
            name: 'Foo',
            color: '#FFAAFFFF'
@@ -69,7 +69,7 @@
       expect(json_response['message']['color']).to eq(['must be a valid color code'])
     end
 
-    it 'should return 400 for invalid name' do
+    it 'returns 400 for invalid name' do
       post api("/projects/#{project.id}/labels", user),
            name: ',',
            color: '#FFAABB'
@@ -77,7 +77,7 @@
       expect(json_response['message']['title']).to eq(['is invalid'])
     end
 
-    it 'should return 409 if label already exists' do
+    it 'returns 409 if label already exists' do
       post api("/projects/#{project.id}/labels", user),
            name: 'label1',
            color: '#FFAABB'
@@ -87,25 +87,25 @@
   end
 
   describe 'DELETE /projects/:id/labels' do
-    it 'should return 200 for existing label' do
+    it 'returns 200 for existing label' do
       delete api("/projects/#{project.id}/labels", user), name: 'label1'
       expect(response).to have_http_status(200)
     end
 
-    it 'should return 404 for non existing label' do
+    it 'returns 404 for non existing label' do
       delete api("/projects/#{project.id}/labels", user), name: 'label2'
       expect(response).to have_http_status(404)
       expect(json_response['message']).to eq('404 Label Not Found')
     end
 
-    it 'should return 400 for wrong parameters' do
+    it 'returns 400 for wrong parameters' do
       delete api("/projects/#{project.id}/labels", user)
       expect(response).to have_http_status(400)
     end
   end
 
   describe 'PUT /projects/:id/labels' do
-    it 'should return 200 if name and colors and description are changed' do
+    it 'returns 200 if name and colors and description are changed' do
       put api("/projects/#{project.id}/labels", user),
           name: 'label1',
           new_name: 'New Label',
@@ -117,7 +117,7 @@
       expect(json_response['description']).to eq('test')
     end
 
-    it 'should return 200 if name is changed' do
+    it 'returns 200 if name is changed' do
       put api("/projects/#{project.id}/labels", user),
           name: 'label1',
           new_name: 'New Label'
@@ -126,7 +126,7 @@
       expect(json_response['color']).to eq(label1.color)
     end
 
-    it 'should return 200 if colors is changed' do
+    it 'returns 200 if colors is changed' do
       put api("/projects/#{project.id}/labels", user),
           name: 'label1',
           color: '#FFFFFF'
@@ -135,7 +135,7 @@
       expect(json_response['color']).to eq('#FFFFFF')
     end
 
-    it 'should return 200 if description is changed' do
+    it 'returns 200 if description is changed' do
       put api("/projects/#{project.id}/labels", user),
           name: 'label1',
           description: 'test'
@@ -144,27 +144,27 @@
       expect(json_response['description']).to eq('test')
     end
 
-    it 'should return 404 if label does not exist' do
+    it 'returns 404 if label does not exist' do
       put api("/projects/#{project.id}/labels", user),
           name: 'label2',
           new_name: 'label3'
       expect(response).to have_http_status(404)
     end
 
-    it 'should return 400 if no label name given' do
+    it 'returns 400 if no label name given' do
       put api("/projects/#{project.id}/labels", user), new_name: 'label2'
       expect(response).to have_http_status(400)
       expect(json_response['message']).to eq('400 (Bad request) "name" not given')
     end
 
-    it 'should return 400 if no new parameters given' do
+    it 'returns 400 if no new parameters given' do
       put api("/projects/#{project.id}/labels", user), name: 'label1'
       expect(response).to have_http_status(400)
       expect(json_response['message']).to eq('Required parameters '\
                                          '"new_name" or "color" missing')
     end
 
-    it 'should return 400 for invalid name' do
+    it 'returns 400 for invalid name' do
       put api("/projects/#{project.id}/labels", user),
           name: 'label1',
           new_name: ',',
@@ -173,7 +173,7 @@
       expect(json_response['message']['title']).to eq(['is invalid'])
     end
 
-    it 'should return 400 when color code is too short' do
+    it 'returns 400 when color code is too short' do
       put api("/projects/#{project.id}/labels", user),
           name: 'label1',
           color: '#FF'
@@ -181,7 +181,7 @@
       expect(json_response['message']['color']).to eq(['must be a valid color code'])
     end
 
-    it 'should return 400 for too long color code' do
+    it 'returns 400 for too long color code' do
       post api("/projects/#{project.id}/labels", user),
            name: 'Foo',
            color: '#FFAAFFFF'
@@ -192,7 +192,7 @@
 
   describe "POST /projects/:id/labels/:label_id/subscription" do
     context "when label_id is a label title" do
-      it "should subscribe to the label" do
+      it "subscribes to the label" do
         post api("/projects/#{project.id}/labels/#{label1.title}/subscription", user)
 
         expect(response).to have_http_status(201)
@@ -202,7 +202,7 @@
     end
 
     context "when label_id is a label ID" do
-      it "should subscribe to the label" do
+      it "subscribes to the label" do
         post api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
 
         expect(response).to have_http_status(201)
@@ -214,7 +214,7 @@
     context "when user is already subscribed to label" do
       before { label1.subscribe(user) }
 
-      it "should return 304" do
+      it "returns 304" do
         post api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
 
         expect(response).to have_http_status(304)
@@ -222,7 +222,7 @@
     end
 
     context "when label ID is not found" do
-      it "should a return 404 error" do
+      it "returns 404 error" do
         post api("/projects/#{project.id}/labels/1234/subscription", user)
 
         expect(response).to have_http_status(404)
@@ -234,7 +234,7 @@
     before { label1.subscribe(user) }
 
     context "when label_id is a label title" do
-      it "should unsubscribe from the label" do
+      it "unsubscribes from the label" do
         delete api("/projects/#{project.id}/labels/#{label1.title}/subscription", user)
 
         expect(response).to have_http_status(200)
@@ -244,7 +244,7 @@
     end
 
     context "when label_id is a label ID" do
-      it "should unsubscribe from the label" do
+      it "unsubscribes from the label" do
         delete api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
 
         expect(response).to have_http_status(200)
@@ -256,7 +256,7 @@
     context "when user is already unsubscribed from label" do
       before { label1.unsubscribe(user) }
 
-      it "should return 304" do
+      it "returns 304" do
         delete api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
 
         expect(response).to have_http_status(304)
@@ -264,7 +264,7 @@
     end
 
     context "when label ID is not found" do
-      it "should a return 404 error" do
+      it "returns 404 error" do
         delete api("/projects/#{project.id}/labels/1234/subscription", user)
 
         expect(response).to have_http_status(404)
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 651b91e9f68a4d..617600d617344f 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -20,14 +20,14 @@
 
   describe "GET /projects/:id/merge_requests" do
     context "when unauthenticated" do
-      it "should return authentication error" do
+      it "returns authentication error" do
         get api("/projects/#{project.id}/merge_requests")
         expect(response).to have_http_status(401)
       end
     end
 
     context "when authenticated" do
-      it "should return an array of all merge_requests" do
+      it "returns an array of all merge_requests" do
         get api("/projects/#{project.id}/merge_requests", user)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -35,7 +35,7 @@
         expect(json_response.last['title']).to eq(merge_request.title)
       end
 
-      it "should return an array of all merge_requests" do
+      it "returns an array of all merge_requests" do
         get api("/projects/#{project.id}/merge_requests?state", user)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -43,7 +43,7 @@
         expect(json_response.last['title']).to eq(merge_request.title)
       end
 
-      it "should return an array of open merge_requests" do
+      it "returns an array of open merge_requests" do
         get api("/projects/#{project.id}/merge_requests?state=opened", user)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -51,7 +51,7 @@
         expect(json_response.last['title']).to eq(merge_request.title)
       end
 
-      it "should return an array of closed merge_requests" do
+      it "returns an array of closed merge_requests" do
         get api("/projects/#{project.id}/merge_requests?state=closed", user)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -59,7 +59,7 @@
         expect(json_response.first['title']).to eq(merge_request_closed.title)
       end
 
-      it "should return an array of merged merge_requests" do
+      it "returns an array of merged merge_requests" do
         get api("/projects/#{project.id}/merge_requests?state=merged", user)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -73,7 +73,7 @@
           @mr_earlier = mr_with_earlier_created_and_updated_at_time
         end
 
-        it "should return an array of merge_requests in ascending order" do
+        it "returns an array of merge_requests in ascending order" do
           get api("/projects/#{project.id}/merge_requests?sort=asc", user)
           expect(response).to have_http_status(200)
           expect(json_response).to be_an Array
@@ -82,7 +82,7 @@
           expect(response_dates).to eq(response_dates.sort)
         end
 
-        it "should return an array of merge_requests in descending order" do
+        it "returns an array of merge_requests in descending order" do
           get api("/projects/#{project.id}/merge_requests?sort=desc", user)
           expect(response).to have_http_status(200)
           expect(json_response).to be_an Array
@@ -91,7 +91,7 @@
           expect(response_dates).to eq(response_dates.sort.reverse)
         end
 
-        it "should return an array of merge_requests ordered by updated_at" do
+        it "returns an array of merge_requests ordered by updated_at" do
           get api("/projects/#{project.id}/merge_requests?order_by=updated_at", user)
           expect(response).to have_http_status(200)
           expect(json_response).to be_an Array
@@ -100,7 +100,7 @@
           expect(response_dates).to eq(response_dates.sort.reverse)
         end
 
-        it "should return an array of merge_requests ordered by created_at" do
+        it "returns an array of merge_requests ordered by created_at" do
           get api("/projects/#{project.id}/merge_requests?order_by=created_at&sort=asc", user)
           expect(response).to have_http_status(200)
           expect(json_response).to be_an Array
@@ -142,7 +142,7 @@
       expect(json_response['force_close_merge_request']).to be_falsy
     end
 
-    it "should return merge_request" do
+    it "returns merge_request" do
       get api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user)
       expect(response).to have_http_status(200)
       expect(json_response['title']).to eq(merge_request.title)
@@ -153,7 +153,7 @@
       expect(json_response['force_close_merge_request']).to be_falsy
     end
 
-    it 'should return merge_request by iid' do
+    it 'returns merge_request by iid' do
       url = "/projects/#{project.id}/merge_requests?iid=#{merge_request.iid}"
       get api(url, user)
       expect(response.status).to eq 200
@@ -161,7 +161,7 @@
       expect(json_response.first['id']).to eq merge_request.id
     end
 
-    it "should return a 404 error if merge_request_id not found" do
+    it "returns a 404 error if merge_request_id not found" do
       get api("/projects/#{project.id}/merge_requests/999", user)
       expect(response).to have_http_status(404)
     end
@@ -169,7 +169,7 @@
     context 'Work in Progress' do
       let!(:merge_request_wip) { create(:merge_request, author: user, assignee: user, source_project: project, target_project: project, title: "WIP: Test", created_at: base_time + 1.second) }
 
-      it "should return merge_request" do
+      it "returns merge_request" do
         get api("/projects/#{project.id}/merge_requests/#{merge_request_wip.id}", user)
         expect(response).to have_http_status(200)
         expect(json_response['work_in_progress']).to eq(true)
@@ -195,7 +195,7 @@
   end
 
   describe 'GET /projects/:id/merge_requests/:merge_request_id/changes' do
-    it 'should return the change information of the merge_request' do
+    it 'returns the change information of the merge_request' do
       get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/changes", user)
       expect(response.status).to eq 200
       expect(json_response['changes'].size).to eq(merge_request.diffs.size)
@@ -209,7 +209,7 @@
 
   describe "POST /projects/:id/merge_requests" do
     context 'between branches projects' do
-      it "should return merge_request" do
+      it "returns merge_request" do
         post api("/projects/#{project.id}/merge_requests", user),
              title: 'Test merge_request',
              source_branch: 'feature_conflict',
@@ -223,31 +223,31 @@
         expect(json_response['milestone']['id']).to eq(milestone.id)
       end
 
-      it "should return 422 when source_branch equals target_branch" do
+      it "returns 422 when source_branch equals target_branch" do
         post api("/projects/#{project.id}/merge_requests", user),
         title: "Test merge_request", source_branch: "master", target_branch: "master", author: user
         expect(response).to have_http_status(422)
       end
 
-      it "should return 400 when source_branch is missing" do
+      it "returns 400 when source_branch is missing" do
         post api("/projects/#{project.id}/merge_requests", user),
         title: "Test merge_request", target_branch: "master", author: user
         expect(response).to have_http_status(400)
       end
 
-      it "should return 400 when target_branch is missing" do
+      it "returns 400 when target_branch is missing" do
         post api("/projects/#{project.id}/merge_requests", user),
         title: "Test merge_request", source_branch: "markdown", author: user
         expect(response).to have_http_status(400)
       end
 
-      it "should return 400 when title is missing" do
+      it "returns 400 when title is missing" do
         post api("/projects/#{project.id}/merge_requests", user),
         target_branch: 'master', source_branch: 'markdown'
         expect(response).to have_http_status(400)
       end
 
-      it 'should allow special label names' do
+      it 'allows special label names' do
         post api("/projects/#{project.id}/merge_requests", user),
              title: 'Test merge_request',
              source_branch: 'markdown',
@@ -272,7 +272,7 @@
           @mr = MergeRequest.all.last
         end
 
-        it 'should return 409 when MR already exists for source/target' do
+        it 'returns 409 when MR already exists for source/target' do
           expect do
             post api("/projects/#{project.id}/merge_requests", user),
                  title: 'New test merge_request',
@@ -294,7 +294,7 @@
         fork_project.team << [user2, :reporters]
       end
 
-      it "should return merge_request" do
+      it "returns merge_request" do
         post api("/projects/#{fork_project.id}/merge_requests", user2),
           title: 'Test merge_request', source_branch: "feature_conflict", target_branch: "master",
           author: user2, target_project_id: project.id, description: 'Test description for Test merge_request'
@@ -303,7 +303,7 @@
         expect(json_response['description']).to eq('Test description for Test merge_request')
       end
 
-      it "should not return 422 when source_branch equals target_branch" do
+      it "does not return 422 when source_branch equals target_branch" do
         expect(project.id).not_to eq(fork_project.id)
         expect(fork_project.forked?).to be_truthy
         expect(fork_project.forked_from_project).to eq(project)
@@ -313,26 +313,26 @@
         expect(json_response['title']).to eq('Test merge_request')
       end
 
-      it "should return 400 when source_branch is missing" do
+      it "returns 400 when source_branch is missing" do
         post api("/projects/#{fork_project.id}/merge_requests", user2),
         title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id
         expect(response).to have_http_status(400)
       end
 
-      it "should return 400 when target_branch is missing" do
+      it "returns 400 when target_branch is missing" do
         post api("/projects/#{fork_project.id}/merge_requests", user2),
         title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id
         expect(response).to have_http_status(400)
       end
 
-      it "should return 400 when title is missing" do
+      it "returns 400 when title is missing" do
         post api("/projects/#{fork_project.id}/merge_requests", user2),
         target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: project.id
         expect(response).to have_http_status(400)
       end
 
       context 'when target_branch is specified' do
-        it 'should return 422 if not a forked project' do
+        it 'returns 422 if not a forked project' do
           post api("/projects/#{project.id}/merge_requests", user),
                title: 'Test merge_request',
                target_branch: 'master',
@@ -342,7 +342,7 @@
           expect(response).to have_http_status(422)
         end
 
-        it 'should return 422 if targeting a different fork' do
+        it 'returns 422 if targeting a different fork' do
           post api("/projects/#{fork_project.id}/merge_requests", user2),
                title: 'Test merge_request',
                target_branch: 'master',
@@ -353,7 +353,7 @@
         end
       end
 
-      it "should return 201 when target_branch is specified and for the same project" do
+      it "returns 201 when target_branch is specified and for the same project" do
         post api("/projects/#{fork_project.id}/merge_requests", user2),
         title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: fork_project.id
         expect(response).to have_http_status(201)
@@ -385,7 +385,7 @@
   end
 
   describe "PUT /projects/:id/merge_requests/:merge_request_id to close MR" do
-    it "should return merge_request" do
+    it "returns merge_request" do
       put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: "close"
       expect(response).to have_http_status(200)
       expect(json_response['state']).to eq('closed')
@@ -395,13 +395,13 @@
   describe "PUT /projects/:id/merge_requests/:merge_request_id/merge" do
     let(:pipeline) { create(:ci_pipeline_without_jobs) }
 
-    it "should return merge_request in case of success" do
+    it "returns merge_request in case of success" do
       put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
 
       expect(response).to have_http_status(200)
     end
 
-    it "should return 406 if branch can't be merged" do
+    it "returns 406 if branch can't be merged" do
       allow_any_instance_of(MergeRequest).
         to receive(:can_be_merged?).and_return(false)
 
@@ -411,14 +411,14 @@
       expect(json_response['message']).to eq('Branch cannot be merged')
     end
 
-    it "should return 405 if merge_request is not open" do
+    it "returns 405 if merge_request is not open" do
       merge_request.close
       put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
       expect(response).to have_http_status(405)
       expect(json_response['message']).to eq('405 Method Not Allowed')
     end
 
-    it "should return 405 if merge_request is a work in progress" do
+    it "returns 405 if merge_request is a work in progress" do
       merge_request.update_attribute(:title, "WIP: #{merge_request.title}")
       put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
       expect(response).to have_http_status(405)
@@ -434,7 +434,7 @@
       expect(json_response['message']).to eq('405 Method Not Allowed')
     end
 
-    it "should return 401 if user has no permissions to merge" do
+    it "returns 401 if user has no permissions to merge" do
       user2 = create(:user)
       project.team << [user2, :reporter]
       put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user2)
@@ -486,19 +486,19 @@
       expect(json_response['milestone']['id']).to eq(milestone.id)
     end
 
-    it "should return 400 when source_branch is specified" do
+    it "returns 400 when source_branch is specified" do
       put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user),
       source_branch: "master", target_branch: "master"
       expect(response).to have_http_status(400)
     end
 
-    it "should return merge_request with renamed target_branch" do
+    it "returns merge_request with renamed target_branch" do
       put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), target_branch: "wiki"
       expect(response).to have_http_status(200)
       expect(json_response['target_branch']).to eq('wiki')
     end
 
-    it 'should allow special label names' do
+    it 'allows special label names' do
       put api("/projects/#{project.id}/merge_requests/#{merge_request.id}",
               user),
           title: 'new issue',
@@ -513,7 +513,7 @@
   end
 
   describe "POST /projects/:id/merge_requests/:merge_request_id/comments" do
-    it "should return comment" do
+    it "returns comment" do
       original_count = merge_request.notes.size
 
       post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user), note: "My comment"
@@ -524,12 +524,12 @@
       expect(merge_request.notes.size).to eq(original_count + 1)
     end
 
-    it "should return 400 if note is missing" do
+    it "returns 400 if note is missing" do
       post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user)
       expect(response).to have_http_status(400)
     end
 
-    it "should return 404 if note is attached to non existent merge request" do
+    it "returns 404 if note is attached to non existent merge request" do
       post api("/projects/#{project.id}/merge_requests/404/comments", user),
            note: 'My comment'
       expect(response).to have_http_status(404)
@@ -537,7 +537,7 @@
   end
 
   describe "GET :id/merge_requests/:merge_request_id/comments" do
-    it "should return merge_request comments ordered by created_at" do
+    it "returns merge_request comments ordered by created_at" do
       get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user)
       expect(response).to have_http_status(200)
       expect(json_response).to be_an Array
@@ -547,7 +547,7 @@
       expect(json_response.last['note']).to eq("another comment on a MR")
     end
 
-    it "should return a 404 error if merge_request_id not found" do
+    it "returns a 404 error if merge_request_id not found" do
       get api("/projects/#{project.id}/merge_requests/999/comments", user)
       expect(response).to have_http_status(404)
     end
diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb
index 0f4e38b24750dd..d6a0c656e7495d 100644
--- a/spec/requests/api/milestones_spec.rb
+++ b/spec/requests/api/milestones_spec.rb
@@ -10,14 +10,14 @@
   before { project.team << [user, :developer] }
 
   describe 'GET /projects/:id/milestones' do
-    it 'should return project milestones' do
+    it 'returns project milestones' do
       get api("/projects/#{project.id}/milestones", user)
       expect(response).to have_http_status(200)
       expect(json_response).to be_an Array
       expect(json_response.first['title']).to eq(milestone.title)
     end
 
-    it 'should return a 401 error if user not authenticated' do
+    it 'returns a 401 error if user not authenticated' do
       get api("/projects/#{project.id}/milestones")
       expect(response).to have_http_status(401)
     end
@@ -42,14 +42,14 @@
   end
 
   describe 'GET /projects/:id/milestones/:milestone_id' do
-    it 'should return a project milestone by id' do
+    it 'returns a project milestone by id' do
       get api("/projects/#{project.id}/milestones/#{milestone.id}", user)
       expect(response).to have_http_status(200)
       expect(json_response['title']).to eq(milestone.title)
       expect(json_response['iid']).to eq(milestone.iid)
     end
 
-    it 'should return a project milestone by iid' do
+    it 'returns a project milestone by iid' do
       get api("/projects/#{project.id}/milestones?iid=#{closed_milestone.iid}", user)
 
       expect(response.status).to eq 200
@@ -58,26 +58,26 @@
       expect(json_response.first['id']).to eq closed_milestone.id
     end
 
-    it 'should return 401 error if user not authenticated' do
+    it 'returns 401 error if user not authenticated' do
       get api("/projects/#{project.id}/milestones/#{milestone.id}")
       expect(response).to have_http_status(401)
     end
 
-    it 'should return a 404 error if milestone id not found' do
+    it 'returns a 404 error if milestone id not found' do
       get api("/projects/#{project.id}/milestones/1234", user)
       expect(response).to have_http_status(404)
     end
   end
 
   describe 'POST /projects/:id/milestones' do
-    it 'should create a new project milestone' do
+    it 'creates a new project milestone' do
       post api("/projects/#{project.id}/milestones", user), title: 'new milestone'
       expect(response).to have_http_status(201)
       expect(json_response['title']).to eq('new milestone')
       expect(json_response['description']).to be_nil
     end
 
-    it 'should create a new project milestone with description and due date' do
+    it 'creates a new project milestone with description and due date' do
       post api("/projects/#{project.id}/milestones", user),
         title: 'new milestone', description: 'release', due_date: '2013-03-02'
       expect(response).to have_http_status(201)
@@ -85,21 +85,21 @@
       expect(json_response['due_date']).to eq('2013-03-02')
     end
 
-    it 'should return a 400 error if title is missing' do
+    it 'returns a 400 error if title is missing' do
       post api("/projects/#{project.id}/milestones", user)
       expect(response).to have_http_status(400)
     end
   end
 
   describe 'PUT /projects/:id/milestones/:milestone_id' do
-    it 'should update a project milestone' do
+    it 'updates a project milestone' do
       put api("/projects/#{project.id}/milestones/#{milestone.id}", user),
         title: 'updated title'
       expect(response).to have_http_status(200)
       expect(json_response['title']).to eq('updated title')
     end
 
-    it 'should return a 404 error if milestone id not found' do
+    it 'returns a 404 error if milestone id not found' do
       put api("/projects/#{project.id}/milestones/1234", user),
         title: 'updated title'
       expect(response).to have_http_status(404)
@@ -107,7 +107,7 @@
   end
 
   describe 'PUT /projects/:id/milestones/:milestone_id to close milestone' do
-    it 'should update a project milestone' do
+    it 'updates a project milestone' do
       put api("/projects/#{project.id}/milestones/#{milestone.id}", user),
         state_event: 'close'
       expect(response).to have_http_status(200)
@@ -117,7 +117,7 @@
   end
 
   describe 'PUT /projects/:id/milestones/:milestone_id to test observer on close' do
-    it 'should create an activity event when an milestone is closed' do
+    it 'creates an activity event when an milestone is closed' do
       expect(Event).to receive(:create)
 
       put api("/projects/#{project.id}/milestones/#{milestone.id}", user),
@@ -129,14 +129,14 @@
     before do
       milestone.issues << create(:issue, project: project)
     end
-    it 'should return project issues for a particular milestone' do
+    it 'returns project issues for a particular milestone' do
       get api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user)
       expect(response).to have_http_status(200)
       expect(json_response).to be_an Array
       expect(json_response.first['milestone']['title']).to eq(milestone.title)
     end
 
-    it 'should return a 401 error if user not authenticated' do
+    it 'returns a 401 error if user not authenticated' do
       get api("/projects/#{project.id}/milestones/#{milestone.id}/issues")
       expect(response).to have_http_status(401)
     end
diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb
index 237b4b17eb5c63..5347cf4f7bcce9 100644
--- a/spec/requests/api/namespaces_spec.rb
+++ b/spec/requests/api/namespaces_spec.rb
@@ -9,14 +9,14 @@
 
   describe "GET /namespaces" do
     context "when unauthenticated" do
-      it "should return authentication error" do
+      it "returns authentication error" do
         get api("/namespaces")
         expect(response).to have_http_status(401)
       end
     end
 
     context "when authenticated as admin" do
-      it "admin: should return an array of all namespaces" do
+      it "admin: returns an array of all namespaces" do
         get api("/namespaces", admin)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -24,7 +24,7 @@
         expect(json_response.length).to eq(Namespace.count)
       end
 
-      it "admin: should return an array of matched namespaces" do
+      it "admin: returns an array of matched namespaces" do
         get api("/namespaces?search=#{group1.name}", admin)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -34,7 +34,7 @@
     end
 
     context "when authenticated as a regular user" do
-      it "user: should return an array of namespaces" do
+      it "user: returns an array of namespaces" do
         get api("/namespaces", user)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -42,7 +42,7 @@
         expect(json_response.length).to eq(1)
       end
 
-      it "admin: should return an array of matched namespaces" do
+      it "admin: returns an array of matched namespaces" do
         get api("/namespaces?search=#{user.username}", user)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb
index 65c53211dd312d..737fa14cbb0c36 100644
--- a/spec/requests/api/notes_spec.rb
+++ b/spec/requests/api/notes_spec.rb
@@ -37,7 +37,7 @@
     end
 
     context "when noteable is an Issue" do
-      it "should return an array of issue notes" do
+      it "returns an array of issue notes" do
         get api("/projects/#{project.id}/issues/#{issue.id}/notes", user)
 
         expect(response).to have_http_status(200)
@@ -45,14 +45,14 @@
         expect(json_response.first['body']).to eq(issue_note.note)
       end
 
-      it "should return a 404 error when issue id not found" do
+      it "returns a 404 error when issue id not found" do
         get api("/projects/#{project.id}/issues/12345/notes", user)
 
         expect(response).to have_http_status(404)
       end
 
       context "and current user cannot view the notes" do
-        it "should return an empty array" do
+        it "returns an empty array" do
           get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user)
 
           expect(response).to have_http_status(200)
@@ -71,7 +71,7 @@
         end
 
         context "and current user can view the note" do
-          it "should return an empty array" do
+          it "returns an empty array" do
             get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", private_user)
 
             expect(response).to have_http_status(200)
@@ -83,7 +83,7 @@
     end
 
     context "when noteable is a Snippet" do
-      it "should return an array of snippet notes" do
+      it "returns an array of snippet notes" do
         get api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user)
 
         expect(response).to have_http_status(200)
@@ -91,7 +91,7 @@
         expect(json_response.first['body']).to eq(snippet_note.note)
       end
 
-      it "should return a 404 error when snippet id not found" do
+      it "returns a 404 error when snippet id not found" do
         get api("/projects/#{project.id}/snippets/42/notes", user)
 
         expect(response).to have_http_status(404)
@@ -105,7 +105,7 @@
     end
 
     context "when noteable is a Merge Request" do
-      it "should return an array of merge_requests notes" do
+      it "returns an array of merge_requests notes" do
         get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/notes", user)
 
         expect(response).to have_http_status(200)
@@ -113,7 +113,7 @@
         expect(json_response.first['body']).to eq(merge_request_note.note)
       end
 
-      it "should return a 404 error if merge request id not found" do
+      it "returns a 404 error if merge request id not found" do
         get api("/projects/#{project.id}/merge_requests/4444/notes", user)
 
         expect(response).to have_http_status(404)
@@ -129,21 +129,21 @@
 
   describe "GET /projects/:id/noteable/:noteable_id/notes/:note_id" do
     context "when noteable is an Issue" do
-      it "should return an issue note by id" do
+      it "returns an issue note by id" do
         get api("/projects/#{project.id}/issues/#{issue.id}/notes/#{issue_note.id}", user)
 
         expect(response).to have_http_status(200)
         expect(json_response['body']).to eq(issue_note.note)
       end
 
-      it "should return a 404 error if issue note not found" do
+      it "returns a 404 error if issue note not found" do
         get api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user)
 
         expect(response).to have_http_status(404)
       end
 
       context "and current user cannot view the note" do
-        it "should return a 404 error" do
+        it "returns a 404 error" do
           get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", user)
 
           expect(response).to have_http_status(404)
@@ -160,7 +160,7 @@
         end
 
         context "and current user can view the note" do
-          it "should return an issue note by id" do
+          it "returns an issue note by id" do
             get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", private_user)
 
             expect(response).to have_http_status(200)
@@ -171,14 +171,14 @@
     end
 
     context "when noteable is a Snippet" do
-      it "should return a snippet note by id" do
+      it "returns a snippet note by id" do
         get api("/projects/#{project.id}/snippets/#{snippet.id}/notes/#{snippet_note.id}", user)
 
         expect(response).to have_http_status(200)
         expect(json_response['body']).to eq(snippet_note.note)
       end
 
-      it "should return a 404 error if snippet note not found" do
+      it "returns a 404 error if snippet note not found" do
         get api("/projects/#{project.id}/snippets/#{snippet.id}/notes/12345", user)
 
         expect(response).to have_http_status(404)
@@ -188,7 +188,7 @@
 
   describe "POST /projects/:id/noteable/:noteable_id/notes" do
     context "when noteable is an Issue" do
-      it "should create a new issue note" do
+      it "creates a new issue note" do
         post api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!'
 
         expect(response).to have_http_status(201)
@@ -196,13 +196,13 @@
         expect(json_response['author']['username']).to eq(user.username)
       end
 
-      it "should return a 400 bad request error if body not given" do
+      it "returns a 400 bad request error if body not given" do
         post api("/projects/#{project.id}/issues/#{issue.id}/notes", user)
 
         expect(response).to have_http_status(400)
       end
 
-      it "should return a 401 unauthorized error if user not authenticated" do
+      it "returns a 401 unauthorized error if user not authenticated" do
         post api("/projects/#{project.id}/issues/#{issue.id}/notes"), body: 'hi!'
 
         expect(response).to have_http_status(401)
@@ -223,7 +223,7 @@
     end
 
     context "when noteable is a Snippet" do
-      it "should create a new snippet note" do
+      it "creates a new snippet note" do
         post api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user), body: 'hi!'
 
         expect(response).to have_http_status(201)
@@ -231,13 +231,13 @@
         expect(json_response['author']['username']).to eq(user.username)
       end
 
-      it "should return a 400 bad request error if body not given" do
+      it "returns a 400 bad request error if body not given" do
         post api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user)
 
         expect(response).to have_http_status(400)
       end
 
-      it "should return a 401 unauthorized error if user not authenticated" do
+      it "returns a 401 unauthorized error if user not authenticated" do
         post api("/projects/#{project.id}/snippets/#{snippet.id}/notes"), body: 'hi!'
 
         expect(response).to have_http_status(401)
@@ -267,7 +267,7 @@
   end
 
   describe "POST /projects/:id/noteable/:noteable_id/notes to test observer on create" do
-    it "should create an activity event when an issue note is created" do
+    it "creates an activity event when an issue note is created" do
       expect(Event).to receive(:create)
 
       post api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!'
@@ -276,7 +276,7 @@
 
   describe 'PUT /projects/:id/noteable/:noteable_id/notes/:note_id' do
     context 'when noteable is an Issue' do
-      it 'should return modified note' do
+      it 'returns modified note' do
         put api("/projects/#{project.id}/issues/#{issue.id}/"\
                   "notes/#{issue_note.id}", user), body: 'Hello!'
 
@@ -284,14 +284,14 @@
         expect(json_response['body']).to eq('Hello!')
       end
 
-      it 'should return a 404 error when note id not found' do
+      it 'returns a 404 error when note id not found' do
         put api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user),
                 body: 'Hello!'
 
         expect(response).to have_http_status(404)
       end
 
-      it 'should return a 400 bad request error if body not given' do
+      it 'returns a 400 bad request error if body not given' do
         put api("/projects/#{project.id}/issues/#{issue.id}/"\
                   "notes/#{issue_note.id}", user)
 
@@ -300,7 +300,7 @@
     end
 
     context 'when noteable is a Snippet' do
-      it 'should return modified note' do
+      it 'returns modified note' do
         put api("/projects/#{project.id}/snippets/#{snippet.id}/"\
                   "notes/#{snippet_note.id}", user), body: 'Hello!'
 
@@ -308,7 +308,7 @@
         expect(json_response['body']).to eq('Hello!')
       end
 
-      it 'should return a 404 error when note id not found' do
+      it 'returns a 404 error when note id not found' do
         put api("/projects/#{project.id}/snippets/#{snippet.id}/"\
                   "notes/12345", user), body: "Hello!"
 
@@ -317,7 +317,7 @@
     end
 
     context 'when noteable is a Merge Request' do
-      it 'should return modified note' do
+      it 'returns modified note' do
         put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\
                   "notes/#{merge_request_note.id}", user), body: 'Hello!'
 
@@ -325,7 +325,7 @@
         expect(json_response['body']).to eq('Hello!')
       end
 
-      it 'should return a 404 error when note id not found' do
+      it 'returns a 404 error when note id not found' do
         put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\
                   "notes/12345", user), body: "Hello!"
 
diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb
index fd1fffa6223264..34fac297923e87 100644
--- a/spec/requests/api/project_hooks_spec.rb
+++ b/spec/requests/api/project_hooks_spec.rb
@@ -20,7 +20,7 @@
 
   describe "GET /projects/:id/hooks" do
     context "authorized user" do
-      it "should return project hooks" do
+      it "returns project hooks" do
         get api("/projects/#{project.id}/hooks", user)
         expect(response).to have_http_status(200)
 
@@ -38,7 +38,7 @@
     end
 
     context "unauthorized user" do
-      it "should not access project hooks" do
+      it "does not access project hooks" do
         get api("/projects/#{project.id}/hooks", user3)
         expect(response).to have_http_status(403)
       end
@@ -47,7 +47,7 @@
 
   describe "GET /projects/:id/hooks/:hook_id" do
     context "authorized user" do
-      it "should return a project hook" do
+      it "returns a project hook" do
         get api("/projects/#{project.id}/hooks/#{hook.id}", user)
         expect(response).to have_http_status(200)
         expect(json_response['url']).to eq(hook.url)
@@ -59,27 +59,27 @@
         expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification)
       end
 
-      it "should return a 404 error if hook id is not available" do
+      it "returns a 404 error if hook id is not available" do
         get api("/projects/#{project.id}/hooks/1234", user)
         expect(response).to have_http_status(404)
       end
     end
 
     context "unauthorized user" do
-      it "should not access an existing hook" do
+      it "does not access an existing hook" do
         get api("/projects/#{project.id}/hooks/#{hook.id}", user3)
         expect(response).to have_http_status(403)
       end
     end
 
-    it "should return a 404 error if hook id is not available" do
+    it "returns a 404 error if hook id is not available" do
       get api("/projects/#{project.id}/hooks/1234", user)
       expect(response).to have_http_status(404)
     end
   end
 
   describe "POST /projects/:id/hooks" do
-    it "should add hook to project" do
+    it "adds hook to project" do
       expect do
         post api("/projects/#{project.id}/hooks", user), url: "http://example.com", issues_events: true
       end.to change {project.hooks.count}.by(1)
@@ -94,19 +94,19 @@
       expect(json_response['enable_ssl_verification']).to eq(true)
     end
 
-    it "should return a 400 error if url not given" do
+    it "returns a 400 error if url not given" do
       post api("/projects/#{project.id}/hooks", user)
       expect(response).to have_http_status(400)
     end
 
-    it "should return a 422 error if url not valid" do
+    it "returns a 422 error if url not valid" do
       post api("/projects/#{project.id}/hooks", user), "url" => "ftp://example.com"
       expect(response).to have_http_status(422)
     end
   end
 
   describe "PUT /projects/:id/hooks/:hook_id" do
-    it "should update an existing project hook" do
+    it "updates an existing project hook" do
       put api("/projects/#{project.id}/hooks/#{hook.id}", user),
         url: 'http://example.org', push_events: false
       expect(response).to have_http_status(200)
@@ -119,46 +119,46 @@
       expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification)
     end
 
-    it "should return 404 error if hook id not found" do
+    it "returns 404 error if hook id not found" do
       put api("/projects/#{project.id}/hooks/1234", user), url: 'http://example.org'
       expect(response).to have_http_status(404)
     end
 
-    it "should return 400 error if url is not given" do
+    it "returns 400 error if url is not given" do
       put api("/projects/#{project.id}/hooks/#{hook.id}", user)
       expect(response).to have_http_status(400)
     end
 
-    it "should return a 422 error if url is not valid" do
+    it "returns a 422 error if url is not valid" do
       put api("/projects/#{project.id}/hooks/#{hook.id}", user), url: 'ftp://example.com'
       expect(response).to have_http_status(422)
     end
   end
 
   describe "DELETE /projects/:id/hooks/:hook_id" do
-    it "should delete hook from project" do
+    it "deletes hook from project" do
       expect do
         delete api("/projects/#{project.id}/hooks/#{hook.id}", user)
       end.to change {project.hooks.count}.by(-1)
       expect(response).to have_http_status(200)
     end
 
-    it "should return success when deleting hook" do
+    it "returns success when deleting hook" do
       delete api("/projects/#{project.id}/hooks/#{hook.id}", user)
       expect(response).to have_http_status(200)
     end
 
-    it "should return a 404 error when deleting non existent hook" do
+    it "returns a 404 error when deleting non existent hook" do
       delete api("/projects/#{project.id}/hooks/42", user)
       expect(response).to have_http_status(404)
     end
 
-    it "should return a 405 error if hook id not given" do
+    it "returns a 405 error if hook id not given" do
       delete api("/projects/#{project.id}/hooks", user)
       expect(response).to have_http_status(405)
     end
 
-    it "shold return a 404 if a user attempts to delete project hooks he/she does not own" do
+    it "returns a 404 if a user attempts to delete project hooks he/she does not own" do
       test_user = create(:user)
       other_project = create(:project)
       other_project.team << [test_user, :master]
diff --git a/spec/requests/api/project_members_spec.rb b/spec/requests/api/project_members_spec.rb
index 9a7c1da44018b4..13cc0d81ac8ecb 100644
--- a/spec/requests/api/project_members_spec.rb
+++ b/spec/requests/api/project_members_spec.rb
@@ -13,7 +13,7 @@
     before { project_member }
     before { project_member2 }
 
-    it "should return project team members" do
+    it "returns project team members" do
       get api("/projects/#{project.id}/members", user)
       expect(response).to have_http_status(200)
       expect(json_response).to be_an Array
@@ -29,7 +29,7 @@
       expect(json_response.first['username']).to eq(user.username)
     end
 
-    it "should return a 404 error if id not found" do
+    it "returns a 404 error if id not found" do
       get api("/projects/9999/members", user)
       expect(response).to have_http_status(404)
     end
@@ -38,21 +38,21 @@
   describe "GET /projects/:id/members/:user_id" do
     before { project_member }
 
-    it "should return project team member" do
+    it "returns project team member" do
       get api("/projects/#{project.id}/members/#{user.id}", user)
       expect(response).to have_http_status(200)
       expect(json_response['username']).to eq(user.username)
       expect(json_response['access_level']).to eq(ProjectMember::MASTER)
     end
 
-    it "should return a 404 error if user id not found" do
+    it "returns a 404 error if user id not found" do
       get api("/projects/#{project.id}/members/1234", user)
       expect(response).to have_http_status(404)
     end
   end
 
   describe "POST /projects/:id/members" do
-    it "should add user to project team" do
+    it "adds user to project team" do
       expect do
         post api("/projects/#{project.id}/members", user), user_id: user2.id, access_level: ProjectMember::DEVELOPER
       end.to change { ProjectMember.count }.by(1)
@@ -62,7 +62,7 @@
       expect(json_response['access_level']).to eq(ProjectMember::DEVELOPER)
     end
 
-    it "should return a 201 status if user is already project member" do
+    it "returns a 201 status if user is already project member" do
       post api("/projects/#{project.id}/members", user),
            user_id: user2.id,
            access_level: ProjectMember::DEVELOPER
@@ -75,17 +75,17 @@
       expect(json_response['access_level']).to eq(ProjectMember::DEVELOPER)
     end
 
-    it "should return a 400 error when user id is not given" do
+    it "returns a 400 error when user id is not given" do
       post api("/projects/#{project.id}/members", user), access_level: ProjectMember::MASTER
       expect(response).to have_http_status(400)
     end
 
-    it "should return a 400 error when access level is not given" do
+    it "returns a 400 error when access level is not given" do
       post api("/projects/#{project.id}/members", user), user_id: user2.id
       expect(response).to have_http_status(400)
     end
 
-    it "should return a 422 error when access level is not known" do
+    it "returns a 422 error when access level is not known" do
       post api("/projects/#{project.id}/members", user), user_id: user2.id, access_level: 1234
       expect(response).to have_http_status(422)
     end
@@ -94,24 +94,24 @@
   describe "PUT /projects/:id/members/:user_id" do
     before { project_member2 }
 
-    it "should update project team member" do
+    it "updates project team member" do
       put api("/projects/#{project.id}/members/#{user3.id}", user), access_level: ProjectMember::MASTER
       expect(response).to have_http_status(200)
       expect(json_response['username']).to eq(user3.username)
       expect(json_response['access_level']).to eq(ProjectMember::MASTER)
     end
 
-    it "should return a 404 error if user_id is not found" do
+    it "returns a 404 error if user_id is not found" do
       put api("/projects/#{project.id}/members/1234", user), access_level: ProjectMember::MASTER
       expect(response).to have_http_status(404)
     end
 
-    it "should return a 400 error when access level is not given" do
+    it "returns a 400 error when access level is not given" do
       put api("/projects/#{project.id}/members/#{user3.id}", user)
       expect(response).to have_http_status(400)
     end
 
-    it "should return a 422 error when access level is not known" do
+    it "returns a 422 error when access level is not known" do
       put api("/projects/#{project.id}/members/#{user3.id}", user), access_level: 123
       expect(response).to have_http_status(422)
     end
@@ -123,13 +123,13 @@
       project_member2
     end
 
-    it "should remove user from project team" do
+    it "removes user from project team" do
       expect do
         delete api("/projects/#{project.id}/members/#{user3.id}", user)
       end.to change { ProjectMember.count }.by(-1)
     end
 
-    it "should return 200 if team member is not part of a project" do
+    it "returns 200 if team member is not part of a project" do
       delete api("/projects/#{project.id}/members/#{user3.id}", user)
       expect do
         delete api("/projects/#{project.id}/members/#{user3.id}", user)
@@ -137,13 +137,13 @@
       expect(response).to have_http_status(200)
     end
 
-    it "should return 200 if team member already removed" do
+    it "returns 200 if team member already removed" do
       delete api("/projects/#{project.id}/members/#{user3.id}", user)
       delete api("/projects/#{project.id}/members/#{user3.id}", user)
       expect(response).to have_http_status(200)
     end
 
-    it "should return 200 OK when the user was not member" do
+    it "returns 200 OK when the user was not member" do
       expect do
         delete api("/projects/#{project.id}/members/1000000", user)
       end.to change { ProjectMember.count }.by(0)
diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb
index 4ebde20194115c..42757ff21b081d 100644
--- a/spec/requests/api/project_snippets_spec.rb
+++ b/spec/requests/api/project_snippets_spec.rb
@@ -17,7 +17,7 @@
   end
 
   describe 'GET /projects/:project_id/snippets/' do
-    it 'all snippets available to team member' do
+    it 'returns all snippets available to team member' do
       project = create(:project, :public)
       user = create(:user)
       project.team << [user, :developer]
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 6b78326213b16a..4742b3d0e374e6 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -43,14 +43,14 @@
     before { project }
 
     context 'when unauthenticated' do
-      it 'should return authentication error' do
+      it 'returns authentication error' do
         get api('/projects')
         expect(response).to have_http_status(401)
       end
     end
 
     context 'when authenticated' do
-      it 'should return an array of projects' do
+      it 'returns an array of projects' do
         get api('/projects', user)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -58,21 +58,21 @@
         expect(json_response.first['owner']['username']).to eq(user.username)
       end
 
-      it 'should include the project labels as the tag_list' do
+      it 'includes the project labels as the tag_list' do
         get api('/projects', user)
         expect(response.status).to eq 200
         expect(json_response).to be_an Array
         expect(json_response.first.keys).to include('tag_list')
       end
 
-      it 'should include open_issues_count' do
+      it 'includes open_issues_count' do
         get api('/projects', user)
         expect(response.status).to eq 200
         expect(json_response).to be_an Array
         expect(json_response.first.keys).to include('open_issues_count')
       end
 
-      it 'should not include open_issues_count' do
+      it 'does not include open_issues_count' do
         project.update_attributes( { issues_enabled: false } )
 
         get api('/projects', user)
@@ -94,7 +94,7 @@
       end
 
       context 'and using search' do
-        it 'should return searched project' do
+        it 'returns searched project' do
           get api('/projects', user), { search: project.name }
           expect(response).to have_http_status(200)
           expect(json_response).to be_an Array
@@ -103,21 +103,21 @@
       end
 
       context 'and using the visibility filter' do
-        it 'should filter based on private visibility param' do
+        it 'filters based on private visibility param' do
           get api('/projects', user), { visibility: 'private' }
           expect(response).to have_http_status(200)
           expect(json_response).to be_an Array
           expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::PRIVATE).count)
         end
 
-        it 'should filter based on internal visibility param' do
+        it 'filters based on internal visibility param' do
           get api('/projects', user), { visibility: 'internal' }
           expect(response).to have_http_status(200)
           expect(json_response).to be_an Array
           expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::INTERNAL).count)
         end
 
-        it 'should filter based on public visibility param' do
+        it 'filters based on public visibility param' do
           get api('/projects', user), { visibility: 'public' }
           expect(response).to have_http_status(200)
           expect(json_response).to be_an Array
@@ -131,7 +131,7 @@
           project3
         end
 
-        it 'should return the correct order when sorted by id' do
+        it 'returns the correct order when sorted by id' do
           get api('/projects', user), { order_by: 'id', sort: 'desc' }
           expect(response).to have_http_status(200)
           expect(json_response).to be_an Array
@@ -145,21 +145,21 @@
     before { project }
 
     context 'when unauthenticated' do
-      it 'should return authentication error' do
+      it 'returns authentication error' do
         get api('/projects/all')
         expect(response).to have_http_status(401)
       end
     end
 
     context 'when authenticated as regular user' do
-      it 'should return authentication error' do
+      it 'returns authentication error' do
         get api('/projects/all', user)
         expect(response).to have_http_status(403)
       end
     end
 
     context 'when authenticated as admin' do
-      it 'should return an array of all projects' do
+      it 'returns an array of all projects' do
         get api('/projects/all', admin)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -183,7 +183,7 @@
       user3.update_attributes(starred_projects: [project, project2, project3, public_project])
     end
 
-    it 'should return the starred projects viewable by the user' do
+    it 'returns the starred projects viewable by the user' do
       get api('/projects/starred', user3)
       expect(response).to have_http_status(200)
       expect(json_response).to be_an Array
@@ -193,7 +193,7 @@
 
   describe 'POST /projects' do
     context 'maximum number of projects reached' do
-      it 'should not create new project and respond with 403' do
+      it 'does not create new project and respond with 403' do
         allow_any_instance_of(User).to receive(:projects_limit_left).and_return(0)
         expect { post api('/projects', user2), name: 'foo' }.
           to change {Project.count}.by(0)
@@ -201,24 +201,24 @@
       end
     end
 
-    it 'should create new project without path and return 201' do
+    it 'creates new project without path and return 201' do
       expect { post api('/projects', user), name: 'foo' }.
         to change { Project.count }.by(1)
       expect(response).to have_http_status(201)
     end
 
-    it 'should create last project before reaching project limit' do
+    it 'creates last project before reaching project limit' do
       allow_any_instance_of(User).to receive(:projects_limit_left).and_return(1)
       post api('/projects', user2), name: 'foo'
       expect(response).to have_http_status(201)
     end
 
-    it 'should not create new project without name and return 400' do
+    it 'does not create new project without name and return 400' do
       expect { post api('/projects', user) }.not_to change { Project.count }
       expect(response).to have_http_status(400)
     end
 
-    it "should assign attributes to project" do
+    it "assigns attributes to project" do
       project = attributes_for(:project, {
         path: 'camelCasePath',
         description: FFaker::Lorem.sentence,
@@ -234,42 +234,42 @@
       end
     end
 
-    it 'should set a project as public' do
+    it 'sets a project as public' do
       project = attributes_for(:project, :public)
       post api('/projects', user), project
       expect(json_response['public']).to be_truthy
       expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC)
     end
 
-    it 'should set a project as public using :public' do
+    it 'sets a project as public using :public' do
       project = attributes_for(:project, { public: true })
       post api('/projects', user), project
       expect(json_response['public']).to be_truthy
       expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC)
     end
 
-    it 'should set a project as internal' do
+    it 'sets a project as internal' do
       project = attributes_for(:project, :internal)
       post api('/projects', user), project
       expect(json_response['public']).to be_falsey
       expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL)
     end
 
-    it 'should set a project as internal overriding :public' do
+    it 'sets a project as internal overriding :public' do
       project = attributes_for(:project, :internal, { public: true })
       post api('/projects', user), project
       expect(json_response['public']).to be_falsey
       expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL)
     end
 
-    it 'should set a project as private' do
+    it 'sets a project as private' do
       project = attributes_for(:project, :private)
       post api('/projects', user), project
       expect(json_response['public']).to be_falsey
       expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
     end
 
-    it 'should set a project as private using :public' do
+    it 'sets a project as private using :public' do
       project = attributes_for(:project, { public: false })
       post api('/projects', user), project
       expect(json_response['public']).to be_falsey
@@ -282,7 +282,7 @@
         stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
       end
 
-      it 'should not allow a non-admin to use a restricted visibility level' do
+      it 'does not allow a non-admin to use a restricted visibility level' do
         post api('/projects', user), @project
 
         expect(response).to have_http_status(400)
@@ -291,7 +291,7 @@
         )
       end
 
-      it 'should allow an admin to override restricted visibility settings' do
+      it 'allows an admin to override restricted visibility settings' do
         post api('/projects', admin), @project
         expect(json_response['public']).to be_truthy
         expect(json_response['visibility_level']).to(
@@ -310,7 +310,7 @@
       expect(response).to have_http_status(201)
     end
 
-    it 'should respond with 400 on failure and not project' do
+    it 'responds with 400 on failure and not project' do
       expect { post api("/projects/user/#{user.id}", admin) }.
         not_to change { Project.count }
 
@@ -327,7 +327,7 @@
       ])
     end
 
-    it 'should assign attributes to project' do
+    it 'assigns attributes to project' do
       project = attributes_for(:project, {
         description: FFaker::Lorem.sentence,
         issues_enabled: false,
@@ -343,42 +343,42 @@
       end
     end
 
-    it 'should set a project as public' do
+    it 'sets a project as public' do
       project = attributes_for(:project, :public)
       post api("/projects/user/#{user.id}", admin), project
       expect(json_response['public']).to be_truthy
       expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC)
     end
 
-    it 'should set a project as public using :public' do
+    it 'sets a project as public using :public' do
       project = attributes_for(:project, { public: true })
       post api("/projects/user/#{user.id}", admin), project
       expect(json_response['public']).to be_truthy
       expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC)
     end
 
-    it 'should set a project as internal' do
+    it 'sets a project as internal' do
       project = attributes_for(:project, :internal)
       post api("/projects/user/#{user.id}", admin), project
       expect(json_response['public']).to be_falsey
       expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL)
     end
 
-    it 'should set a project as internal overriding :public' do
+    it 'sets a project as internal overriding :public' do
       project = attributes_for(:project, :internal, { public: true })
       post api("/projects/user/#{user.id}", admin), project
       expect(json_response['public']).to be_falsey
       expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL)
     end
 
-    it 'should set a project as private' do
+    it 'sets a project as private' do
       project = attributes_for(:project, :private)
       post api("/projects/user/#{user.id}", admin), project
       expect(json_response['public']).to be_falsey
       expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
     end
 
-    it 'should set a project as private using :public' do
+    it 'sets a project as private using :public' do
       project = attributes_for(:project, { public: false })
       post api("/projects/user/#{user.id}", admin), project
       expect(json_response['public']).to be_falsey
@@ -446,25 +446,25 @@
       expect(json_response['shared_with_groups'][0]['group_access_level']).to eq(link.group_access)
     end
 
-    it 'should return a project by path name' do
+    it 'returns a project by path name' do
       get api("/projects/#{project.id}", user)
       expect(response).to have_http_status(200)
       expect(json_response['name']).to eq(project.name)
     end
 
-    it 'should return a 404 error if not found' do
+    it 'returns a 404 error if not found' do
       get api('/projects/42', user)
       expect(response).to have_http_status(404)
       expect(json_response['message']).to eq('404 Project Not Found')
     end
 
-    it 'should return a 404 error if user is not a member' do
+    it 'returns a 404 error if user is not a member' do
       other_user = create(:user)
       get api("/projects/#{project.id}", other_user)
       expect(response).to have_http_status(404)
     end
 
-    it 'should handle users with dots' do
+    it 'handles users with dots' do
       dot_user = create(:user, username: 'dot.user')
       project = create(:project, creator_id: dot_user.id, namespace: dot_user.namespace)
 
@@ -504,7 +504,7 @@
 
         before { project2.group.add_owner(user) }
 
-        it 'should set the owner and return 200' do
+        it 'sets the owner and return 200' do
           get api("/projects/#{project2.id}", user)
 
           expect(response).to have_http_status(200)
@@ -545,13 +545,13 @@
       end
     end
 
-    it 'should return a 404 error if not found' do
+    it 'returns a 404 error if not found' do
       get api('/projects/42/events', user)
       expect(response).to have_http_status(404)
       expect(json_response['message']).to eq('404 Project Not Found')
     end
 
-    it 'should return a 404 error if user is not a member' do
+    it 'returns a 404 error if user is not a member' do
       other_user = create(:user)
       get api("/projects/#{project.id}/events", other_user)
       expect(response).to have_http_status(404)
@@ -561,7 +561,7 @@
   describe 'GET /projects/:id/snippets' do
     before { snippet }
 
-    it 'should return an array of project snippets' do
+    it 'returns an array of project snippets' do
       get api("/projects/#{project.id}/snippets", user)
       expect(response).to have_http_status(200)
       expect(json_response).to be_an Array
@@ -570,20 +570,20 @@
   end
 
   describe 'GET /projects/:id/snippets/:snippet_id' do
-    it 'should return a project snippet' do
+    it 'returns a project snippet' do
       get api("/projects/#{project.id}/snippets/#{snippet.id}", user)
       expect(response).to have_http_status(200)
       expect(json_response['title']).to eq(snippet.title)
     end
 
-    it 'should return a 404 error if snippet id not found' do
+    it 'returns a 404 error if snippet id not found' do
       get api("/projects/#{project.id}/snippets/1234", user)
       expect(response).to have_http_status(404)
     end
   end
 
   describe 'POST /projects/:id/snippets' do
-    it 'should create a new project snippet' do
+    it 'creates a new project snippet' do
       post api("/projects/#{project.id}/snippets", user),
         title: 'api test', file_name: 'sample.rb', code: 'test',
         visibility_level: '0'
@@ -591,14 +591,14 @@
       expect(json_response['title']).to eq('api test')
     end
 
-    it 'should return a 400 error if invalid snippet is given' do
+    it 'returns a 400 error if invalid snippet is given' do
       post api("/projects/#{project.id}/snippets", user)
       expect(status).to eq(400)
     end
   end
 
   describe 'PUT /projects/:id/snippets/:snippet_id' do
-    it 'should update an existing project snippet' do
+    it 'updates an existing project snippet' do
       put api("/projects/#{project.id}/snippets/#{snippet.id}", user),
         code: 'updated code'
       expect(response).to have_http_status(200)
@@ -606,7 +606,7 @@
       expect(snippet.reload.content).to eq('updated code')
     end
 
-    it 'should update an existing project snippet with new title' do
+    it 'updates an existing project snippet with new title' do
       put api("/projects/#{project.id}/snippets/#{snippet.id}", user),
         title: 'other api test'
       expect(response).to have_http_status(200)
@@ -617,31 +617,31 @@
   describe 'DELETE /projects/:id/snippets/:snippet_id' do
     before { snippet }
 
-    it 'should delete existing project snippet' do
+    it 'deletes existing project snippet' do
       expect do
         delete api("/projects/#{project.id}/snippets/#{snippet.id}", user)
       end.to change { Snippet.count }.by(-1)
       expect(response).to have_http_status(200)
     end
 
-    it 'should return 404 when deleting unknown snippet id' do
+    it 'returns 404 when deleting unknown snippet id' do
       delete api("/projects/#{project.id}/snippets/1234", user)
       expect(response).to have_http_status(404)
     end
   end
 
   describe 'GET /projects/:id/snippets/:snippet_id/raw' do
-    it 'should get a raw project snippet' do
+    it 'gets a raw project snippet' do
       get api("/projects/#{project.id}/snippets/#{snippet.id}/raw", user)
       expect(response).to have_http_status(200)
     end
 
-    it 'should return a 404 error if raw project snippet not found' do
+    it 'returns a 404 error if raw project snippet not found' do
       get api("/projects/#{project.id}/snippets/5555/raw", user)
       expect(response).to have_http_status(404)
     end
   end
-  
+
   describe :fork_admin do
     let(:project_fork_target) { create(:project) }
     let(:project_fork_source) { create(:project, :public) }
@@ -649,12 +649,12 @@
     describe 'POST /projects/:id/fork/:forked_from_id' do
       let(:new_project_fork_source) { create(:project, :public) }
 
-      it "shouldn't available for non admin users" do
+      it "is not available for non admin users" do
         post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", user)
         expect(response).to have_http_status(403)
       end
 
-      it 'should allow project to be forked from an existing project' do
+      it 'allows project to be forked from an existing project' do
         expect(project_fork_target.forked?).not_to be_truthy
         post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin)
         expect(response).to have_http_status(201)
@@ -664,12 +664,12 @@
         expect(project_fork_target.forked?).to be_truthy
       end
 
-      it 'should fail if forked_from project which does not exist' do
+      it 'fails if forked_from project which does not exist' do
         post api("/projects/#{project_fork_target.id}/fork/9999", admin)
         expect(response).to have_http_status(404)
       end
 
-      it 'should fail with 409 if already forked' do
+      it 'fails with 409 if already forked' do
         post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin)
         project_fork_target.reload
         expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id)
@@ -682,7 +682,7 @@
     end
 
     describe 'DELETE /projects/:id/fork' do
-      it "shouldn't be visible to users outside group" do
+      it "is not visible to users outside group" do
         delete api("/projects/#{project_fork_target.id}/fork", user)
         expect(response).to have_http_status(404)
       end
@@ -695,12 +695,12 @@
           project_fork_target.group.add_developer user2
         end
 
-        it 'should be forbidden to non-owner users' do
+        it 'is forbidden to non-owner users' do
           delete api("/projects/#{project_fork_target.id}/fork", user2)
           expect(response).to have_http_status(403)
         end
 
-        it 'should make forked project unforked' do
+        it 'makes forked project unforked' do
           post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin)
           project_fork_target.reload
           expect(project_fork_target.forked_from_project).not_to be_nil
@@ -712,7 +712,7 @@
           expect(project_fork_target.forked?).not_to be_truthy
         end
 
-        it 'should be idempotent if not forked' do
+        it 'is idempotent if not forked' do
           expect(project_fork_target.forked_from_project).to be_nil
           delete api("/projects/#{project_fork_target.id}/fork", admin)
           expect(response).to have_http_status(200)
@@ -725,7 +725,7 @@
   describe "POST /projects/:id/share" do
     let(:group) { create(:group) }
 
-    it "should share project with group" do
+    it "shares project with group" do
       expect do
         post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER
       end.to change { ProjectGroupLink.count }.by(1)
@@ -735,23 +735,23 @@
       expect(json_response['group_access']).to eq Gitlab::Access::DEVELOPER
     end
 
-    it "should return a 400 error when group id is not given" do
+    it "returns a 400 error when group id is not given" do
       post api("/projects/#{project.id}/share", user), group_access: Gitlab::Access::DEVELOPER
       expect(response.status).to eq 400
     end
 
-    it "should return a 400 error when access level is not given" do
+    it "returns a 400 error when access level is not given" do
       post api("/projects/#{project.id}/share", user), group_id: group.id
       expect(response.status).to eq 400
     end
 
-    it "should return a 400 error when sharing is disabled" do
+    it "returns a 400 error when sharing is disabled" do
       project.namespace.update(share_with_group_lock: true)
       post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER
       expect(response.status).to eq 400
     end
 
-    it "should return a 409 error when wrong params passed" do
+    it "returns a 409 error when wrong params passed" do
       post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: 1234
       expect(response.status).to eq 409
       expect(json_response['message']).to eq 'Group access is not included in the list'
@@ -771,14 +771,14 @@
     let!(:unfound_public)   { create(:empty_project, :public, name: 'unfound public') }
 
     context 'when unauthenticated' do
-      it 'should return authentication error' do
+      it 'returns authentication error' do
         get api("/projects/search/#{query}")
         expect(response).to have_http_status(401)
       end
     end
 
     context 'when authenticated' do
-      it 'should return an array of projects' do
+      it 'returns an array of projects' do
         get api("/projects/search/#{query}", user)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -788,7 +788,7 @@
     end
 
     context 'when authenticated as a different user' do
-      it 'should return matching public projects' do
+      it 'returns matching public projects' do
         get api("/projects/search/#{query}", user2)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -809,7 +809,7 @@
     before { project_member2 }
 
     context 'when unauthenticated' do
-      it 'should return authentication error' do
+      it 'returns authentication error' do
         project_param = { name: 'bar' }
         put api("/projects/#{project.id}"), project_param
         expect(response).to have_http_status(401)
@@ -817,7 +817,7 @@
     end
 
     context 'when authenticated as project owner' do
-      it 'should update name' do
+      it 'updates name' do
         project_param = { name: 'bar' }
         put api("/projects/#{project.id}", user), project_param
         expect(response).to have_http_status(200)
@@ -826,7 +826,7 @@
         end
       end
 
-      it 'should update visibility_level' do
+      it 'updates visibility_level' do
         project_param = { visibility_level: 20 }
         put api("/projects/#{project3.id}", user), project_param
         expect(response).to have_http_status(200)
@@ -835,7 +835,7 @@
         end
       end
 
-      it 'should update visibility_level from public to private' do
+      it 'updates visibility_level from public to private' do
         project3.update_attributes({ visibility_level: Gitlab::VisibilityLevel::PUBLIC })
 
         project_param = { public: false }
@@ -847,14 +847,14 @@
         expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
       end
 
-      it 'should not update name to existing name' do
+      it 'does not update name to existing name' do
         project_param = { name: project3.name }
         put api("/projects/#{project.id}", user), project_param
         expect(response).to have_http_status(400)
         expect(json_response['message']['name']).to eq(['has already been taken'])
       end
 
-      it 'should update path & name to existing path & name in different namespace' do
+      it 'updates path & name to existing path & name in different namespace' do
         project_param = { path: project4.path, name: project4.name }
         put api("/projects/#{project3.id}", user), project_param
         expect(response).to have_http_status(200)
@@ -865,7 +865,7 @@
     end
 
     context 'when authenticated as project master' do
-      it 'should update path' do
+      it 'updates path' do
         project_param = { path: 'bar' }
         put api("/projects/#{project3.id}", user4), project_param
         expect(response).to have_http_status(200)
@@ -874,7 +874,7 @@
         end
       end
 
-      it 'should update other attributes' do
+      it 'updates other attributes' do
         project_param = { issues_enabled: true,
                           wiki_enabled: true,
                           snippets_enabled: true,
@@ -888,20 +888,20 @@
         end
       end
 
-      it 'should not update path to existing path' do
+      it 'does not update path to existing path' do
         project_param = { path: project.path }
         put api("/projects/#{project3.id}", user4), project_param
         expect(response).to have_http_status(400)
         expect(json_response['message']['path']).to eq(['has already been taken'])
       end
 
-      it 'should not update name' do
+      it 'does not update name' do
         project_param = { name: 'bar' }
         put api("/projects/#{project3.id}", user4), project_param
         expect(response).to have_http_status(403)
       end
 
-      it 'should not update visibility_level' do
+      it 'does not update visibility_level' do
         project_param = { visibility_level: 20 }
         put api("/projects/#{project3.id}", user4), project_param
         expect(response).to have_http_status(403)
@@ -909,7 +909,7 @@
     end
 
     context 'when authenticated as project developer' do
-      it 'should not update other attributes' do
+      it 'does not update other attributes' do
         project_param = { path: 'bar',
                           issues_enabled: true,
                           wiki_enabled: true,
@@ -1044,36 +1044,36 @@
 
   describe 'DELETE /projects/:id' do
     context 'when authenticated as user' do
-      it 'should remove project' do
+      it 'removes project' do
         delete api("/projects/#{project.id}", user)
         expect(response).to have_http_status(200)
       end
 
-      it 'should not remove a project if not an owner' do
+      it 'does not remove a project if not an owner' do
         user3 = create(:user)
         project.team << [user3, :developer]
         delete api("/projects/#{project.id}", user3)
         expect(response).to have_http_status(403)
       end
 
-      it 'should not remove a non existing project' do
+      it 'does not remove a non existing project' do
         delete api('/projects/1328', user)
         expect(response).to have_http_status(404)
       end
 
-      it 'should not remove a project not attached to user' do
+      it 'does not remove a project not attached to user' do
         delete api("/projects/#{project.id}", user2)
         expect(response).to have_http_status(404)
       end
     end
 
     context 'when authenticated as admin' do
-      it 'should remove any existing project' do
+      it 'removes any existing project' do
         delete api("/projects/#{project.id}", admin)
         expect(response).to have_http_status(200)
       end
 
-      it 'should not remove a non existing project' do
+      it 'does not remove a non existing project' do
         delete api('/projects/1328', admin)
         expect(response).to have_http_status(404)
       end
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index 5890e9c9d3d97f..80a856a6e9095d 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -16,7 +16,7 @@
     context "authorized user" do
       before { project.team << [user2, :reporter] }
 
-      it "should return project commits" do
+      it "returns project commits" do
         get api("/projects/#{project.id}/repository/tree", user)
         expect(response).to have_http_status(200)
 
@@ -26,7 +26,7 @@
         expect(json_response.first['mode']).to eq('040000')
       end
 
-      it 'should return a 404 for unknown ref' do
+      it 'returns a 404 for unknown ref' do
         get api("/projects/#{project.id}/repository/tree?ref_name=foo", user)
         expect(response).to have_http_status(404)
 
@@ -36,7 +36,7 @@
     end
 
     context "unauthorized user" do
-      it "should not return project commits" do
+      it "does not return project commits" do
         get api("/projects/#{project.id}/repository/tree")
         expect(response).to have_http_status(401)
       end
@@ -44,41 +44,41 @@
   end
 
   describe "GET /projects/:id/repository/blobs/:sha" do
-    it "should get the raw file contents" do
+    it "gets the raw file contents" do
       get api("/projects/#{project.id}/repository/blobs/master?filepath=README.md", user)
       expect(response).to have_http_status(200)
     end
 
-    it "should return 404 for invalid branch_name" do
+    it "returns 404 for invalid branch_name" do
       get api("/projects/#{project.id}/repository/blobs/invalid_branch_name?filepath=README.md", user)
       expect(response).to have_http_status(404)
     end
 
-    it "should return 404 for invalid file" do
+    it "returns 404 for invalid file" do
       get api("/projects/#{project.id}/repository/blobs/master?filepath=README.invalid", user)
       expect(response).to have_http_status(404)
     end
 
-    it "should return a 400 error if filepath is missing" do
+    it "returns a 400 error if filepath is missing" do
       get api("/projects/#{project.id}/repository/blobs/master", user)
       expect(response).to have_http_status(400)
     end
   end
 
   describe "GET /projects/:id/repository/commits/:sha/blob" do
-    it "should get the raw file contents" do
+    it "gets the raw file contents" do
       get api("/projects/#{project.id}/repository/commits/master/blob?filepath=README.md", user)
       expect(response).to have_http_status(200)
     end
   end
 
   describe "GET /projects/:id/repository/raw_blobs/:sha" do
-    it "should get the raw file contents" do
+    it "gets the raw file contents" do
       get api("/projects/#{project.id}/repository/raw_blobs/#{sample_blob.oid}", user)
       expect(response).to have_http_status(200)
     end
 
-    it 'should return a 404 for unknown blob' do
+    it 'returns a 404 for unknown blob' do
       get api("/projects/#{project.id}/repository/raw_blobs/123456", user)
       expect(response).to have_http_status(404)
 
@@ -88,7 +88,7 @@
   end
 
   describe "GET /projects/:id/repository/archive(.:format)?:sha" do
-    it "should get the archive" do
+    it "gets the archive" do
       get api("/projects/#{project.id}/repository/archive", user)
       repo_name = project.repository.name.gsub("\.git", "")
       expect(response).to have_http_status(200)
@@ -97,7 +97,7 @@
       expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.gz/)
     end
 
-    it "should get the archive.zip" do
+    it "gets the archive.zip" do
       get api("/projects/#{project.id}/repository/archive.zip", user)
       repo_name = project.repository.name.gsub("\.git", "")
       expect(response).to have_http_status(200)
@@ -106,7 +106,7 @@
       expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.zip/)
     end
 
-    it "should get the archive.tar.bz2" do
+    it "gets the archive.tar.bz2" do
       get api("/projects/#{project.id}/repository/archive.tar.bz2", user)
       repo_name = project.repository.name.gsub("\.git", "")
       expect(response).to have_http_status(200)
@@ -115,28 +115,28 @@
       expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.bz2/)
     end
 
-    it "should return 404 for invalid sha" do
+    it "returns 404 for invalid sha" do
       get api("/projects/#{project.id}/repository/archive/?sha=xxx", user)
       expect(response).to have_http_status(404)
     end
   end
 
   describe 'GET /projects/:id/repository/compare' do
-    it "should compare branches" do
+    it "compares branches" do
       get api("/projects/#{project.id}/repository/compare", user), from: 'master', to: 'feature'
       expect(response).to have_http_status(200)
       expect(json_response['commits']).to be_present
       expect(json_response['diffs']).to be_present
     end
 
-    it "should compare tags" do
+    it "compares tags" do
       get api("/projects/#{project.id}/repository/compare", user), from: 'v1.0.0', to: 'v1.1.0'
       expect(response).to have_http_status(200)
       expect(json_response['commits']).to be_present
       expect(json_response['diffs']).to be_present
     end
 
-    it "should compare commits" do
+    it "compares commits" do
       get api("/projects/#{project.id}/repository/compare", user), from: sample_commit.id, to: sample_commit.parent_id
       expect(response).to have_http_status(200)
       expect(json_response['commits']).to be_empty
@@ -144,14 +144,14 @@
       expect(json_response['compare_same_ref']).to be_falsey
     end
 
-    it "should compare commits in reverse order" do
+    it "compares commits in reverse order" do
       get api("/projects/#{project.id}/repository/compare", user), from: sample_commit.parent_id, to: sample_commit.id
       expect(response).to have_http_status(200)
       expect(json_response['commits']).to be_present
       expect(json_response['diffs']).to be_present
     end
 
-    it "should compare same refs" do
+    it "compares same refs" do
       get api("/projects/#{project.id}/repository/compare", user), from: 'master', to: 'master'
       expect(response).to have_http_status(200)
       expect(json_response['commits']).to be_empty
@@ -161,7 +161,7 @@
   end
 
   describe 'GET /projects/:id/repository/contributors' do
-    it 'should return valid data' do
+    it 'returns valid data' do
       get api("/projects/#{project.id}/repository/contributors", user)
       expect(response).to have_http_status(200)
       expect(json_response).to be_an Array
diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb
index 00a3c917b6a3e3..f46f016135ee0b 100644
--- a/spec/requests/api/runners_spec.rb
+++ b/spec/requests/api/runners_spec.rb
@@ -35,7 +35,7 @@
 
   describe 'GET /runners' do
     context 'authorized user' do
-      it 'should return user available runners' do
+      it 'returns user available runners' do
         get api('/runners', user)
         shared = json_response.any?{ |r| r['is_shared'] }
 
@@ -44,7 +44,7 @@
         expect(shared).to be_falsey
       end
 
-      it 'should filter runners by scope' do
+      it 'filters runners by scope' do
         get api('/runners?scope=active', user)
         shared = json_response.any?{ |r| r['is_shared'] }
 
@@ -53,14 +53,14 @@
         expect(shared).to be_falsey
       end
 
-      it 'should avoid filtering if scope is invalid' do
+      it 'avoids filtering if scope is invalid' do
         get api('/runners?scope=unknown', user)
         expect(response).to have_http_status(400)
       end
     end
 
     context 'unauthorized user' do
-      it 'should not return runners' do
+      it 'does not return runners' do
         get api('/runners')
 
         expect(response).to have_http_status(401)
@@ -71,7 +71,7 @@
   describe 'GET /runners/all' do
     context 'authorized user' do
       context 'with admin privileges' do
-        it 'should return all runners' do
+        it 'returns all runners' do
           get api('/runners/all', admin)
           shared = json_response.any?{ |r| r['is_shared'] }
 
@@ -82,14 +82,14 @@
       end
 
       context 'without admin privileges' do
-        it 'should not return runners list' do
+        it 'does not return runners list' do
           get api('/runners/all', user)
 
           expect(response).to have_http_status(403)
         end
       end
 
-      it 'should filter runners by scope' do
+      it 'filters runners by scope' do
         get api('/runners/all?scope=specific', admin)
         shared = json_response.any?{ |r| r['is_shared'] }
 
@@ -98,14 +98,14 @@
         expect(shared).to be_falsey
       end
 
-      it 'should avoid filtering if scope is invalid' do
+      it 'avoids filtering if scope is invalid' do
         get api('/runners?scope=unknown', admin)
         expect(response).to have_http_status(400)
       end
     end
 
     context 'unauthorized user' do
-      it 'should not return runners' do
+      it 'does not return runners' do
         get api('/runners')
 
         expect(response).to have_http_status(401)
@@ -116,7 +116,7 @@
   describe 'GET /runners/:id' do
     context 'admin user' do
       context 'when runner is shared' do
-        it "should return runner's details" do
+        it "returns runner's details" do
           get api("/runners/#{shared_runner.id}", admin)
 
           expect(response).to have_http_status(200)
@@ -125,7 +125,7 @@
       end
 
       context 'when runner is not shared' do
-        it "should return runner's details" do
+        it "returns runner's details" do
           get api("/runners/#{specific_runner.id}", admin)
 
           expect(response).to have_http_status(200)
@@ -133,7 +133,7 @@
         end
       end
 
-      it 'should return 404 if runner does not exists' do
+      it 'returns 404 if runner does not exists' do
         get api('/runners/9999', admin)
 
         expect(response).to have_http_status(404)
@@ -142,7 +142,7 @@
 
     context "runner project's administrative user" do
       context 'when runner is not shared' do
-        it "should return runner's details" do
+        it "returns runner's details" do
           get api("/runners/#{specific_runner.id}", user)
 
           expect(response).to have_http_status(200)
@@ -151,7 +151,7 @@
       end
 
       context 'when runner is shared' do
-        it "should return runner's details" do
+        it "returns runner's details" do
           get api("/runners/#{shared_runner.id}", user)
 
           expect(response).to have_http_status(200)
@@ -161,7 +161,7 @@
     end
 
     context 'other authorized user' do
-      it "should not return runner's details" do
+      it "does not return runner's details" do
         get api("/runners/#{specific_runner.id}", user2)
 
         expect(response).to have_http_status(403)
@@ -169,7 +169,7 @@
     end
 
     context 'unauthorized user' do
-      it "should not return runner's details" do
+      it "does not return runner's details" do
         get api("/runners/#{specific_runner.id}")
 
         expect(response).to have_http_status(401)
@@ -180,7 +180,7 @@
   describe 'PUT /runners/:id' do
     context 'admin user' do
       context 'when runner is shared' do
-        it 'should update runner' do
+        it 'updates runner' do
           description = shared_runner.description
           active = shared_runner.active
 
@@ -201,7 +201,7 @@
       end
 
       context 'when runner is not shared' do
-        it 'should update runner' do
+        it 'updates runner' do
           description = specific_runner.description
           update_runner(specific_runner.id, admin, description: 'test')
           specific_runner.reload
@@ -212,7 +212,7 @@
         end
       end
 
-      it 'should return 404 if runner does not exists' do
+      it 'returns 404 if runner does not exists' do
         update_runner(9999, admin, description: 'test')
 
         expect(response).to have_http_status(404)
@@ -225,7 +225,7 @@ def update_runner(id, user, args)
 
     context 'authorized user' do
       context 'when runner is shared' do
-        it 'should not update runner' do
+        it 'does not update runner' do
           put api("/runners/#{shared_runner.id}", user)
 
           expect(response).to have_http_status(403)
@@ -233,13 +233,13 @@ def update_runner(id, user, args)
       end
 
       context 'when runner is not shared' do
-        it 'should not update runner without access to it' do
+        it 'does not update runner without access to it' do
           put api("/runners/#{specific_runner.id}", user2)
 
           expect(response).to have_http_status(403)
         end
 
-        it 'should update runner with access to it' do
+        it 'updates runner with access to it' do
           description = specific_runner.description
           put api("/runners/#{specific_runner.id}", admin), description: 'test'
           specific_runner.reload
@@ -252,7 +252,7 @@ def update_runner(id, user, args)
     end
 
     context 'unauthorized user' do
-      it 'should not delete runner' do
+      it 'does not delete runner' do
         put api("/runners/#{specific_runner.id}")
 
         expect(response).to have_http_status(401)
@@ -263,7 +263,7 @@ def update_runner(id, user, args)
   describe 'DELETE /runners/:id' do
     context 'admin user' do
       context 'when runner is shared' do
-        it 'should delete runner' do
+        it 'deletes runner' do
           expect do
             delete api("/runners/#{shared_runner.id}", admin)
           end.to change{ Ci::Runner.shared.count }.by(-1)
@@ -272,14 +272,14 @@ def update_runner(id, user, args)
       end
 
       context 'when runner is not shared' do
-        it 'should delete unused runner' do
+        it 'deletes unused runner' do
           expect do
             delete api("/runners/#{unused_specific_runner.id}", admin)
           end.to change{ Ci::Runner.specific.count }.by(-1)
           expect(response).to have_http_status(200)
         end
 
-        it 'should delete used runner' do
+        it 'deletes used runner' do
           expect do
             delete api("/runners/#{specific_runner.id}", admin)
           end.to change{ Ci::Runner.specific.count }.by(-1)
@@ -287,7 +287,7 @@ def update_runner(id, user, args)
         end
       end
 
-      it 'should return 404 if runner does not exists' do
+      it 'returns 404 if runner does not exists' do
         delete api('/runners/9999', admin)
 
         expect(response).to have_http_status(404)
@@ -296,24 +296,24 @@ def update_runner(id, user, args)
 
     context 'authorized user' do
       context 'when runner is shared' do
-        it 'should not delete runner' do
+        it 'does not delete runner' do
           delete api("/runners/#{shared_runner.id}", user)
           expect(response).to have_http_status(403)
         end
       end
 
       context 'when runner is not shared' do
-        it 'should not delete runner without access to it' do
+        it 'does not delete runner without access to it' do
           delete api("/runners/#{specific_runner.id}", user2)
           expect(response).to have_http_status(403)
         end
 
-        it 'should not delete runner with more than one associated project' do
+        it 'does not delete runner with more than one associated project' do
           delete api("/runners/#{two_projects_runner.id}", user)
           expect(response).to have_http_status(403)
         end
 
-        it 'should delete runner for one owned project' do
+        it 'deletes runner for one owned project' do
           expect do
             delete api("/runners/#{specific_runner.id}", user)
           end.to change{ Ci::Runner.specific.count }.by(-1)
@@ -323,7 +323,7 @@ def update_runner(id, user, args)
     end
 
     context 'unauthorized user' do
-      it 'should not delete runner' do
+      it 'does not delete runner' do
         delete api("/runners/#{specific_runner.id}")
 
         expect(response).to have_http_status(401)
@@ -333,7 +333,7 @@ def update_runner(id, user, args)
 
   describe 'GET /projects/:id/runners' do
     context 'authorized user with master privileges' do
-      it "should return project's runners" do
+      it "returns project's runners" do
         get api("/projects/#{project.id}/runners", user)
         shared = json_response.any?{ |r| r['is_shared'] }
 
@@ -344,7 +344,7 @@ def update_runner(id, user, args)
     end
 
     context 'authorized user without master privileges' do
-      it "should not return project's runners" do
+      it "does not return project's runners" do
         get api("/projects/#{project.id}/runners", user2)
 
         expect(response).to have_http_status(403)
@@ -352,7 +352,7 @@ def update_runner(id, user, args)
     end
 
     context 'unauthorized user' do
-      it "should not return project's runners" do
+      it "does not return project's runners" do
         get api("/projects/#{project.id}/runners")
 
         expect(response).to have_http_status(401)
@@ -368,21 +368,21 @@ def update_runner(id, user, args)
         end
       end
 
-      it 'should enable specific runner' do
+      it 'enables specific runner' do
         expect do
           post api("/projects/#{project.id}/runners", user), runner_id: specific_runner2.id
         end.to change{ project.runners.count }.by(+1)
         expect(response).to have_http_status(201)
       end
 
-      it 'should avoid changes when enabling already enabled runner' do
+      it 'avoids changes when enabling already enabled runner' do
         expect do
           post api("/projects/#{project.id}/runners", user), runner_id: specific_runner.id
         end.to change{ project.runners.count }.by(0)
         expect(response).to have_http_status(409)
       end
 
-      it 'should not enable locked runner' do
+      it 'does not enable locked runner' do
         specific_runner2.update(locked: true)
 
         expect do
@@ -392,14 +392,14 @@ def update_runner(id, user, args)
         expect(response).to have_http_status(403)
       end
 
-      it 'should not enable shared runner' do
+      it 'does not enable shared runner' do
         post api("/projects/#{project.id}/runners", user), runner_id: shared_runner.id
 
         expect(response).to have_http_status(403)
       end
 
       context 'user is admin' do
-        it 'should enable any specific runner' do
+        it 'enables any specific runner' do
           expect do
             post api("/projects/#{project.id}/runners", admin), runner_id: unused_specific_runner.id
           end.to change{ project.runners.count }.by(+1)
@@ -408,14 +408,14 @@ def update_runner(id, user, args)
       end
 
       context 'user is not admin' do
-        it 'should not enable runner without access to' do
+        it 'does not enable runner without access to' do
           post api("/projects/#{project.id}/runners", user), runner_id: unused_specific_runner.id
 
           expect(response).to have_http_status(403)
         end
       end
 
-      it 'should raise an error when no runner_id param is provided' do
+      it 'raises an error when no runner_id param is provided' do
         post api("/projects/#{project.id}/runners", admin)
 
         expect(response).to have_http_status(400)
@@ -423,7 +423,7 @@ def update_runner(id, user, args)
     end
 
     context 'authorized user without permissions' do
-      it 'should not enable runner' do
+      it 'does not enable runner' do
         post api("/projects/#{project.id}/runners", user2)
 
         expect(response).to have_http_status(403)
@@ -431,7 +431,7 @@ def update_runner(id, user, args)
     end
 
     context 'unauthorized user' do
-      it 'should not enable runner' do
+      it 'does not enable runner' do
         post api("/projects/#{project.id}/runners")
 
         expect(response).to have_http_status(401)
@@ -442,7 +442,7 @@ def update_runner(id, user, args)
   describe 'DELETE /projects/:id/runners/:runner_id' do
     context 'authorized user' do
       context 'when runner have more than one associated projects' do
-        it "should disable project's runner" do
+        it "disables project's runner" do
           expect do
             delete api("/projects/#{project.id}/runners/#{two_projects_runner.id}", user)
           end.to change{ project.runners.count }.by(-1)
@@ -451,7 +451,7 @@ def update_runner(id, user, args)
       end
 
       context 'when runner have one associated projects' do
-        it "should not disable project's runner" do
+        it "does not disable project's runner" do
           expect do
             delete api("/projects/#{project.id}/runners/#{specific_runner.id}", user)
           end.to change{ project.runners.count }.by(0)
@@ -459,7 +459,7 @@ def update_runner(id, user, args)
         end
       end
 
-      it 'should return 404 is runner is not found' do
+      it 'returns 404 is runner is not found' do
         delete api("/projects/#{project.id}/runners/9999", user)
 
         expect(response).to have_http_status(404)
@@ -467,7 +467,7 @@ def update_runner(id, user, args)
     end
 
     context 'authorized user without permissions' do
-      it "should not disable project's runner" do
+      it "does not disable project's runner" do
         delete api("/projects/#{project.id}/runners/#{specific_runner.id}", user2)
 
         expect(response).to have_http_status(403)
@@ -475,7 +475,7 @@ def update_runner(id, user, args)
     end
 
     context 'unauthorized user' do
-      it "should not disable project's runner" do
+      it "does not disable project's runner" do
         delete api("/projects/#{project.id}/runners/#{specific_runner.id}")
 
         expect(response).to have_http_status(401)
diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb
index a2446e12804762..375671bca4c27b 100644
--- a/spec/requests/api/services_spec.rb
+++ b/spec/requests/api/services_spec.rb
@@ -11,13 +11,13 @@
     describe "PUT /projects/:id/services/#{service.dasherize}" do
       include_context service
 
-      it "should update #{service} settings" do
+      it "updates #{service} settings" do
         put api("/projects/#{project.id}/services/#{dashed_service}", user), service_attrs
 
         expect(response).to have_http_status(200)
       end
 
-      it "should return if required fields missing" do
+      it "returns if required fields missing" do
         attrs = service_attrs
 
         required_attributes = service_attrs_list.select do |attr|
@@ -32,7 +32,7 @@
           attrs.delete(required_attributes.sample)
           expected_code = 400
         end
-        
+
         put api("/projects/#{project.id}/services/#{dashed_service}", user), attrs
 
         expect(response.status).to eq(expected_code)
@@ -42,7 +42,7 @@
     describe "DELETE /projects/:id/services/#{service.dasherize}" do
       include_context service
 
-      it "should delete #{service}" do
+      it "deletes #{service}" do
         delete api("/projects/#{project.id}/services/#{dashed_service}", user)
 
         expect(response).to have_http_status(200)
@@ -62,29 +62,29 @@
         service_object.save
       end
 
-      it 'should return authentication error when unauthenticated' do
+      it 'returns authentication error when unauthenticated' do
         get api("/projects/#{project.id}/services/#{dashed_service}")
         expect(response).to have_http_status(401)
       end
-      
-      it "should return all properties of service #{service} when authenticated as admin" do
+
+      it "returns all properties of service #{service} when authenticated as admin" do
         get api("/projects/#{project.id}/services/#{dashed_service}", admin)
-        
+
         expect(response).to have_http_status(200)
         expect(json_response['properties'].keys.map(&:to_sym)).to match_array(service_attrs_list.map)
       end
 
-      it "should return properties of service #{service} other than passwords when authenticated as project owner" do
+      it "returns properties of service #{service} other than passwords when authenticated as project owner" do
         get api("/projects/#{project.id}/services/#{dashed_service}", user)
 
         expect(response).to have_http_status(200)
         expect(json_response['properties'].keys.map(&:to_sym)).to match_array(service_attrs_list_without_passwords)
       end
 
-      it "should return error when authenticated but not a project owner" do
+      it "returns error when authenticated but not a project owner" do
         project.team << [user2, :developer]
         get api("/projects/#{project.id}/services/#{dashed_service}", user2)
-        
+
         expect(response).to have_http_status(403)
       end
     end
diff --git a/spec/requests/api/session_spec.rb b/spec/requests/api/session_spec.rb
index c15b7ff97928c7..519e7ce12ad187 100644
--- a/spec/requests/api/session_spec.rb
+++ b/spec/requests/api/session_spec.rb
@@ -7,7 +7,7 @@
 
   describe "POST /session" do
     context "when valid password" do
-      it "should return private token" do
+      it "returns private token" do
         post api("/session"), email: user.email, password: '12345678'
         expect(response).to have_http_status(201)
 
@@ -20,7 +20,7 @@
     end
 
     context 'when email has case-typo and password is valid' do
-      it 'should return private token' do
+      it 'returns private token' do
         post api('/session'), email: user.email.upcase, password: '12345678'
         expect(response.status).to eq 201
 
@@ -33,7 +33,7 @@
     end
 
     context 'when login has case-typo and password is valid' do
-      it 'should return private token' do
+      it 'returns private token' do
         post api('/session'), login: user.username.upcase, password: '12345678'
         expect(response.status).to eq 201
 
@@ -46,7 +46,7 @@
     end
 
     context "when invalid password" do
-      it "should return authentication error" do
+      it "returns authentication error" do
         post api("/session"), email: user.email, password: '123'
         expect(response).to have_http_status(401)
 
@@ -56,7 +56,7 @@
     end
 
     context "when empty password" do
-      it "should return authentication error" do
+      it "returns authentication error" do
         post api("/session"), email: user.email
         expect(response).to have_http_status(401)
 
@@ -66,7 +66,7 @@
     end
 
     context "when empty name" do
-      it "should return authentication error" do
+      it "returns authentication error" do
         post api("/session"), password: user.password
         expect(response).to have_http_status(401)
 
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 684c2cd8e24455..54d096e8b7ffd9 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -7,7 +7,7 @@
   let(:admin) { create(:admin) }
 
   describe "GET /application/settings" do
-    it "should return application settings" do
+    it "returns application settings" do
       get api("/application/settings", admin)
       expect(response).to have_http_status(200)
       expect(json_response).to be_an Hash
@@ -23,7 +23,7 @@
       allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
     end
 
-    it "should update application settings" do
+    it "updates application settings" do
       put api("/application/settings", admin),
         default_projects_limit: 3, signin_enabled: false, repository_storage: 'custom'
       expect(response).to have_http_status(200)
diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb
index cf66f261ade2da..1ce2658569eabc 100644
--- a/spec/requests/api/system_hooks_spec.rb
+++ b/spec/requests/api/system_hooks_spec.rb
@@ -11,21 +11,21 @@
 
   describe "GET /hooks" do
     context "when no user" do
-      it "should return authentication error" do
+      it "returns authentication error" do
         get api("/hooks")
         expect(response).to have_http_status(401)
       end
     end
 
     context "when not an admin" do
-      it "should return forbidden error" do
+      it "returns forbidden error" do
         get api("/hooks", user)
         expect(response).to have_http_status(403)
       end
     end
 
     context "when authenticated as admin" do
-      it "should return an array of hooks" do
+      it "returns an array of hooks" do
         get api("/hooks", admin)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -35,18 +35,18 @@
   end
 
   describe "POST /hooks" do
-    it "should create new hook" do
+    it "creates new hook" do
       expect do
         post api("/hooks", admin), url: 'http://example.com'
       end.to change { SystemHook.count }.by(1)
     end
 
-    it "should respond with 400 if url not given" do
+    it "responds with 400 if url not given" do
       post api("/hooks", admin)
       expect(response).to have_http_status(400)
     end
 
-    it "should not create new hook without url" do
+    it "does not create new hook without url" do
       expect do
         post api("/hooks", admin)
       end.not_to change { SystemHook.count }
@@ -54,26 +54,26 @@
   end
 
   describe "GET /hooks/:id" do
-    it "should return hook by id" do
+    it "returns hook by id" do
       get api("/hooks/#{hook.id}", admin)
       expect(response).to have_http_status(200)
       expect(json_response['event_name']).to eq('project_create')
     end
 
-    it "should return 404 on failure" do
+    it "returns 404 on failure" do
       get api("/hooks/404", admin)
       expect(response).to have_http_status(404)
     end
   end
 
   describe "DELETE /hooks/:id" do
-    it "should delete a hook" do
+    it "deletes a hook" do
       expect do
         delete api("/hooks/#{hook.id}", admin)
       end.to change { SystemHook.count }.by(-1)
     end
 
-    it "should return success if hook id not found" do
+    it "returns success if hook id not found" do
       delete api("/hooks/12345", admin)
       expect(response).to have_http_status(200)
     end
diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb
index fa700ab73436e3..d563883cd4767f 100644
--- a/spec/requests/api/tags_spec.rb
+++ b/spec/requests/api/tags_spec.rb
@@ -16,7 +16,7 @@
     let(:description) { 'Awesome release!' }
 
     context 'without releases' do
-      it "should return an array of project tags" do
+      it "returns an array of project tags" do
         get api("/projects/#{project.id}/repository/tags", user)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -30,7 +30,7 @@
         release.update_attributes(description: description)
       end
 
-      it "should return an array of project tags with release info" do
+      it "returns an array of project tags with release info" do
         get api("/projects/#{project.id}/repository/tags", user)
 
         expect(response).to have_http_status(200)
@@ -61,7 +61,7 @@
 
   describe 'POST /projects/:id/repository/tags' do
     context 'lightweight tags' do
-      it 'should create a new tag' do
+      it 'creates a new tag' do
         post api("/projects/#{project.id}/repository/tags", user),
              tag_name: 'v7.0.1',
              ref: 'master'
@@ -72,7 +72,7 @@
     end
 
     context 'lightweight tags with release notes' do
-      it 'should create a new tag' do
+      it 'creates a new tag' do
         post api("/projects/#{project.id}/repository/tags", user),
              tag_name: 'v7.0.1',
              ref: 'master',
@@ -92,13 +92,13 @@
       end
 
       context 'delete tag' do
-        it 'should delete an existing tag' do
+        it 'deletes an existing tag' do
           delete api("/projects/#{project.id}/repository/tags/#{tag_name}", user)
           expect(response).to have_http_status(200)
           expect(json_response['tag_name']).to eq(tag_name)
         end
 
-        it 'should raise 404 if the tag does not exist' do
+        it 'raises 404 if the tag does not exist' do
           delete api("/projects/#{project.id}/repository/tags/foobar", user)
           expect(response).to have_http_status(404)
         end
@@ -106,7 +106,7 @@
     end
 
     context 'annotated tag' do
-      it 'should create a new annotated tag' do
+      it 'creates a new annotated tag' do
         # Identity must be set in .gitconfig to create annotated tag.
         repo_path = project.repository.path_to_repo
         system(*%W(#{Gitlab.config.git.bin_path} --git-dir=#{repo_path} config user.name #{user.name}))
@@ -123,14 +123,14 @@
       end
     end
 
-    it 'should deny for user without push access' do
+    it 'denies for user without push access' do
       post api("/projects/#{project.id}/repository/tags", user2),
            tag_name: 'v1.9.0',
            ref: '621491c677087aa243f165eab467bfdfbee00be1'
       expect(response).to have_http_status(403)
     end
 
-    it 'should return 400 if tag name is invalid' do
+    it 'returns 400 if tag name is invalid' do
       post api("/projects/#{project.id}/repository/tags", user),
            tag_name: 'v 1.0.0',
            ref: 'master'
@@ -138,7 +138,7 @@
       expect(json_response['message']).to eq('Tag name invalid')
     end
 
-    it 'should return 400 if tag already exists' do
+    it 'returns 400 if tag already exists' do
       post api("/projects/#{project.id}/repository/tags", user),
            tag_name: 'v8.0.0',
            ref: 'master'
@@ -150,7 +150,7 @@
       expect(json_response['message']).to eq('Tag v8.0.0 already exists')
     end
 
-    it 'should return 400 if ref name is invalid' do
+    it 'returns 400 if ref name is invalid' do
       post api("/projects/#{project.id}/repository/tags", user),
            tag_name: 'mytag',
            ref: 'foo'
@@ -163,7 +163,7 @@
     let(:tag_name) { project.repository.tag_names.first }
     let(:description) { 'Awesome release!' }
 
-    it 'should create description for existing git tag' do
+    it 'creates description for existing git tag' do
       post api("/projects/#{project.id}/repository/tags/#{tag_name}/release", user),
         description: description
 
@@ -172,7 +172,7 @@
       expect(json_response['description']).to eq(description)
     end
 
-    it 'should return 404 if the tag does not exist' do
+    it 'returns 404 if the tag does not exist' do
       post api("/projects/#{project.id}/repository/tags/foobar/release", user),
         description: description
 
@@ -186,7 +186,7 @@
         release.update_attributes(description: description)
       end
 
-      it 'should return 409 if there is already a release' do
+      it 'returns 409 if there is already a release' do
         post api("/projects/#{project.id}/repository/tags/#{tag_name}/release", user),
           description: description
 
@@ -207,7 +207,7 @@
         release.update_attributes(description: description)
       end
 
-      it 'should update the release description' do
+      it 'updates the release description' do
         put api("/projects/#{project.id}/repository/tags/#{tag_name}/release", user),
           description: new_description
 
@@ -217,7 +217,7 @@
       end
     end
 
-    it 'should return 404 if the tag does not exist' do
+    it 'returns 404 if the tag does not exist' do
       put api("/projects/#{project.id}/repository/tags/foobar/release", user),
         description: new_description
 
@@ -225,7 +225,7 @@
       expect(json_response['message']).to eq('Tag does not exist')
     end
 
-    it 'should return 404 if the release does not exist' do
+    it 'returns 404 if the release does not exist' do
       put api("/projects/#{project.id}/repository/tags/#{tag_name}/release", user),
         description: new_description
 
diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb
index 8992996c30aaca..5702682fc7da2f 100644
--- a/spec/requests/api/triggers_spec.rb
+++ b/spec/requests/api/triggers_spec.rb
@@ -27,17 +27,17 @@
     end
 
     context 'Handles errors' do
-      it 'should return bad request if token is missing' do
+      it 'returns bad request if token is missing' do
         post api("/projects/#{project.id}/trigger/builds"), ref: 'master'
         expect(response).to have_http_status(400)
       end
 
-      it 'should return not found if project is not found' do
+      it 'returns not found if project is not found' do
         post api('/projects/0/trigger/builds'), options.merge(ref: 'master')
         expect(response).to have_http_status(404)
       end
 
-      it 'should return unauthorized if token is for different project' do
+      it 'returns unauthorized if token is for different project' do
         post api("/projects/#{project2.id}/trigger/builds"), options.merge(ref: 'master')
         expect(response).to have_http_status(401)
       end
@@ -46,14 +46,14 @@
     context 'Have a commit' do
       let(:pipeline) { project.pipelines.last }
 
-      it 'should create builds' do
+      it 'creates builds' do
         post api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'master')
         expect(response).to have_http_status(201)
         pipeline.builds.reload
         expect(pipeline.builds.size).to eq(2)
       end
 
-      it 'should return bad request with no builds created if there\'s no commit for that ref' do
+      it 'returns bad request with no builds created if there\'s no commit for that ref' do
         post api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'other-branch')
         expect(response).to have_http_status(400)
         expect(json_response['message']).to eq('No builds created')
@@ -64,19 +64,19 @@
           { 'TRIGGER_KEY' => 'TRIGGER_VALUE' }
         end
 
-        it 'should validate variables to be a hash' do
+        it 'validates variables to be a hash' do
           post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: 'value', ref: 'master')
           expect(response).to have_http_status(400)
           expect(json_response['message']).to eq('variables needs to be a hash')
         end
 
-        it 'should validate variables needs to be a map of key-valued strings' do
+        it 'validates variables needs to be a map of key-valued strings' do
           post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: { key: %w(1 2) }, ref: 'master')
           expect(response).to have_http_status(400)
           expect(json_response['message']).to eq('variables needs to be a map of key-valued strings')
         end
 
-        it 'create trigger request with variables' do
+        it 'creates trigger request with variables' do
           post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: variables, ref: 'master')
           expect(response).to have_http_status(201)
           pipeline.builds.reload
@@ -88,7 +88,7 @@
 
   describe 'GET /projects/:id/triggers' do
     context 'authenticated user with valid permissions' do
-      it 'should return list of triggers' do
+      it 'returns list of triggers' do
         get api("/projects/#{project.id}/triggers", user)
 
         expect(response).to have_http_status(200)
@@ -98,7 +98,7 @@
     end
 
     context 'authenticated user with invalid permissions' do
-      it 'should not return triggers list' do
+      it 'does not return triggers list' do
         get api("/projects/#{project.id}/triggers", user2)
 
         expect(response).to have_http_status(403)
@@ -106,7 +106,7 @@
     end
 
     context 'unauthenticated user' do
-      it 'should not return triggers list' do
+      it 'does not return triggers list' do
         get api("/projects/#{project.id}/triggers")
 
         expect(response).to have_http_status(401)
@@ -116,14 +116,14 @@
 
   describe 'GET /projects/:id/triggers/:token' do
     context 'authenticated user with valid permissions' do
-      it 'should return trigger details' do
+      it 'returns trigger details' do
         get api("/projects/#{project.id}/triggers/#{trigger.token}", user)
 
         expect(response).to have_http_status(200)
         expect(json_response).to be_a(Hash)
       end
 
-      it 'should respond with 404 Not Found if requesting non-existing trigger' do
+      it 'responds with 404 Not Found if requesting non-existing trigger' do
         get api("/projects/#{project.id}/triggers/abcdef012345", user)
 
         expect(response).to have_http_status(404)
@@ -131,7 +131,7 @@
     end
 
     context 'authenticated user with invalid permissions' do
-      it 'should not return triggers list' do
+      it 'does not return triggers list' do
         get api("/projects/#{project.id}/triggers/#{trigger.token}", user2)
 
         expect(response).to have_http_status(403)
@@ -139,7 +139,7 @@
     end
 
     context 'unauthenticated user' do
-      it 'should not return triggers list' do
+      it 'does not return triggers list' do
         get api("/projects/#{project.id}/triggers/#{trigger.token}")
 
         expect(response).to have_http_status(401)
@@ -149,7 +149,7 @@
 
   describe 'POST /projects/:id/triggers' do
     context 'authenticated user with valid permissions' do
-      it 'should create trigger' do
+      it 'creates trigger' do
         expect do
           post api("/projects/#{project.id}/triggers", user)
         end.to change{project.triggers.count}.by(1)
@@ -160,7 +160,7 @@
     end
 
     context 'authenticated user with invalid permissions' do
-      it 'should not create trigger' do
+      it 'does not create trigger' do
         post api("/projects/#{project.id}/triggers", user2)
 
         expect(response).to have_http_status(403)
@@ -168,7 +168,7 @@
     end
 
     context 'unauthenticated user' do
-      it 'should not create trigger' do
+      it 'does not create trigger' do
         post api("/projects/#{project.id}/triggers")
 
         expect(response).to have_http_status(401)
@@ -178,14 +178,14 @@
 
   describe 'DELETE /projects/:id/triggers/:token' do
     context 'authenticated user with valid permissions' do
-      it 'should delete trigger' do
+      it 'deletes trigger' do
         expect do
           delete api("/projects/#{project.id}/triggers/#{trigger.token}", user)
         end.to change{project.triggers.count}.by(-1)
         expect(response).to have_http_status(200)
       end
 
-      it 'should respond with 404 Not Found if requesting non-existing trigger' do
+      it 'responds with 404 Not Found if requesting non-existing trigger' do
         delete api("/projects/#{project.id}/triggers/abcdef012345", user)
 
         expect(response).to have_http_status(404)
@@ -193,7 +193,7 @@
     end
 
     context 'authenticated user with invalid permissions' do
-      it 'should not delete trigger' do
+      it 'does not delete trigger' do
         delete api("/projects/#{project.id}/triggers/#{trigger.token}", user2)
 
         expect(response).to have_http_status(403)
@@ -201,7 +201,7 @@
     end
 
     context 'unauthenticated user' do
-      it 'should not delete trigger' do
+      it 'does not delete trigger' do
         delete api("/projects/#{project.id}/triggers/#{trigger.token}")
 
         expect(response).to have_http_status(401)
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index e43e3e269bfc28..69b5072a81ec2c 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -13,7 +13,7 @@
 
   describe "GET /users" do
     context "when unauthenticated" do
-      it "should return authentication error" do
+      it "returns authentication error" do
         get api("/users")
         expect(response).to have_http_status(401)
       end
@@ -38,7 +38,7 @@
         end
       end
 
-      it "should return an array of users" do
+      it "returns an array of users" do
         get api("/users", user)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -48,7 +48,7 @@
         end['username']).to eq(username)
       end
 
-      it "should return one user" do
+      it "returns one user" do
         get api("/users?username=#{omniauth_user.username}", user)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -57,7 +57,7 @@
     end
 
     context "when admin" do
-      it "should return an array of users" do
+      it "returns an array of users" do
         get api("/users", admin)
         expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
@@ -72,24 +72,24 @@
   end
 
   describe "GET /users/:id" do
-    it "should return a user by id" do
+    it "returns a user by id" do
       get api("/users/#{user.id}", user)
       expect(response).to have_http_status(200)
       expect(json_response['username']).to eq(user.username)
     end
 
-    it "should return a 401 if unauthenticated" do
+    it "returns a 401 if unauthenticated" do
       get api("/users/9998")
       expect(response).to have_http_status(401)
     end
 
-    it "should return a 404 error if user id not found" do
+    it "returns a 404 error if user id not found" do
       get api("/users/9999", user)
       expect(response).to have_http_status(404)
       expect(json_response['message']).to eq('404 Not found')
     end
 
-    it "should return a 404 if invalid ID" do
+    it "returns a 404 if invalid ID" do
       get api("/users/1ASDF", user)
       expect(response).to have_http_status(404)
     end
@@ -98,13 +98,13 @@
   describe "POST /users" do
     before{ admin }
 
-    it "should create user" do
+    it "creates user" do
       expect do
         post api("/users", admin), attributes_for(:user, projects_limit: 3)
       end.to change { User.count }.by(1)
     end
 
-    it "should create user with correct attributes" do
+    it "creates user with correct attributes" do
       post api('/users', admin), attributes_for(:user, admin: true, can_create_group: true)
       expect(response).to have_http_status(201)
       user_id = json_response['id']
@@ -114,7 +114,7 @@
       expect(new_user.can_create_group).to eq(true)
     end
 
-    it "should create non-admin user" do
+    it "creates non-admin user" do
       post api('/users', admin), attributes_for(:user, admin: false, can_create_group: false)
       expect(response).to have_http_status(201)
       user_id = json_response['id']
@@ -124,7 +124,7 @@
       expect(new_user.can_create_group).to eq(false)
     end
 
-    it "should create non-admin users by default" do
+    it "creates non-admin users by default" do
       post api('/users', admin), attributes_for(:user)
       expect(response).to have_http_status(201)
       user_id = json_response['id']
@@ -133,7 +133,7 @@
       expect(new_user.admin).to eq(false)
     end
 
-    it "should return 201 Created on success" do
+    it "returns 201 Created on success" do
       post api("/users", admin), attributes_for(:user, projects_limit: 3)
       expect(response).to have_http_status(201)
     end
@@ -148,7 +148,7 @@
       expect(new_user.external).to be_falsy
     end
 
-    it 'should allow an external user to be created' do
+    it 'allows an external user to be created' do
       post api("/users", admin), attributes_for(:user, external: true)
       expect(response).to have_http_status(201)
 
@@ -158,7 +158,7 @@
       expect(new_user.external).to be_truthy
     end
 
-    it "should not create user with invalid email" do
+    it "does not create user with invalid email" do
       post api('/users', admin),
         email: 'invalid email',
         password: 'password',
@@ -166,27 +166,27 @@
       expect(response).to have_http_status(400)
     end
 
-    it 'should return 400 error if name not given' do
+    it 'returns 400 error if name not given' do
       post api('/users', admin), attributes_for(:user).except(:name)
       expect(response).to have_http_status(400)
     end
 
-    it 'should return 400 error if password not given' do
+    it 'returns 400 error if password not given' do
       post api('/users', admin), attributes_for(:user).except(:password)
       expect(response).to have_http_status(400)
     end
 
-    it 'should return 400 error if email not given' do
+    it 'returns 400 error if email not given' do
       post api('/users', admin), attributes_for(:user).except(:email)
       expect(response).to have_http_status(400)
     end
 
-    it 'should return 400 error if username not given' do
+    it 'returns 400 error if username not given' do
       post api('/users', admin), attributes_for(:user).except(:username)
       expect(response).to have_http_status(400)
     end
 
-    it 'should return 400 error if user does not validate' do
+    it 'returns 400 error if user does not validate' do
       post api('/users', admin),
         password: 'pass',
         email: 'test@example.com',
@@ -205,7 +205,7 @@
         to eq([Gitlab::Regex.namespace_regex_message])
     end
 
-    it "shouldn't available for non admin users" do
+    it "is not available for non admin users" do
       post api("/users", user), attributes_for(:user)
       expect(response).to have_http_status(403)
     end
@@ -219,7 +219,7 @@
           name: 'foo'
       end
 
-      it 'should return 409 conflict error if user with same email exists' do
+      it 'returns 409 conflict error if user with same email exists' do
         expect do
           post api('/users', admin),
             name: 'foo',
@@ -231,7 +231,7 @@
         expect(json_response['message']).to eq('Email has already been taken')
       end
 
-      it 'should return 409 conflict error if same username exists' do
+      it 'returns 409 conflict error if same username exists' do
         expect do
           post api('/users', admin),
             name: 'foo',
@@ -246,7 +246,7 @@
   end
 
   describe "GET /users/sign_up" do
-    it "should redirect to sign in page" do
+    it "redirects to sign in page" do
       get "/users/sign_up"
       expect(response).to have_http_status(302)
       expect(response).to redirect_to(new_user_session_path)
@@ -258,55 +258,55 @@
 
     before { admin }
 
-    it "should update user with new bio" do
+    it "updates user with new bio" do
       put api("/users/#{user.id}", admin), { bio: 'new test bio' }
       expect(response).to have_http_status(200)
       expect(json_response['bio']).to eq('new test bio')
       expect(user.reload.bio).to eq('new test bio')
     end
 
-    it 'should update user with his own email' do
+    it 'updates user with his own email' do
       put api("/users/#{user.id}", admin), email: user.email
       expect(response).to have_http_status(200)
       expect(json_response['email']).to eq(user.email)
       expect(user.reload.email).to eq(user.email)
     end
 
-    it 'should update user with his own username' do
+    it 'updates user with his own username' do
       put api("/users/#{user.id}", admin), username: user.username
       expect(response).to have_http_status(200)
       expect(json_response['username']).to eq(user.username)
       expect(user.reload.username).to eq(user.username)
     end
 
-    it "should update user's existing identity" do
+    it "updates user's existing identity" do
       put api("/users/#{omniauth_user.id}", admin), provider: 'ldapmain', extern_uid: '654321'
       expect(response).to have_http_status(200)
       expect(omniauth_user.reload.identities.first.extern_uid).to eq('654321')
     end
 
-    it 'should update user with new identity' do
+    it 'updates user with new identity' do
       put api("/users/#{user.id}", admin), provider: 'github', extern_uid: '67890'
       expect(response).to have_http_status(200)
       expect(user.reload.identities.first.extern_uid).to eq('67890')
       expect(user.reload.identities.first.provider).to eq('github')
     end
 
-    it "should update admin status" do
+    it "updates admin status" do
       put api("/users/#{user.id}", admin), { admin: true }
       expect(response).to have_http_status(200)
       expect(json_response['is_admin']).to eq(true)
       expect(user.reload.admin).to eq(true)
     end
 
-    it "should update external status" do
+    it "updates external status" do
       put api("/users/#{user.id}", admin), { external: true }
       expect(response.status).to eq 200
       expect(json_response['external']).to eq(true)
       expect(user.reload.external?).to be_truthy
     end
 
-    it "should not update admin status" do
+    it "does not update admin status" do
       put api("/users/#{admin_user.id}", admin), { can_create_group: false }
       expect(response).to have_http_status(200)
       expect(json_response['is_admin']).to eq(true)
@@ -314,28 +314,28 @@
       expect(admin_user.can_create_group).to eq(false)
     end
 
-    it "should not allow invalid update" do
+    it "does not allow invalid update" do
       put api("/users/#{user.id}", admin), { email: 'invalid email' }
       expect(response).to have_http_status(400)
       expect(user.reload.email).not_to eq('invalid email')
     end
 
-    it "shouldn't available for non admin users" do
+    it "is not available for non admin users" do
       put api("/users/#{user.id}", user), attributes_for(:user)
       expect(response).to have_http_status(403)
     end
 
-    it "should return 404 for non-existing user" do
+    it "returns 404 for non-existing user" do
       put api("/users/999999", admin), { bio: 'update should fail' }
       expect(response).to have_http_status(404)
       expect(json_response['message']).to eq('404 Not found')
     end
 
-    it "should raise error for invalid ID" do
+    it "raises error for invalid ID" do
       expect{put api("/users/ASDF", admin) }.to raise_error(ActionController::RoutingError)
     end
 
-    it 'should return 400 error if user does not validate' do
+    it 'returns 400 error if user does not validate' do
       put api("/users/#{user.id}", admin),
         password: 'pass',
         email: 'test@example.com',
@@ -361,13 +361,13 @@
         @user = User.all.last
       end
 
-      it 'should return 409 conflict error if email address exists' do
+      it 'returns 409 conflict error if email address exists' do
         put api("/users/#{@user.id}", admin), email: 'test@example.com'
         expect(response).to have_http_status(409)
         expect(@user.reload.email).to eq(@user.email)
       end
 
-      it 'should return 409 conflict error if username taken' do
+      it 'returns 409 conflict error if username taken' do
         @user_id = User.all.last.id
         put api("/users/#{@user.id}", admin), username: 'test'
         expect(response).to have_http_status(409)
@@ -379,26 +379,26 @@
   describe "POST /users/:id/keys" do
     before { admin }
 
-    it "should not create invalid ssh key" do
+    it "does not create invalid ssh key" do
       post api("/users/#{user.id}/keys", admin), { title: "invalid key" }
       expect(response).to have_http_status(400)
       expect(json_response['message']).to eq('400 (Bad request) "key" not given')
     end
 
-    it 'should not create key without title' do
+    it 'does not create key without title' do
       post api("/users/#{user.id}/keys", admin), key: 'some key'
       expect(response).to have_http_status(400)
       expect(json_response['message']).to eq('400 (Bad request) "title" not given')
     end
 
-    it "should create ssh key" do
+    it "creates ssh key" do
       key_attrs = attributes_for :key
       expect do
         post api("/users/#{user.id}/keys", admin), key_attrs
       end.to change{ user.keys.count }.by(1)
     end
 
-    it "should return 405 for invalid ID" do
+    it "returns 405 for invalid ID" do
       post api("/users/ASDF/keys", admin)
       expect(response).to have_http_status(405)
     end
@@ -408,20 +408,20 @@
     before { admin }
 
     context 'when unauthenticated' do
-      it 'should return authentication error' do
+      it 'returns authentication error' do
         get api("/users/#{user.id}/keys")
         expect(response).to have_http_status(401)
       end
     end
 
     context 'when authenticated' do
-      it 'should return 404 for non-existing user' do
+      it 'returns 404 for non-existing user' do
         get api('/users/999999/keys', admin)
         expect(response).to have_http_status(404)
         expect(json_response['message']).to eq('404 User Not Found')
       end
 
-      it 'should return array of ssh keys' do
+      it 'returns array of ssh keys' do
         user.keys << key
         user.save
         get api("/users/#{user.id}/keys", admin)
@@ -430,7 +430,7 @@
         expect(json_response.first['title']).to eq(key.title)
       end
 
-      it "should return 405 for invalid ID" do
+      it "returns 405 for invalid ID" do
         get api("/users/ASDF/keys", admin)
         expect(response).to have_http_status(405)
       end
@@ -441,14 +441,14 @@
     before { admin }
 
     context 'when unauthenticated' do
-      it 'should return authentication error' do
+      it 'returns authentication error' do
         delete api("/users/#{user.id}/keys/42")
         expect(response).to have_http_status(401)
       end
     end
 
     context 'when authenticated' do
-      it 'should delete existing key' do
+      it 'deletes existing key' do
         user.keys << key
         user.save
         expect do
@@ -457,7 +457,7 @@
         expect(response).to have_http_status(200)
       end
 
-      it 'should return 404 error if user not found' do
+      it 'returns 404 error if user not found' do
         user.keys << key
         user.save
         delete api("/users/999999/keys/#{key.id}", admin)
@@ -465,7 +465,7 @@
         expect(json_response['message']).to eq('404 User Not Found')
       end
 
-      it 'should return 404 error if key not foud' do
+      it 'returns 404 error if key not foud' do
         delete api("/users/#{user.id}/keys/42", admin)
         expect(response).to have_http_status(404)
         expect(json_response['message']).to eq('404 Key Not Found')
@@ -476,20 +476,20 @@
   describe "POST /users/:id/emails" do
     before { admin }
 
-    it "should not create invalid email" do
+    it "does not create invalid email" do
       post api("/users/#{user.id}/emails", admin), {}
       expect(response).to have_http_status(400)
       expect(json_response['message']).to eq('400 (Bad request) "email" not given')
     end
 
-    it "should create email" do
+    it "creates email" do
       email_attrs = attributes_for :email
       expect do
         post api("/users/#{user.id}/emails", admin), email_attrs
       end.to change{ user.emails.count }.by(1)
     end
 
-    it "should raise error for invalid ID" do
+    it "raises error for invalid ID" do
       post api("/users/ASDF/emails", admin)
       expect(response).to have_http_status(405)
     end
@@ -499,20 +499,20 @@
     before { admin }
 
     context 'when unauthenticated' do
-      it 'should return authentication error' do
+      it 'returns authentication error' do
         get api("/users/#{user.id}/emails")
         expect(response).to have_http_status(401)
       end
     end
 
     context 'when authenticated' do
-      it 'should return 404 for non-existing user' do
+      it 'returns 404 for non-existing user' do
         get api('/users/999999/emails', admin)
         expect(response).to have_http_status(404)
         expect(json_response['message']).to eq('404 User Not Found')
       end
 
-      it 'should return array of emails' do
+      it 'returns array of emails' do
         user.emails << email
         user.save
         get api("/users/#{user.id}/emails", admin)
@@ -521,7 +521,7 @@
         expect(json_response.first['email']).to eq(email.email)
       end
 
-      it "should raise error for invalid ID" do
+      it "raises error for invalid ID" do
         put api("/users/ASDF/emails", admin)
         expect(response).to have_http_status(405)
       end
@@ -532,14 +532,14 @@
     before { admin }
 
     context 'when unauthenticated' do
-      it 'should return authentication error' do
+      it 'returns authentication error' do
         delete api("/users/#{user.id}/emails/42")
         expect(response).to have_http_status(401)
       end
     end
 
     context 'when authenticated' do
-      it 'should delete existing email' do
+      it 'deletes existing email' do
         user.emails << email
         user.save
         expect do
@@ -548,7 +548,7 @@
         expect(response).to have_http_status(200)
       end
 
-      it 'should return 404 error if user not found' do
+      it 'returns 404 error if user not found' do
         user.emails << email
         user.save
         delete api("/users/999999/emails/#{email.id}", admin)
@@ -556,13 +556,13 @@
         expect(json_response['message']).to eq('404 User Not Found')
       end
 
-      it 'should return 404 error if email not foud' do
+      it 'returns 404 error if email not foud' do
         delete api("/users/#{user.id}/emails/42", admin)
         expect(response).to have_http_status(404)
         expect(json_response['message']).to eq('404 Email Not Found')
       end
 
-      it "should raise error for invalid ID" do
+      it "raises error for invalid ID" do
         expect{delete api("/users/ASDF/emails/bar", admin) }.to raise_error(ActionController::RoutingError)
       end
     end
@@ -571,36 +571,36 @@
   describe "DELETE /users/:id" do
     before { admin }
 
-    it "should delete user" do
+    it "deletes user" do
       delete api("/users/#{user.id}", admin)
       expect(response).to have_http_status(200)
       expect { User.find(user.id) }.to raise_error ActiveRecord::RecordNotFound
       expect(json_response['email']).to eq(user.email)
     end
 
-    it "should not delete for unauthenticated user" do
+    it "does not delete for unauthenticated user" do
       delete api("/users/#{user.id}")
       expect(response).to have_http_status(401)
     end
 
-    it "shouldn't available for non admin users" do
+    it "is not available for non admin users" do
       delete api("/users/#{user.id}", user)
       expect(response).to have_http_status(403)
     end
 
-    it "should return 404 for non-existing user" do
+    it "returns 404 for non-existing user" do
       delete api("/users/999999", admin)
       expect(response).to have_http_status(404)
       expect(json_response['message']).to eq('404 User Not Found')
     end
 
-    it "should raise error for invalid ID" do
+    it "raises error for invalid ID" do
       expect{delete api("/users/ASDF", admin) }.to raise_error(ActionController::RoutingError)
     end
   end
 
   describe "GET /user" do
-    it "should return current user" do
+    it "returns current user" do
       get api("/user", user)
       expect(response).to have_http_status(200)
       expect(json_response['email']).to eq(user.email)
@@ -610,7 +610,7 @@
       expect(json_response['projects_limit']).to eq(user.projects_limit)
     end
 
-    it "should return 401 error if user is unauthenticated" do
+    it "returns 401 error if user is unauthenticated" do
       get api("/user")
       expect(response).to have_http_status(401)
     end
@@ -618,14 +618,14 @@
 
   describe "GET /user/keys" do
     context "when unauthenticated" do
-      it "should return authentication error" do
+      it "returns authentication error" do
         get api("/user/keys")
         expect(response).to have_http_status(401)
       end
     end
 
     context "when authenticated" do
-      it "should return array of ssh keys" do
+      it "returns array of ssh keys" do
         user.keys << key
         user.save
         get api("/user/keys", user)
@@ -637,7 +637,7 @@
   end
 
   describe "GET /user/keys/:id" do
-    it "should return single key" do
+    it "returns single key" do
       user.keys << key
       user.save
       get api("/user/keys/#{key.id}", user)
@@ -645,13 +645,13 @@
       expect(json_response["title"]).to eq(key.title)
     end
 
-    it "should return 404 Not Found within invalid ID" do
+    it "returns 404 Not Found within invalid ID" do
       get api("/user/keys/42", user)
       expect(response).to have_http_status(404)
       expect(json_response['message']).to eq('404 Not found')
     end
 
-    it "should return 404 error if admin accesses user's ssh key" do
+    it "returns 404 error if admin accesses user's ssh key" do
       user.keys << key
       user.save
       admin
@@ -660,14 +660,14 @@
       expect(json_response['message']).to eq('404 Not found')
     end
 
-    it "should return 404 for invalid ID" do
+    it "returns 404 for invalid ID" do
       get api("/users/keys/ASDF", admin)
       expect(response).to have_http_status(404)
     end
   end
 
   describe "POST /user/keys" do
-    it "should create ssh key" do
+    it "creates ssh key" do
       key_attrs = attributes_for :key
       expect do
         post api("/user/keys", user), key_attrs
@@ -675,31 +675,31 @@
       expect(response).to have_http_status(201)
     end
 
-    it "should return a 401 error if unauthorized" do
+    it "returns a 401 error if unauthorized" do
       post api("/user/keys"), title: 'some title', key: 'some key'
       expect(response).to have_http_status(401)
     end
 
-    it "should not create ssh key without key" do
+    it "does not create ssh key without key" do
       post api("/user/keys", user), title: 'title'
       expect(response).to have_http_status(400)
       expect(json_response['message']).to eq('400 (Bad request) "key" not given')
     end
 
-    it 'should not create ssh key without title' do
+    it 'does not create ssh key without title' do
       post api('/user/keys', user), key: 'some key'
       expect(response).to have_http_status(400)
       expect(json_response['message']).to eq('400 (Bad request) "title" not given')
     end
 
-    it "should not create ssh key without title" do
+    it "does not create ssh key without title" do
       post api("/user/keys", user), key: "somekey"
       expect(response).to have_http_status(400)
     end
   end
 
   describe "DELETE /user/keys/:id" do
-    it "should delete existed key" do
+    it "deletes existed key" do
       user.keys << key
       user.save
       expect do
@@ -708,33 +708,33 @@
       expect(response).to have_http_status(200)
     end
 
-    it "should return success if key ID not found" do
+    it "returns success if key ID not found" do
       delete api("/user/keys/42", user)
       expect(response).to have_http_status(200)
     end
 
-    it "should return 401 error if unauthorized" do
+    it "returns 401 error if unauthorized" do
       user.keys << key
       user.save
       delete api("/user/keys/#{key.id}")
       expect(response).to have_http_status(401)
     end
 
-    it "should raise error for invalid ID" do
+    it "raises error for invalid ID" do
       expect{delete api("/users/keys/ASDF", admin) }.to raise_error(ActionController::RoutingError)
     end
   end
 
   describe "GET /user/emails" do
     context "when unauthenticated" do
-      it "should return authentication error" do
+      it "returns authentication error" do
         get api("/user/emails")
         expect(response).to have_http_status(401)
       end
     end
 
     context "when authenticated" do
-      it "should return array of emails" do
+      it "returns array of emails" do
         user.emails << email
         user.save
         get api("/user/emails", user)
@@ -746,7 +746,7 @@
   end
 
   describe "GET /user/emails/:id" do
-    it "should return single email" do
+    it "returns single email" do
       user.emails << email
       user.save
       get api("/user/emails/#{email.id}", user)
@@ -754,13 +754,13 @@
       expect(json_response["email"]).to eq(email.email)
     end
 
-    it "should return 404 Not Found within invalid ID" do
+    it "returns 404 Not Found within invalid ID" do
       get api("/user/emails/42", user)
       expect(response).to have_http_status(404)
       expect(json_response['message']).to eq('404 Not found')
     end
 
-    it "should return 404 error if admin accesses user's email" do
+    it "returns 404 error if admin accesses user's email" do
       user.emails << email
       user.save
       admin
@@ -769,14 +769,14 @@
       expect(json_response['message']).to eq('404 Not found')
     end
 
-    it "should return 404 for invalid ID" do
+    it "returns 404 for invalid ID" do
       get api("/users/emails/ASDF", admin)
       expect(response).to have_http_status(404)
     end
   end
 
   describe "POST /user/emails" do
-    it "should create email" do
+    it "creates email" do
       email_attrs = attributes_for :email
       expect do
         post api("/user/emails", user), email_attrs
@@ -784,12 +784,12 @@
       expect(response).to have_http_status(201)
     end
 
-    it "should return a 401 error if unauthorized" do
+    it "returns a 401 error if unauthorized" do
       post api("/user/emails"), email: 'some email'
       expect(response).to have_http_status(401)
     end
 
-    it "should not create email with invalid email" do
+    it "does not create email with invalid email" do
       post api("/user/emails", user), {}
       expect(response).to have_http_status(400)
       expect(json_response['message']).to eq('400 (Bad request) "email" not given')
@@ -797,7 +797,7 @@
   end
 
   describe "DELETE /user/emails/:id" do
-    it "should delete existed email" do
+    it "deletes existed email" do
       user.emails << email
       user.save
       expect do
@@ -806,44 +806,44 @@
       expect(response).to have_http_status(200)
     end
 
-    it "should return success if email ID not found" do
+    it "returns success if email ID not found" do
       delete api("/user/emails/42", user)
       expect(response).to have_http_status(200)
     end
 
-    it "should return 401 error if unauthorized" do
+    it "returns 401 error if unauthorized" do
       user.emails << email
       user.save
       delete api("/user/emails/#{email.id}")
       expect(response).to have_http_status(401)
     end
 
-    it "should raise error for invalid ID" do
+    it "raises error for invalid ID" do
       expect{delete api("/users/emails/ASDF", admin) }.to raise_error(ActionController::RoutingError)
     end
   end
 
   describe 'PUT /user/:id/block' do
     before { admin }
-    it 'should block existing user' do
+    it 'blocks existing user' do
       put api("/users/#{user.id}/block", admin)
       expect(response).to have_http_status(200)
       expect(user.reload.state).to eq('blocked')
     end
 
-    it 'should not re-block ldap blocked users' do
+    it 'does not re-block ldap blocked users' do
       put api("/users/#{ldap_blocked_user.id}/block", admin)
       expect(response).to have_http_status(403)
       expect(ldap_blocked_user.reload.state).to eq('ldap_blocked')
     end
 
-    it 'should not be available for non admin users' do
+    it 'does not be available for non admin users' do
       put api("/users/#{user.id}/block", user)
       expect(response).to have_http_status(403)
       expect(user.reload.state).to eq('active')
     end
 
-    it 'should return a 404 error if user id not found' do
+    it 'returns a 404 error if user id not found' do
       put api('/users/9999/block', admin)
       expect(response).to have_http_status(404)
       expect(json_response['message']).to eq('404 User Not Found')
@@ -854,37 +854,37 @@
     let(:blocked_user)  { create(:user, state: 'blocked') }
     before { admin }
 
-    it 'should unblock existing user' do
+    it 'unblocks existing user' do
       put api("/users/#{user.id}/unblock", admin)
       expect(response).to have_http_status(200)
       expect(user.reload.state).to eq('active')
     end
 
-    it 'should unblock a blocked user' do
+    it 'unblocks a blocked user' do
       put api("/users/#{blocked_user.id}/unblock", admin)
       expect(response).to have_http_status(200)
       expect(blocked_user.reload.state).to eq('active')
     end
 
-    it 'should not unblock ldap blocked users' do
+    it 'does not unblock ldap blocked users' do
       put api("/users/#{ldap_blocked_user.id}/unblock", admin)
       expect(response).to have_http_status(403)
       expect(ldap_blocked_user.reload.state).to eq('ldap_blocked')
     end
 
-    it 'should not be available for non admin users' do
+    it 'does not be available for non admin users' do
       put api("/users/#{user.id}/unblock", user)
       expect(response).to have_http_status(403)
       expect(user.reload.state).to eq('active')
     end
 
-    it 'should return a 404 error if user id not found' do
+    it 'returns a 404 error if user id not found' do
       put api('/users/9999/block', admin)
       expect(response).to have_http_status(404)
       expect(json_response['message']).to eq('404 User Not Found')
     end
 
-    it "should raise error for invalid ID" do
+    it "raises error for invalid ID" do
       expect{put api("/users/ASDF/block", admin) }.to raise_error(ActionController::RoutingError)
     end
   end
diff --git a/spec/requests/api/variables_spec.rb b/spec/requests/api/variables_spec.rb
index ddba18245f862b..05fbdb909dce66 100644
--- a/spec/requests/api/variables_spec.rb
+++ b/spec/requests/api/variables_spec.rb
@@ -12,7 +12,7 @@
 
   describe 'GET /projects/:id/variables' do
     context 'authorized user with proper permissions' do
-      it 'should return project variables' do
+      it 'returns project variables' do
         get api("/projects/#{project.id}/variables", user)
 
         expect(response).to have_http_status(200)
@@ -21,7 +21,7 @@
     end
 
     context 'authorized user with invalid permissions' do
-      it 'should not return project variables' do
+      it 'does not return project variables' do
         get api("/projects/#{project.id}/variables", user2)
 
         expect(response).to have_http_status(403)
@@ -29,7 +29,7 @@
     end
 
     context 'unauthorized user' do
-      it 'should not return project variables' do
+      it 'does not return project variables' do
         get api("/projects/#{project.id}/variables")
 
         expect(response).to have_http_status(401)
@@ -39,14 +39,14 @@
 
   describe 'GET /projects/:id/variables/:key' do
     context 'authorized user with proper permissions' do
-      it 'should return project variable details' do
+      it 'returns project variable details' do
         get api("/projects/#{project.id}/variables/#{variable.key}", user)
 
         expect(response).to have_http_status(200)
         expect(json_response['value']).to eq(variable.value)
       end
 
-      it 'should respond with 404 Not Found if requesting non-existing variable' do
+      it 'responds with 404 Not Found if requesting non-existing variable' do
         get api("/projects/#{project.id}/variables/non_existing_variable", user)
 
         expect(response).to have_http_status(404)
@@ -54,7 +54,7 @@
     end
 
     context 'authorized user with invalid permissions' do
-      it 'should not return project variable details' do
+      it 'does not return project variable details' do
         get api("/projects/#{project.id}/variables/#{variable.key}", user2)
 
         expect(response).to have_http_status(403)
@@ -62,7 +62,7 @@
     end
 
     context 'unauthorized user' do
-      it 'should not return project variable details' do
+      it 'does not return project variable details' do
         get api("/projects/#{project.id}/variables/#{variable.key}")
 
         expect(response).to have_http_status(401)
@@ -72,7 +72,7 @@
 
   describe 'POST /projects/:id/variables' do
     context 'authorized user with proper permissions' do
-      it 'should create variable' do
+      it 'creates variable' do
         expect do
           post api("/projects/#{project.id}/variables", user), key: 'TEST_VARIABLE_2', value: 'VALUE_2'
         end.to change{project.variables.count}.by(1)
@@ -82,7 +82,7 @@
         expect(json_response['value']).to eq('VALUE_2')
       end
 
-      it 'should not allow to duplicate variable key' do
+      it 'does not allow to duplicate variable key' do
         expect do
           post api("/projects/#{project.id}/variables", user), key: variable.key, value: 'VALUE_2'
         end.to change{project.variables.count}.by(0)
@@ -92,7 +92,7 @@
     end
 
     context 'authorized user with invalid permissions' do
-      it 'should not create variable' do
+      it 'does not create variable' do
         post api("/projects/#{project.id}/variables", user2)
 
         expect(response).to have_http_status(403)
@@ -100,7 +100,7 @@
     end
 
     context 'unauthorized user' do
-      it 'should not create variable' do
+      it 'does not create variable' do
         post api("/projects/#{project.id}/variables")
 
         expect(response).to have_http_status(401)
@@ -110,7 +110,7 @@
 
   describe 'PUT /projects/:id/variables/:key' do
     context 'authorized user with proper permissions' do
-      it 'should update variable data' do
+      it 'updates variable data' do
         initial_variable = project.variables.first
         value_before = initial_variable.value
 
@@ -123,7 +123,7 @@
         expect(updated_variable.value).to eq('VALUE_1_UP')
       end
 
-      it 'should responde with 404 Not Found if requesting non-existing variable' do
+      it 'responds with 404 Not Found if requesting non-existing variable' do
         put api("/projects/#{project.id}/variables/non_existing_variable", user)
 
         expect(response).to have_http_status(404)
@@ -131,7 +131,7 @@
     end
 
     context 'authorized user with invalid permissions' do
-      it 'should not update variable' do
+      it 'does not update variable' do
         put api("/projects/#{project.id}/variables/#{variable.key}", user2)
 
         expect(response).to have_http_status(403)
@@ -139,7 +139,7 @@
     end
 
     context 'unauthorized user' do
-      it 'should not update variable' do
+      it 'does not update variable' do
         put api("/projects/#{project.id}/variables/#{variable.key}")
 
         expect(response).to have_http_status(401)
@@ -149,14 +149,14 @@
 
   describe 'DELETE /projects/:id/variables/:key' do
     context 'authorized user with proper permissions' do
-      it 'should delete variable' do
+      it 'deletes variable' do
         expect do
           delete api("/projects/#{project.id}/variables/#{variable.key}", user)
         end.to change{project.variables.count}.by(-1)
         expect(response).to have_http_status(200)
       end
 
-      it 'should responde with 404 Not Found if requesting non-existing variable' do
+      it 'responds with 404 Not Found if requesting non-existing variable' do
         delete api("/projects/#{project.id}/variables/non_existing_variable", user)
 
         expect(response).to have_http_status(404)
@@ -164,7 +164,7 @@
     end
 
     context 'authorized user with invalid permissions' do
-      it 'should not delete variable' do
+      it 'does not delete variable' do
         delete api("/projects/#{project.id}/variables/#{variable.key}", user2)
 
         expect(response).to have_http_status(403)
@@ -172,7 +172,7 @@
     end
 
     context 'unauthorized user' do
-      it 'should not delete variable' do
+      it 'does not delete variable' do
         delete api("/projects/#{project.id}/variables/#{variable.key}")
 
         expect(response).to have_http_status(401)
diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb
index cf1e8d9b514740..05b309096cb7d8 100644
--- a/spec/requests/ci/api/builds_spec.rb
+++ b/spec/requests/ci/api/builds_spec.rb
@@ -19,7 +19,7 @@
     end
 
     describe "POST /builds/register" do
-      it "should start a build" do
+      it "starts a build" do
         pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master')
         pipeline.create_builds(nil)
         build = pipeline.builds.first
@@ -31,13 +31,13 @@
         expect(runner.reload.platform).to eq("darwin")
       end
 
-      it "should return 404 error if no pending build found" do
+      it "returns 404 error if no pending build found" do
         post ci_api("/builds/register"), token: runner.token
 
         expect(response).to have_http_status(404)
       end
 
-      it "should return 404 error if no builds for specific runner" do
+      it "returns 404 error if no builds for specific runner" do
         pipeline = FactoryGirl.create(:ci_pipeline, project: shared_project)
         FactoryGirl.create(:ci_build, pipeline: pipeline, status: 'pending')
 
@@ -46,7 +46,7 @@
         expect(response).to have_http_status(404)
       end
 
-      it "should return 404 error if no builds for shared runner" do
+      it "returns 404 error if no builds for shared runner" do
         pipeline = FactoryGirl.create(:ci_pipeline, project: project)
         FactoryGirl.create(:ci_build, pipeline: pipeline, status: 'pending')
 
@@ -171,18 +171,18 @@ def register_builds
         put ci_api("/builds/#{build.id}"), token: runner.token
       end
 
-      it "should update a running build" do
+      it "updates a running build" do
         expect(response).to have_http_status(200)
       end
 
-      it 'should not override trace information when no trace is given' do
+      it 'does not override trace information when no trace is given' do
         expect(build.reload.trace).to eq 'BUILD TRACE'
       end
 
       context 'build has been erased' do
         let(:build) { create(:ci_build, runner_id: runner.id, erased_at: Time.now) }
 
-        it 'should respond with forbidden' do
+        it 'responds with forbidden' do
           expect(response.status).to eq 403
         end
       end
@@ -280,7 +280,7 @@ def register_builds
         context 'authorization token is invalid' do
           before { post authorize_url, { token: 'invalid', filesize: 100 } }
 
-          it 'should respond with forbidden' do
+          it 'responds with forbidden' do
             expect(response).to have_http_status(403)
           end
         end
@@ -300,7 +300,7 @@ def register_builds
               upload_artifacts(file_upload, headers_with_token)
             end
 
-            it 'should respond with forbidden' do
+            it 'responds with forbidden' do
               expect(response.status).to eq 403
             end
           end
@@ -342,7 +342,7 @@ def register_builds
             end
           end
 
-          context 'should post artifacts file and metadata file' do
+          context 'posts artifacts file and metadata file' do
             let!(:artifacts) { file_upload }
             let!(:metadata) { file_upload2 }
 
@@ -354,7 +354,7 @@ def register_builds
               post(post_url, post_data, headers_with_token)
             end
 
-            context 'post data accelerated by workhorse is correct' do
+            context 'posts data accelerated by workhorse is correct' do
               let(:post_data) do
                 { 'file.path' => artifacts.path,
                   'file.name' => artifacts.original_filename,
@@ -422,7 +422,7 @@ def register_builds
           end
 
           context "artifacts file is too large" do
-            it "should fail to post too large artifact" do
+            it "fails to post too large artifact" do
               stub_application_setting(max_artifacts_size: 0)
               upload_artifacts(file_upload, headers_with_token)
               expect(response).to have_http_status(413)
@@ -430,14 +430,14 @@ def register_builds
           end
 
           context "artifacts post request does not contain file" do
-            it "should fail to post artifacts without file" do
+            it "fails to post artifacts without file" do
               post post_url, {}, headers_with_token
               expect(response).to have_http_status(400)
             end
           end
 
           context 'GitLab Workhorse is not configured' do
-            it "should fail to post artifacts without GitLab-Workhorse" do
+            it "fails to post artifacts without GitLab-Workhorse" do
               post post_url, { token: build.token }, {}
               expect(response).to have_http_status(403)
             end
@@ -456,7 +456,7 @@ def register_builds
             FileUtils.remove_entry @tmpdir
           end
 
-          it "should fail to post artifacts for outside of tmp path" do
+          it "fails to post artifacts for outside of tmp path" do
             upload_artifacts(file_upload, headers_with_token)
             expect(response).to have_http_status(400)
           end
@@ -482,7 +482,7 @@ def upload_artifacts(file, headers = {}, accelerated = true)
           build.reload
         end
 
-        it 'should remove build artifacts' do
+        it 'removes build artifacts' do
           expect(response).to have_http_status(200)
           expect(build.artifacts_file.exists?).to be_falsy
           expect(build.artifacts_metadata.exists?).to be_falsy
@@ -500,14 +500,14 @@ def upload_artifacts(file, headers = {}, accelerated = true)
               'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' }
           end
 
-          it 'should download artifact' do
+          it 'downloads artifact' do
             expect(response).to have_http_status(200)
             expect(response.headers).to include download_headers
           end
         end
 
         context 'build does not has artifacts' do
-          it 'should respond with not found' do
+          it 'responds with not found' do
             expect(response).to have_http_status(404)
           end
         end
diff --git a/spec/requests/ci/api/triggers_spec.rb b/spec/requests/ci/api/triggers_spec.rb
index f12678e5a8ecd0..3312bd11669847 100644
--- a/spec/requests/ci/api/triggers_spec.rb
+++ b/spec/requests/ci/api/triggers_spec.rb
@@ -19,17 +19,17 @@
     end
 
     context 'Handles errors' do
-      it 'should return bad request if token is missing' do
+      it 'returns bad request if token is missing' do
         post ci_api("/projects/#{project.ci_id}/refs/master/trigger")
         expect(response).to have_http_status(400)
       end
 
-      it 'should return not found if project is not found' do
+      it 'returns not found if project is not found' do
         post ci_api('/projects/0/refs/master/trigger'), options
         expect(response).to have_http_status(404)
       end
 
-      it 'should return unauthorized if token is for different project' do
+      it 'returns unauthorized if token is for different project' do
         post ci_api("/projects/#{project2.ci_id}/refs/master/trigger"), options
         expect(response).to have_http_status(401)
       end
@@ -38,14 +38,14 @@
     context 'Have a commit' do
       let(:pipeline) { project.pipelines.last }
 
-      it 'should create builds' do
+      it 'creates builds' do
         post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options
         expect(response).to have_http_status(201)
         pipeline.builds.reload
         expect(pipeline.builds.size).to eq(2)
       end
 
-      it 'should return bad request with no builds created if there\'s no commit for that ref' do
+      it 'returns bad request with no builds created if there\'s no commit for that ref' do
         post ci_api("/projects/#{project.ci_id}/refs/other-branch/trigger"), options
         expect(response).to have_http_status(400)
         expect(json_response['message']).to eq('No builds created')
@@ -56,19 +56,19 @@
           { 'TRIGGER_KEY' => 'TRIGGER_VALUE' }
         end
 
-        it 'should validate variables to be a hash' do
+        it 'validates variables to be a hash' do
           post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options.merge(variables: 'value')
           expect(response).to have_http_status(400)
           expect(json_response['message']).to eq('variables needs to be a hash')
         end
 
-        it 'should validate variables needs to be a map of key-valued strings' do
+        it 'validates variables needs to be a map of key-valued strings' do
           post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options.merge(variables: { key: %w(1 2) })
           expect(response).to have_http_status(400)
           expect(json_response['message']).to eq('variables needs to be a map of key-valued strings')
         end
 
-        it 'create trigger request with variables' do
+        it 'creates trigger request with variables' do
           post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options.merge(variables: variables)
           expect(response).to have_http_status(201)
           pipeline.builds.reload
diff --git a/spec/services/create_snippet_service_spec.rb b/spec/services/create_snippet_service_spec.rb
index 7a850066bf86b8..d81d0fd76c9680 100644
--- a/spec/services/create_snippet_service_spec.rb
+++ b/spec/services/create_snippet_service_spec.rb
@@ -19,7 +19,7 @@
       @opts.merge!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
     end
 
-    it 'non-admins should not be able to create a public snippet' do
+    it 'non-admins are not able to create a public snippet' do
       snippet = create_snippet(nil, @user, @opts)
       expect(snippet.errors.messages).to have_key(:visibility_level)
       expect(snippet.errors.messages[:visibility_level].first).to(
@@ -27,7 +27,7 @@
       )
     end
 
-    it 'admins should be able to create a public snippet' do
+    it 'admins are able to create a public snippet' do
       snippet = create_snippet(nil, @admin, @opts)
       expect(snippet.errors.any?).to be_falsey
       expect(snippet.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC)
diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb
index 789836f71bb2d3..16a9956fe7f998 100644
--- a/spec/services/event_create_service_spec.rb
+++ b/spec/services/event_create_service_spec.rb
@@ -41,7 +41,7 @@
 
       it { expect(service.open_mr(merge_request, merge_request.author)).to be_truthy }
 
-      it "should create new event" do
+      it "creates new event" do
         expect { service.open_mr(merge_request, merge_request.author) }.to change { Event.count }
       end
     end
@@ -51,7 +51,7 @@
 
       it { expect(service.close_mr(merge_request, merge_request.author)).to be_truthy }
 
-      it "should create new event" do
+      it "creates new event" do
         expect { service.close_mr(merge_request, merge_request.author) }.to change { Event.count }
       end
     end
@@ -61,7 +61,7 @@
 
       it { expect(service.merge_mr(merge_request, merge_request.author)).to be_truthy }
 
-      it "should create new event" do
+      it "creates new event" do
         expect { service.merge_mr(merge_request, merge_request.author) }.to change { Event.count }
       end
     end
@@ -71,7 +71,7 @@
 
       it { expect(service.reopen_mr(merge_request, merge_request.author)).to be_truthy }
 
-      it "should create new event" do
+      it "creates new event" do
         expect { service.reopen_mr(merge_request, merge_request.author) }.to change { Event.count }
       end
     end
@@ -85,7 +85,7 @@
 
       it { expect(service.open_milestone(milestone, user)).to be_truthy }
 
-      it "should create new event" do
+      it "creates new event" do
         expect { service.open_milestone(milestone, user) }.to change { Event.count }
       end
     end
@@ -95,7 +95,7 @@
 
       it { expect(service.close_milestone(milestone, user)).to be_truthy }
 
-      it "should create new event" do
+      it "creates new event" do
         expect { service.close_milestone(milestone, user) }.to change { Event.count }
       end
     end
@@ -105,7 +105,7 @@
 
       it { expect(service.destroy_milestone(milestone, user)).to be_truthy }
 
-      it "should create new event" do
+      it "creates new event" do
         expect { service.destroy_milestone(milestone, user) }.to change { Event.count }
       end
     end
diff --git a/spec/services/git_hooks_service_spec.rb b/spec/services/git_hooks_service_spec.rb
index 3fc37a315c0d0b..41b0968b8b412a 100644
--- a/spec/services/git_hooks_service_spec.rb
+++ b/spec/services/git_hooks_service_spec.rb
@@ -17,7 +17,7 @@
 
   describe '#execute' do
     context 'when receive hooks were successful' do
-      it 'should call post-receive hook' do
+      it 'calls post-receive hook' do
         hook = double(trigger: [true, nil])
         expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook)
 
@@ -26,7 +26,7 @@
     end
 
     context 'when pre-receive hook failed' do
-      it 'should not call post-receive hook' do
+      it 'does not call post-receive hook' do
         expect(service).to receive(:run_hook).with('pre-receive').and_return([false, ''])
         expect(service).not_to receive(:run_hook).with('post-receive')
 
@@ -37,7 +37,7 @@
     end
 
     context 'when update hook failed' do
-      it 'should not call post-receive hook' do
+      it 'does not call post-receive hook' do
         expect(service).to receive(:run_hook).with('pre-receive').and_return([true, nil])
         expect(service).to receive(:run_hook).with('update').and_return([false, ''])
         expect(service).not_to receive(:run_hook).with('post-receive')
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index ffa998dffc3e18..80f6ebac86c588 100644
--- a/spec/services/git_push_service_spec.rb
+++ b/spec/services/git_push_service_spec.rb
@@ -420,7 +420,7 @@
       context "mentioning an issue" do
         let(:message) { "this is some work.\n\nrelated to JIRA-1" }
 
-        it "should initiate one api call to jira server to mention the issue" do
+        it "initiates one api call to jira server to mention the issue" do
           execute_service(project, user, @oldrev, @newrev, @ref )
 
           expect(WebMock).to have_requested(:post, jira_api_comment_url).with(
@@ -432,7 +432,7 @@
       context "closing an issue" do
         let(:message) { "this is some work.\n\ncloses JIRA-1" }
 
-        it "should initiate one api call to jira server to close the issue" do
+        it "initiates one api call to jira server to close the issue" do
           transition_body = {
             transition: {
               id: '2'
@@ -445,7 +445,7 @@
           ).once
         end
 
-        it "should initiate one api call to jira server to comment on the issue" do
+        it "initiates one api call to jira server to comment on the issue" do
           comment_body = {
             body: "Issue solved with [#{closing_commit.id}|http://localhost/#{project.path_with_namespace}/commit/#{closing_commit.id}]."
           }.to_json
diff --git a/spec/services/issues/bulk_update_service_spec.rb b/spec/services/issues/bulk_update_service_spec.rb
index 321b54ac39df8b..ac08aa53b0ba04 100644
--- a/spec/services/issues/bulk_update_service_spec.rb
+++ b/spec/services/issues/bulk_update_service_spec.rb
@@ -217,7 +217,7 @@ def create_issue_with_labels(labels)
       let(:labels) { [merge_requests] }
       let(:remove_labels) { [regression] }
 
-      it 'remove the label IDs from all issues passed' do
+      it 'removes the label IDs from all issues passed' do
         expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(regression.id)
       end
 
diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb
index 67a919ba8ee320..1318607a388015 100644
--- a/spec/services/issues/close_service_spec.rb
+++ b/spec/services/issues/close_service_spec.rb
@@ -23,13 +23,13 @@
       it { expect(@issue).to be_valid }
       it { expect(@issue).to be_closed }
 
-      it 'should send email to user2 about assign of new issue' do
+      it 'sends email to user2 about assign of new issue' do
         email = ActionMailer::Base.deliveries.last
         expect(email.to.first).to eq(user2.email)
         expect(email.subject).to include(issue.title)
       end
 
-      it 'should create system note about issue reassign' do
+      it 'creates system note about issue reassign' do
         note = @issue.notes.last
         expect(note.note).to include "Status changed to closed"
       end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index dacbcd8fb46e07..088c3d48bf76d2 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -53,7 +53,7 @@ def find_note(starting_with)
       it { expect(@issue.labels.count).to eq(1) }
       it { expect(@issue.labels.first.title).to eq(label.name) }
 
-      it 'should send email to user2 about assign of new issue and email to user3 about issue unassignment' do
+      it 'sends email to user2 about assign of new issue and email to user3 about issue unassignment' do
         deliveries = ActionMailer::Base.deliveries
         email = deliveries.last
         recipients = deliveries.last(2).map(&:to).flatten
@@ -61,14 +61,14 @@ def find_note(starting_with)
         expect(email.subject).to include(issue.title)
       end
 
-      it 'should create system note about issue reassign' do
+      it 'creates system note about issue reassign' do
         note = find_note('Reassigned to')
 
         expect(note).not_to be_nil
         expect(note.note).to include "Reassigned to \@#{user2.username}"
       end
 
-      it 'should create system note about issue label edit' do
+      it 'creates system note about issue label edit' do
         note = find_note('Added ~')
 
         expect(note).not_to be_nil
@@ -267,7 +267,7 @@ def update_issue(opts)
           expect(note).to be_nil
         end
 
-        it 'should not generate a new note at all' do
+        it 'does not generate a new note at all' do
           expect do
             update_issue({ description: "- [ ] One\n- [ ] Two\n- [ ] Three" })
           end.not_to change { Note.count }
diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb
index c1db4f3284b18e..403533be5d9bd4 100644
--- a/spec/services/merge_requests/close_service_spec.rb
+++ b/spec/services/merge_requests/close_service_spec.rb
@@ -32,13 +32,13 @@
                                with(@merge_request, 'close')
       end
 
-      it 'should send email to user2 about assign of new merge_request' do
+      it 'sends email to user2 about assign of new merge_request' do
         email = ActionMailer::Base.deliveries.last
         expect(email.to.first).to eq(user2.email)
         expect(email.subject).to include(merge_request.title)
       end
 
-      it 'should create system note about merge_request reassign' do
+      it 'creates system note about merge_request reassign' do
         note = @merge_request.notes.last
         expect(note.note).to include 'Status changed to closed'
       end
diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb
index d0b55d2d5094bd..b84a580967ad92 100644
--- a/spec/services/merge_requests/create_service_spec.rb
+++ b/spec/services/merge_requests/create_service_spec.rb
@@ -32,7 +32,7 @@
       it { expect(@merge_request.assignee).to be_nil }
       it { expect(@merge_request.merge_params['force_remove_source_branch']).to eq('1') }
 
-      it 'should execute hooks with default action' do
+      it 'executes hooks with default action' do
         expect(service).to have_received(:execute_hooks).with(@merge_request)
       end
 
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index 8ffebcac6986e7..159f6817e8d9bb 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -26,13 +26,13 @@
       it { expect(merge_request).to be_valid }
       it { expect(merge_request).to be_merged }
 
-      it 'should send email to user2 about merge of new merge_request' do
+      it 'sends email to user2 about merge of new merge_request' do
         email = ActionMailer::Base.deliveries.last
         expect(email.to.first).to eq(user2.email)
         expect(email.subject).to include(merge_request.title)
       end
 
-      it 'should create system note about merge_request merge' do
+      it 'creates system note about merge_request merge' do
         note = merge_request.notes.last
         expect(note.note).to include 'Status changed to merged'
       end
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 781ee7ffed3ed1..fff86480c6d777 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -55,7 +55,7 @@
         reload_mrs
       end
 
-      it 'should execute hooks with update action' do
+      it 'executes hooks with update action' do
         expect(refresh_service).to have_received(:execute_hooks).
           with(@merge_request, 'update', @oldrev)
       end
@@ -111,7 +111,7 @@
         reload_mrs
       end
 
-      it 'should execute hooks with update action' do
+      it 'executes hooks with update action' do
         expect(refresh_service).to have_received(:execute_hooks).
           with(@fork_merge_request, 'update', @oldrev)
       end
diff --git a/spec/services/merge_requests/reopen_service_spec.rb b/spec/services/merge_requests/reopen_service_spec.rb
index 88c9c640514533..3419b8bf5e6aca 100644
--- a/spec/services/merge_requests/reopen_service_spec.rb
+++ b/spec/services/merge_requests/reopen_service_spec.rb
@@ -27,18 +27,18 @@
       it { expect(merge_request).to be_valid }
       it { expect(merge_request).to be_reopened }
 
-      it 'should execute hooks with reopen action' do
+      it 'executes hooks with reopen action' do
         expect(service).to have_received(:execute_hooks).
                                with(merge_request, 'reopen')
       end
 
-      it 'should send email to user2 about reopen of merge_request' do
+      it 'sends email to user2 about reopen of merge_request' do
         email = ActionMailer::Base.deliveries.last
         expect(email.to.first).to eq(user2.email)
         expect(email.subject).to include(merge_request.title)
       end
 
-      it 'should create system note about merge_request reopen' do
+      it 'creates system note about merge_request reopen' do
         note = merge_request.notes.last
         expect(note.note).to include 'Status changed to reopened'
       end
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index d4ebe28c276a68..283a336afd9346 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -64,12 +64,12 @@ def update_merge_request(opts)
       it { expect(@merge_request.target_branch).to eq('target') }
       it { expect(@merge_request.merge_params['force_remove_source_branch']).to eq('1') }
 
-      it 'should execute hooks with update action' do
+      it 'executes hooks with update action' do
         expect(service).to have_received(:execute_hooks).
                                with(@merge_request, 'update')
       end
 
-      it 'should send email to user2 about assign of new merge request and email to user3 about merge request unassignment' do
+      it 'sends email to user2 about assign of new merge request and email to user3 about merge request unassignment' do
         deliveries = ActionMailer::Base.deliveries
         email = deliveries.last
         recipients = deliveries.last(2).map(&:to).flatten
@@ -77,14 +77,14 @@ def update_merge_request(opts)
         expect(email.subject).to include(merge_request.title)
       end
 
-      it 'should create system note about merge_request reassign' do
+      it 'creates system note about merge_request reassign' do
         note = find_note('Reassigned to')
 
         expect(note).not_to be_nil
         expect(note.note).to include "Reassigned to \@#{user2.username}"
       end
 
-      it 'should create system note about merge_request label edit' do
+      it 'creates system note about merge_request label edit' do
         note = find_note('Added ~')
 
         expect(note).not_to be_nil
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 9fc93f325f7291..92b441c28caa4d 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -15,7 +15,7 @@
 
       it { expect(notification.new_key(key)).to be_truthy }
 
-      it 'should sent email to key owner' do
+      it 'sends email to key owner' do
         expect{ notification.new_key(key) }.to change{ ActionMailer::Base.deliveries.size }.by(1)
       end
     end
@@ -27,7 +27,7 @@
 
       it { expect(notification.new_email(email)).to be_truthy }
 
-      it 'should send email to email owner' do
+      it 'sends email to email owner' do
         expect{ notification.new_email(email) }.to change{ ActionMailer::Base.deliveries.size }.by(1)
       end
     end
@@ -593,7 +593,7 @@
         update_custom_notification(:close_issue, @u_custom_global)
       end
 
-      it 'should sent email to issue assignee and issue author' do
+      it 'sends email to issue assignee and issue author' do
         notification.close_issue(issue, @u_disabled)
 
         should_email(issue.assignee)
@@ -646,7 +646,7 @@
         update_custom_notification(:reopen_issue, @u_custom_global)
       end
 
-      it 'should send email to issue assignee and issue author' do
+      it 'sends email to issue assignee and issue author' do
         notification.reopen_issue(issue, @u_disabled)
 
         should_email(issue.assignee)
diff --git a/spec/services/projects/autocomplete_service_spec.rb b/spec/services/projects/autocomplete_service_spec.rb
index 0971fec2e9f29b..7916c2d957cc06 100644
--- a/spec/services/projects/autocomplete_service_spec.rb
+++ b/spec/services/projects/autocomplete_service_spec.rb
@@ -13,7 +13,7 @@
       let!(:security_issue_1) { create(:issue, :confidential, project: project, title: 'Security issue 1', author: author) }
       let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project, assignee: assignee) }
 
-      it 'should not list project confidential issues for guests' do
+      it 'does not list project confidential issues for guests' do
         autocomplete = described_class.new(project, nil)
         issues = autocomplete.issues.map(&:iid)
 
@@ -23,7 +23,7 @@
         expect(issues.count).to eq 1
       end
 
-      it 'should not list project confidential issues for non project members' do
+      it 'does not list project confidential issues for non project members' do
         autocomplete = described_class.new(project, non_member)
         issues = autocomplete.issues.map(&:iid)
 
@@ -33,7 +33,7 @@
         expect(issues.count).to eq 1
       end
 
-      it 'should not list project confidential issues for project members with guest role' do
+      it 'does not list project confidential issues for project members with guest role' do
         project.team << [member, :guest]
 
         autocomplete = described_class.new(project, non_member)
@@ -45,7 +45,7 @@
         expect(issues.count).to eq 1
       end
 
-      it 'should list project confidential issues for author' do
+      it 'lists project confidential issues for author' do
         autocomplete = described_class.new(project, author)
         issues = autocomplete.issues.map(&:iid)
 
@@ -55,7 +55,7 @@
         expect(issues.count).to eq 2
       end
 
-      it 'should list project confidential issues for assignee' do
+      it 'lists project confidential issues for assignee' do
         autocomplete = described_class.new(project, assignee)
         issues = autocomplete.issues.map(&:iid)
 
@@ -65,7 +65,7 @@
         expect(issues.count).to eq 2
       end
 
-      it 'should list project confidential issues for project members' do
+      it 'lists project confidential issues for project members' do
         project.team << [member, :developer]
 
         autocomplete = described_class.new(project, member)
@@ -77,7 +77,7 @@
         expect(issues.count).to eq 3
       end
 
-      it 'should list all project issues for admin' do
+      it 'lists all project issues for admin' do
         autocomplete = described_class.new(project, admin)
         issues = autocomplete.issues.map(&:iid)
 
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index fd1143594672e5..bbced59ff023b1 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -109,7 +109,7 @@
         )
       end
 
-      it 'should not allow a restricted visibility level for non-admins' do
+      it 'does not allow a restricted visibility level for non-admins' do
         project = create_project(@user, @opts)
         expect(project).to respond_to(:errors)
         expect(project.errors.messages).to have_key(:visibility_level)
@@ -118,7 +118,7 @@
         )
       end
 
-      it 'should allow a restricted visibility level for admins' do
+      it 'allows a restricted visibility level for admins' do
         admin = create(:admin)
         project = create_project(admin, @opts)
 
@@ -128,7 +128,7 @@
     end
 
     context 'repository creation' do
-      it 'should synchronously create the repository' do
+      it 'synchronously creates the repository' do
         expect_any_instance_of(Project).to receive(:create_repository)
 
         project = create_project(@user, @opts)
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index 31bb7120d84c67..ef2036c78b1ab8 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -26,7 +26,7 @@
     end
 
     context 'project already exists' do
-      it "should fail due to validation, not transaction failure" do
+      it "fails due to validation, not transaction failure" do
         @existing_project = create(:project, creator_id: @to_user.id, name: @from_project.name, namespace: @to_namespace)
         @to_project = fork_project(@from_project, @to_user)
         expect(@existing_project.persisted?).to be_truthy
@@ -36,7 +36,7 @@
     end
 
     context 'GitLab CI is enabled' do
-      it "fork and enable CI for fork" do
+      it "forks and enables CI for fork" do
         @from_project.enable_ci
         @to_project = fork_project(@from_project, @to_user)
         expect(@to_project.builds_enabled?).to be_truthy
@@ -97,14 +97,14 @@
     end
 
     context 'fork project for group when user not owner' do
-      it 'group developer should fail to fork project into the group' do
+      it 'group developer fails to fork project into the group' do
         to_project = fork_project(@project, @developer, @opts)
         expect(to_project.errors[:namespace]).to eq(['is not valid'])
       end
     end
 
     context 'project already exists in group' do
-      it 'should fail due to validation, not transaction failure' do
+      it 'fails due to validation, not transaction failure' do
         existing_project = create(:project, name: @project.name,
                                             namespace: @group)
         to_project = fork_project(@project, @group_owner, @opts)
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index e8b9e6b923840a..e139be19140cd6 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -9,7 +9,7 @@
       @opts = {}
     end
 
-    context 'should be private when updated to private' do
+    context 'is private when updated to private' do
       before do
         @created_private = @project.private?
 
@@ -21,7 +21,7 @@
       it { expect(@project.private?).to be_truthy }
     end
 
-    context 'should be internal when updated to internal' do
+    context 'is internal when updated to internal' do
       before do
         @created_private = @project.private?
 
@@ -33,7 +33,7 @@
       it { expect(@project.internal?).to be_truthy }
     end
 
-    context 'should be public when updated to public' do
+    context 'is public when updated to public' do
       before do
         @created_private = @project.private?
 
@@ -50,7 +50,7 @@
         stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
       end
 
-      context 'should be private when updated to private' do
+      context 'is private when updated to private' do
         before do
           @created_private = @project.private?
 
@@ -62,7 +62,7 @@
         it { expect(@project.private?).to be_truthy }
       end
 
-      context 'should be internal when updated to internal' do
+      context 'is internal when updated to internal' do
         before do
           @created_private = @project.private?
 
@@ -74,7 +74,7 @@
         it { expect(@project.internal?).to be_truthy }
       end
 
-      context 'should be private when updated to public' do
+      context 'is private when updated to public' do
         before do
           @created_private = @project.private?
 
@@ -86,7 +86,7 @@
         it { expect(@project.private?).to be_truthy }
       end
 
-      context 'should be public when updated to public by admin' do
+      context 'is public when updated to public by admin' do
         before do
           @created_private = @project.private?
 
@@ -114,7 +114,7 @@
       @fork_created_internal = forked_project.internal?
     end
 
-    context 'should update forks visibility level when parent set to more restrictive' do
+    context 'updates forks visibility level when parent set to more restrictive' do
       before do
         opts.merge!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
         update_project(project, user, opts).inspect
@@ -126,7 +126,7 @@
       it { expect(project.forks.first.private?).to be_truthy }
     end
 
-    context 'should not update forks visibility level when parent set to less restrictive' do
+    context 'does not update forks visibility level when parent set to less restrictive' do
       before do
         opts.merge!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
         update_project(project, user, opts).inspect
diff --git a/spec/services/repair_ldap_blocked_user_service_spec.rb b/spec/services/repair_ldap_blocked_user_service_spec.rb
index ce7d1455975409..87192457298c57 100644
--- a/spec/services/repair_ldap_blocked_user_service_spec.rb
+++ b/spec/services/repair_ldap_blocked_user_service_spec.rb
@@ -6,14 +6,14 @@
   subject(:service) { RepairLdapBlockedUserService.new(user) }
 
   describe '#execute' do
-    it 'change to normal block after destroying last ldap identity' do
+    it 'changes to normal block after destroying last ldap identity' do
       identity.destroy
       service.execute
 
       expect(user.reload).not_to be_ldap_blocked
     end
 
-    it 'change to normal block after changing last ldap identity to another provider' do
+    it 'changes to normal block after changing last ldap identity to another provider' do
       identity.update_attribute(:provider, 'twitter')
       service.execute
 
diff --git a/spec/services/search_service_spec.rb b/spec/services/search_service_spec.rb
index 7b3a9a75d7c4d5..bd89c4a7c116bc 100644
--- a/spec/services/search_service_spec.rb
+++ b/spec/services/search_service_spec.rb
@@ -16,7 +16,7 @@
 
   describe '#execute' do
     context 'unauthenticated' do
-      it 'should return public projects only' do
+      it 'returns public projects only' do
         context = Search::GlobalService.new(nil, search: "searchable")
         results = context.execute
         expect(results.objects('projects')).to match_array [public_project]
@@ -24,19 +24,19 @@
     end
 
     context 'authenticated' do
-      it 'should return public, internal and private projects' do
+      it 'returns public, internal and private projects' do
         context = Search::GlobalService.new(user, search: "searchable")
         results = context.execute
         expect(results.objects('projects')).to match_array [public_project, found_project, internal_project]
       end
 
-      it 'should return only public & internal projects' do
+      it 'returns only public & internal projects' do
         context = Search::GlobalService.new(internal_user, search: "searchable")
         results = context.execute
         expect(results.objects('projects')).to match_array [internal_project, public_project]
       end
 
-      it 'namespace name should be searchable' do
+      it 'namespace name is searchable' do
         context = Search::GlobalService.new(user, search: found_project.namespace.path)
         results = context.execute
         expect(results.objects('projects')).to match_array [found_project]
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 43693441450f7b..00427d6db2a6c2 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -471,15 +471,15 @@
     shared_examples 'cross project mentionable' do
       include GitlabMarkdownHelper
 
-      it 'should contain cross reference to new noteable' do
+      it 'contains cross reference to new noteable' do
         expect(subject.note).to include cross_project_reference(new_project, new_noteable)
       end
 
-      it 'should mention referenced noteable' do
+      it 'mentions referenced noteable' do
         expect(subject.note).to include new_noteable.to_reference
       end
 
-      it 'should mention referenced project' do
+      it 'mentions referenced project' do
         expect(subject.note).to include new_project.to_reference
       end
     end
@@ -489,7 +489,7 @@
 
       it_behaves_like 'cross project mentionable'
 
-      it 'should notify about noteable being moved to' do
+      it 'notifies about noteable being moved to' do
         expect(subject.note).to match /Moved to/
       end
     end
@@ -499,7 +499,7 @@
 
       it_behaves_like 'cross project mentionable'
 
-      it 'should notify about noteable being moved from' do
+      it 'notifies about noteable being moved from' do
         expect(subject.note).to match /Moved from/
       end
     end
@@ -507,7 +507,7 @@
     context 'invalid direction' do
       let(:direction) { :invalid }
 
-      it 'should raise error' do
+      it 'raises error' do
         expect { subject }.to raise_error StandardError, /Invalid direction/
       end
     end
diff --git a/spec/services/test_hook_service_spec.rb b/spec/services/test_hook_service_spec.rb
index 4f47e89b4b57d7..4f6dd8c6d3f1ce 100644
--- a/spec/services/test_hook_service_spec.rb
+++ b/spec/services/test_hook_service_spec.rb
@@ -6,7 +6,7 @@
   let(:hook)    { create :project_hook, project: project }
 
   describe '#execute' do
-    it "should execute successfully" do
+    it "executes successfully" do
       stub_request(:post, hook.url).to_return(status: 200)
       expect(TestHookService.new.execute(hook, user)).to be_truthy
     end
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index d2c056d8e14c0a..baf78208ec5415 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -53,7 +53,7 @@ def reenable_backup_sub_tasks
 
       let(:gitlab_version) { Gitlab::VERSION }
 
-      it 'should fail on mismatch' do
+      it 'fails on mismatch' do
         allow(YAML).to receive(:load_file).
           and_return({ gitlab_version: "not #{gitlab_version}" })
 
@@ -61,7 +61,7 @@ def reenable_backup_sub_tasks
           to raise_error(SystemExit)
       end
 
-      it 'should invoke restoration on match' do
+      it 'invokes restoration on match' do
         allow(YAML).to receive(:load_file).
           and_return({ gitlab_version: gitlab_version })
         expect(Rake::Task['gitlab:db:drop_tables']).to receive(:invoke)
@@ -107,7 +107,7 @@ def create_backup
       end
 
       context 'archive file permissions' do
-        it 'should set correct permissions on the tar file' do
+        it 'sets correct permissions on the tar file' do
           expect(File.exist?(@backup_tar)).to be_truthy
           expect(File::Stat.new(@backup_tar).mode.to_s(8)).to eq('100600')
         end
@@ -127,7 +127,7 @@ def create_backup
         end
       end
 
-      it 'should set correct permissions on the tar contents' do
+      it 'sets correct permissions on the tar contents' do
         tar_contents, exit_status = Gitlab::Popen.popen(
           %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz lfs.tar.gz registry.tar.gz}
         )
@@ -142,7 +142,7 @@ def create_backup
         expect(tar_contents).not_to match(/^.{4,9}[rwx].* (database.sql.gz|uploads.tar.gz|repositories|builds.tar.gz|artifacts.tar.gz|registry.tar.gz)\/$/)
       end
 
-      it 'should delete temp directories' do
+      it 'deletes temp directories' do
         temp_dirs = Dir.glob(
           File.join(Gitlab.config.backup.path, '{db,repositories,uploads,builds,artifacts,lfs,registry}')
         )
@@ -153,7 +153,7 @@ def create_backup
       context 'registry disabled' do
         let(:enable_registry) { false }
 
-        it 'should not create registry.tar.gz' do
+        it 'does not create registry.tar.gz' do
           tar_contents, exit_status = Gitlab::Popen.popen(
             %W{tar -tvf #{@backup_tar}}
           )
@@ -191,7 +191,7 @@ def create_backup
         FileUtils.rm(@backup_tar)
       end
 
-      it 'should include repositories in all repository storages' do
+      it 'includes repositories in all repository storages' do
         tar_contents, exit_status = Gitlab::Popen.popen(
           %W{tar -tvf #{@backup_tar} repositories}
         )
diff --git a/spec/tasks/gitlab/db_rake_spec.rb b/spec/tasks/gitlab/db_rake_spec.rb
index 36d03a224e4f21..fc52c04e78d713 100644
--- a/spec/tasks/gitlab/db_rake_spec.rb
+++ b/spec/tasks/gitlab/db_rake_spec.rb
@@ -19,7 +19,7 @@
   end
 
   describe 'configure' do
-    it 'should invoke db:migrate when schema has already been loaded' do
+    it 'invokes db:migrate when schema has already been loaded' do
       allow(ActiveRecord::Base.connection).to receive(:tables).and_return(['default'])
       expect(Rake::Task['db:migrate']).to receive(:invoke)
       expect(Rake::Task['db:schema:load']).not_to receive(:invoke)
@@ -27,7 +27,7 @@
       expect { run_rake_task('gitlab:db:configure') }.not_to raise_error
     end
 
-    it 'should invoke db:shema:load and db:seed_fu when schema is not loaded' do
+    it 'invokes db:shema:load and db:seed_fu when schema is not loaded' do
       allow(ActiveRecord::Base.connection).to receive(:tables).and_return([])
       expect(Rake::Task['db:schema:load']).to receive(:invoke)
       expect(Rake::Task['db:seed_fu']).to receive(:invoke)
@@ -35,7 +35,7 @@
       expect { run_rake_task('gitlab:db:configure') }.not_to raise_error
     end
 
-    it 'should not invoke any other rake tasks during an error' do
+    it 'does not invoke any other rake tasks during an error' do
       allow(ActiveRecord::Base).to receive(:connection).and_raise(RuntimeError, 'error')
       expect(Rake::Task['db:migrate']).not_to receive(:invoke)
       expect(Rake::Task['db:schema:load']).not_to receive(:invoke)
@@ -45,7 +45,7 @@
       allow(ActiveRecord::Base).to receive(:connection).and_call_original
     end
 
-    it 'should not invoke seed after a failed schema_load' do
+    it 'does not invoke seed after a failed schema_load' do
       allow(ActiveRecord::Base.connection).to receive(:tables).and_return([])
       allow(Rake::Task['db:schema:load']).to receive(:invoke).and_raise(RuntimeError, 'error')
       expect(Rake::Task['db:schema:load']).to receive(:invoke)
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index 20b1a343c27887..7f803a06902b21 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -22,7 +22,7 @@
     context "branches" do
       let(:changes) { "123456 789012 refs/heads/tést" }
 
-      it "should call GitTagPushService" do
+      it "calls GitTagPushService" do
         expect_any_instance_of(GitPushService).to receive(:execute).and_return(true)
         expect_any_instance_of(GitTagPushService).not_to receive(:execute)
         PostReceive.new.perform(pwd(project), key_id, base64_changes)
@@ -32,7 +32,7 @@
     context "tags" do
       let(:changes) { "123456 789012 refs/tags/tag" }
 
-      it "should call GitTagPushService" do
+      it "calls GitTagPushService" do
         expect_any_instance_of(GitPushService).not_to receive(:execute)
         expect_any_instance_of(GitTagPushService).to receive(:execute).and_return(true)
         PostReceive.new.perform(pwd(project), key_id, base64_changes)
@@ -42,7 +42,7 @@
     context "merge-requests" do
       let(:changes) { "123456 789012 refs/merge-requests/123" }
 
-      it "should not call any of the services" do
+      it "does not call any of the services" do
         expect_any_instance_of(GitPushService).not_to receive(:execute)
         expect_any_instance_of(GitTagPushService).not_to receive(:execute)
         PostReceive.new.perform(pwd(project), key_id, base64_changes)
-- 
GitLab


From 6320a83ae8a2e692e62e7e5c56321c98f5f813a6 Mon Sep 17 00:00:00 2001
From: Connor Shea 
Date: Fri, 10 Jun 2016 11:23:51 -0600
Subject: [PATCH 095/153] Update Grape from 0.13.0 to 0.15.0.

Changelog: https://github.com/ruby-grape/grape/blob/v0.15.0/CHANGELOG.md

Works toward #18226.
---
 Gemfile      | 2 +-
 Gemfile.lock | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/Gemfile b/Gemfile
index 104929665e8f9a..91f4f20215f844 100644
--- a/Gemfile
+++ b/Gemfile
@@ -69,7 +69,7 @@ gem 'gollum-rugged_adapter', '~> 0.4.2', require: false
 gem 'github-linguist', '~> 4.7.0', require: 'linguist'
 
 # API
-gem 'grape',        '~> 0.13.0'
+gem 'grape',        '~> 0.15.0'
 gem 'grape-entity', '~> 0.4.2'
 gem 'rack-cors',    '~> 0.4.0', require: 'rack/cors'
 
diff --git a/Gemfile.lock b/Gemfile.lock
index 87f08d6f372d07..43ed6081274d41 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -308,7 +308,7 @@ GEM
       json
       multi_json
       request_store (>= 1.0)
-    grape (0.13.0)
+    grape (0.15.0)
       activesupport
       builder
       hashie (>= 2.1.0)
@@ -876,7 +876,7 @@ DEPENDENCIES
   gollum-lib (~> 4.2)
   gollum-rugged_adapter (~> 0.4.2)
   gon (~> 6.1.0)
-  grape (~> 0.13.0)
+  grape (~> 0.15.0)
   grape-entity (~> 0.4.2)
   hamlit (~> 2.5)
   health_check (~> 2.1.0)
-- 
GitLab


From c53b599e61027e910aa0425150373cb30c67b150 Mon Sep 17 00:00:00 2001
From: Connor Shea 
Date: Tue, 2 Aug 2016 15:56:27 -0600
Subject: [PATCH 096/153] Retain old behavior

---
 lib/api/api.rb | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/lib/api/api.rb b/lib/api/api.rb
index bd16806892b6ac..6cd4a853dbe9f2 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -7,8 +7,10 @@ class API < Grape::API
       rack_response({ 'message' => '404 Not found' }.to_json, 404)
     end
 
-    rescue_from Grape::Exceptions::ValidationErrors do |e|
-      error!({ messages: e.full_messages }, 400)
+    # Retain 405 error rather than a 500 error for Grape 0.15.0+.
+    # See: https://github.com/ruby-grape/grape/commit/252bfd27c320466ec3c0751812cf44245e97e5de
+    rescue_from Grape::Exceptions::Base do |e|
+      error! e.message, e.status, e.headers
     end
 
     rescue_from :all do |exception|
-- 
GitLab


From 4efc4f5b3131b54922543e0973ce8d5e74051183 Mon Sep 17 00:00:00 2001
From: Connor Shea 
Date: Tue, 9 Aug 2016 08:23:34 -0600
Subject: [PATCH 097/153] Fix Grape tests.

---
 spec/requests/api/users_spec.rb | 15 +++++----------
 1 file changed, 5 insertions(+), 10 deletions(-)

diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 69b5072a81ec2c..e0e041b4e152eb 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -398,9 +398,9 @@
       end.to change{ user.keys.count }.by(1)
     end
 
-    it "returns 405 for invalid ID" do
-      post api("/users/ASDF/keys", admin)
-      expect(response).to have_http_status(405)
+    it "returns 400 for invalid ID" do
+      post api("/users/999999/keys", admin)
+      expect(response).to have_http_status(400)
     end
   end
 
@@ -429,11 +429,6 @@
         expect(json_response).to be_an Array
         expect(json_response.first['title']).to eq(key.title)
       end
-
-      it "returns 405 for invalid ID" do
-        get api("/users/ASDF/keys", admin)
-        expect(response).to have_http_status(405)
-      end
     end
   end
 
@@ -490,8 +485,8 @@
     end
 
     it "raises error for invalid ID" do
-      post api("/users/ASDF/emails", admin)
-      expect(response).to have_http_status(405)
+      post api("/users/999999/emails", admin)
+      expect(response).to have_http_status(400)
     end
   end
 
-- 
GitLab


From da29a55d81d0cc24af513a00d08bee4d920061de Mon Sep 17 00:00:00 2001
From: Connor Shea 
Date: Tue, 9 Aug 2016 10:18:49 -0600
Subject: [PATCH 098/153] Add Changelog entry for Grape upgrade [ci skip]

---
 CHANGELOG | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG b/CHANGELOG
index 7bfeff2a4ec15a..f83a86d75df86a 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -33,6 +33,7 @@ v 8.11.0 (unreleased)
   - Remove delay when hitting "Reply..." button on page with a lot of discussions
   - Retrieve rendered HTML from cache in one request
   - Fix renaming repository when name contains invalid chararacters under project settings
+  - Upgrade Grape from 0.13.0 to 0.15.0. !4601
   - Fix devise deprecation warnings.
   - Update version_sorter and use new interface for faster tag sorting
   - Optimize checking if a user has read access to a list of issues !5370
-- 
GitLab


From befb425c920386fce62b649d43a7a556eb3d3ace Mon Sep 17 00:00:00 2001
From: Taurie Davis 
Date: Tue, 9 Aug 2016 21:57:46 +0000
Subject: [PATCH 099/153] Add svg guidelines to ui guide

---
 doc/development/ui_guide.md | 36 ++++++++++++++++++++++++++++++++++++
 1 file changed, 36 insertions(+)

diff --git a/doc/development/ui_guide.md b/doc/development/ui_guide.md
index 6525228801990c..ca82e9182f7a88 100644
--- a/doc/development/ui_guide.md
+++ b/doc/development/ui_guide.md
@@ -47,6 +47,42 @@ information from database or file system
 * `rss` for rss/atom feed
 * `plus` for link or dropdown that lead to page where you create new object (For example new issue page)
 
+### SVGs
+
+When exporting SVGs, be sure to follow the following guidelines:
+
+1. Convert all strokes to outlines.
+2. Use pathfinder tools to combine overlapping paths and create compound paths.
+3. SVGs that are limited to one color should be exported without a fill color so the color can be set using CSS.
+4. Ensure that exported SVGs have been run through an [SVG cleaner](https://github.com/RazrFalcon/SVGCleaner) to remove unused elements and attributes.
+
+You can open you svg in a text editor to ensure that it is clean. 
+Incorrect file will look like this:
+
+```xml
+
+
+    
+    Group
+    Created with Sketch.
+    
+    
+        
+            
+            
+            
+            
+        
+    
+
+```
+
+Correct file will look like this:
+
+```xml
+
+```
+
 
 ## Buttons
 
-- 
GitLab


From 864bdda4aece14cbae2fff715c3416642aae77a8 Mon Sep 17 00:00:00 2001
From: Taurie Davis 
Date: Tue, 9 Aug 2016 21:59:25 +0000
Subject: [PATCH 100/153] spellcheck

---
 doc/development/ui_guide.md | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/doc/development/ui_guide.md b/doc/development/ui_guide.md
index ca82e9182f7a88..3a8c823e026020 100644
--- a/doc/development/ui_guide.md
+++ b/doc/development/ui_guide.md
@@ -56,8 +56,8 @@ When exporting SVGs, be sure to follow the following guidelines:
 3. SVGs that are limited to one color should be exported without a fill color so the color can be set using CSS.
 4. Ensure that exported SVGs have been run through an [SVG cleaner](https://github.com/RazrFalcon/SVGCleaner) to remove unused elements and attributes.
 
-You can open you svg in a text editor to ensure that it is clean. 
-Incorrect file will look like this:
+You can open your svg in a text editor to ensure that it is clean. 
+Incorrect files will look like this:
 
 ```xml
 
-- 
GitLab


From 1e6316172b913b622379675d3d48e6837dfc1843 Mon Sep 17 00:00:00 2001
From: Stan Hu 
Date: Tue, 9 Aug 2016 14:08:33 -0700
Subject: [PATCH 101/153] Add a method in Project to return a cached value of
 total count of projects

This is in preparation to address the DB load caused by the counting in
gitlab-com/infrastructure#303.
---
 app/models/project.rb       |  6 ++++++
 spec/models/project_spec.rb | 14 ++++++++++++++
 spec/spec_helper.rb         |  7 +++++++
 3 files changed, 27 insertions(+)

diff --git a/app/models/project.rb b/app/models/project.rb
index a667857d058dab..d306f86f783d80 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -378,6 +378,12 @@ def trending(since = 1.month.ago)
 
       joins(join_body).reorder('join_note_counts.amount DESC')
     end
+
+    def cached_count
+      Rails.cache.fetch('total_project_count', expires_in: 5.minutes) do
+        Project.count
+      end
+    end
   end
 
   def repository_storage_path
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 1c3d694075af2e..9c3b4712cab653 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -714,6 +714,20 @@
     it { expect(project.builds_enabled?).to be_truthy }
   end
 
+  describe '.cached_count', caching: true do
+    let(:group)     { create(:group, :public) }
+    let!(:project1) { create(:empty_project, :public, group: group) }
+    let!(:project2) { create(:empty_project, :public, group: group) }
+
+    it 'returns total project count' do
+      expect(Project).to receive(:count).once.and_call_original
+
+      3.times do
+        expect(Project.cached_count).to eq(2)
+      end
+    end
+  end
+
   describe '.trending' do
     let(:group)    { create(:group, :public) }
     let(:project1) { create(:empty_project, :public, group: group) }
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 4f3aacf55be9db..2e2aa7c4fc04e7 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -42,6 +42,13 @@
   config.before(:suite) do
     TestEnv.init
   end
+
+  config.around(:each, :caching) do |example|
+    caching_store = Rails.cache
+    Rails.cache = ActiveSupport::Cache::MemoryStore.new if example.metadata[:caching]
+    example.run
+    Rails.cache = caching_store
+  end
 end
 
 FactoryGirl::SyntaxRunner.class_eval do
-- 
GitLab


From 12e93d6f4b3af60d8f1404aa99625f8978075bf3 Mon Sep 17 00:00:00 2001
From: Robert Speicher 
Date: Tue, 9 Aug 2016 18:29:14 -0500
Subject: [PATCH 102/153] Rename `run` task helper method to prevent conflict
 with StateMachine

This prevents the following message from appearing whenever running a
Rake task:

    Instance method "run" is already defined in Object, use generic
    helper instead or set StateMachines::Machine.ignore_method_conflicts
    = true.
---
 lib/tasks/gitlab/check.rake        |  8 ++++----
 lib/tasks/gitlab/info.rake         |  4 ++--
 lib/tasks/gitlab/task_helpers.rake | 14 +++++++-------
 lib/tasks/spinach.rake             |  6 ++----
 4 files changed, 15 insertions(+), 17 deletions(-)

diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index 60f4636e7379be..0894994200f2a3 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -46,7 +46,7 @@ namespace :gitlab do
       }
 
       correct_options = options.map do |name, value|
-        run(%W(#{Gitlab.config.git.bin_path} config --global --get #{name})).try(:squish) == value
+        run_command(%W(#{Gitlab.config.git.bin_path} config --global --get #{name})).try(:squish) == value
       end
 
       if correct_options.all?
@@ -316,7 +316,7 @@ namespace :gitlab do
       min_redis_version = "2.8.0"
       print "Redis version >= #{min_redis_version}? ... "
 
-      redis_version = run(%W(redis-cli --version))
+      redis_version = run_command(%W(redis-cli --version))
       redis_version = redis_version.try(:match, /redis-cli (\d+\.\d+\.\d+)/)
       if redis_version &&
           (Gem::Version.new(redis_version[1]) > Gem::Version.new(min_redis_version))
@@ -893,7 +893,7 @@ namespace :gitlab do
 
   def check_ruby_version
     required_version = Gitlab::VersionInfo.new(2, 1, 0)
-    current_version = Gitlab::VersionInfo.parse(run(%W(ruby --version)))
+    current_version = Gitlab::VersionInfo.parse(run_command(%W(ruby --version)))
 
     print "Ruby version >= #{required_version} ? ... "
 
@@ -910,7 +910,7 @@ namespace :gitlab do
 
   def check_git_version
     required_version = Gitlab::VersionInfo.new(2, 7, 3)
-    current_version = Gitlab::VersionInfo.parse(run(%W(#{Gitlab.config.git.bin_path} --version)))
+    current_version = Gitlab::VersionInfo.parse(run_command(%W(#{Gitlab.config.git.bin_path} --version)))
 
     puts "Your git bin path is \"#{Gitlab.config.git.bin_path}\""
     print "Git version >= #{required_version} ? ... "
diff --git a/lib/tasks/gitlab/info.rake b/lib/tasks/gitlab/info.rake
index fe43d40e6d23da..dffea8ed155e63 100644
--- a/lib/tasks/gitlab/info.rake
+++ b/lib/tasks/gitlab/info.rake
@@ -8,7 +8,7 @@ namespace :gitlab do
       # check Ruby version
       ruby_version = run_and_match(%W(ruby --version), /[\d\.p]+/).try(:to_s)
       # check Gem version
-      gem_version = run(%W(gem --version))
+      gem_version = run_command(%W(gem --version))
       # check Bundler version
       bunder_version = run_and_match(%W(bundle --version), /[\d\.]+/).try(:to_s)
       # check Bundler version
@@ -17,7 +17,7 @@ namespace :gitlab do
       puts ""
       puts "System information".color(:yellow)
       puts "System:\t\t#{os_name || "unknown".color(:red)}"
-      puts "Current User:\t#{run(%W(whoami))}"
+      puts "Current User:\t#{run_command(%W(whoami))}"
       puts "Using RVM:\t#{rvm_version.present? ? "yes".color(:green) : "no"}"
       puts "RVM Version:\t#{rvm_version}" if rvm_version.present?
       puts "Ruby Version:\t#{ruby_version || "unknown".color(:red)}"
diff --git a/lib/tasks/gitlab/task_helpers.rake b/lib/tasks/gitlab/task_helpers.rake
index ab96b1d35932c9..74be413423aa9e 100644
--- a/lib/tasks/gitlab/task_helpers.rake
+++ b/lib/tasks/gitlab/task_helpers.rake
@@ -23,7 +23,7 @@ namespace :gitlab do
   # It will primarily use lsb_relase to determine the OS.
   # It has fallbacks to Debian, SuSE, OS X and systems running systemd.
   def os_name
-    os_name = run(%W(lsb_release -irs))
+    os_name = run_command(%W(lsb_release -irs))
     os_name ||= if File.readable?('/etc/system-release')
                   File.read('/etc/system-release')
                 end
@@ -34,7 +34,7 @@ namespace :gitlab do
     os_name ||= if File.readable?('/etc/SuSE-release')
                   File.read('/etc/SuSE-release')
                 end
-    os_name ||= if os_x_version = run(%W(sw_vers -productVersion))
+    os_name ||= if os_x_version = run_command(%W(sw_vers -productVersion))
                   "Mac OS X #{os_x_version}"
                 end
     os_name ||= if File.readable?('/etc/os-release')
@@ -62,10 +62,10 @@ namespace :gitlab do
   # Returns nil if nothing matched
   # Returns the MatchData if the pattern matched
   #
-  # see also #run
+  # see also #run_command
   # see also String#match
   def run_and_match(command, regexp)
-    run(command).try(:match, regexp)
+    run_command(command).try(:match, regexp)
   end
 
   # Runs the given command
@@ -74,7 +74,7 @@ namespace :gitlab do
   # Returns the output of the command otherwise
   #
   # see also #run_and_match
-  def run(command)
+  def run_command(command)
     output, _ = Gitlab::Popen.popen(command)
     output
   rescue Errno::ENOENT
@@ -82,7 +82,7 @@ namespace :gitlab do
   end
 
   def uid_for(user_name)
-    run(%W(id -u #{user_name})).chomp.to_i
+    run_command(%W(id -u #{user_name})).chomp.to_i
   end
 
   def gid_for(group_name)
@@ -96,7 +96,7 @@ namespace :gitlab do
   def warn_user_is_not_gitlab
     unless @warned_user_not_gitlab
       gitlab_user = Gitlab.config.gitlab.user
-      current_user = run(%W(whoami)).chomp
+      current_user = run_command(%W(whoami)).chomp
       unless current_user == gitlab_user
         puts " Warning ".color(:black).background(:yellow)
         puts "  You are running as user #{current_user.color(:magenta)}, we hope you know what you are doing."
diff --git a/lib/tasks/spinach.rake b/lib/tasks/spinach.rake
index da255f5464b04f..c0f860a82d2aac 100644
--- a/lib/tasks/spinach.rake
+++ b/lib/tasks/spinach.rake
@@ -34,17 +34,15 @@ task :spinach do
   run_spinach_tests(nil)
 end
 
-def run_command(cmd)
+def run_system_command(cmd)
   system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd)
 end
 
 def run_spinach_command(args)
-  run_command(%w(spinach -r rerun) + args)
+  run_system_command(%w(spinach -r rerun) + args)
 end
 
 def run_spinach_tests(tags)
-  #run_command(%w(rake gitlab:setup)) or raise('gitlab:setup failed!')
-
   success = run_spinach_command(%W(--tags #{tags}))
   3.times do |_|
     break if success
-- 
GitLab


From 3756bbe12c922ad23dd5ab6302cf64b7bafe84ba Mon Sep 17 00:00:00 2001
From: Grzegorz Bizon 
Date: Wed, 10 Aug 2016 08:44:19 +0200
Subject: [PATCH 103/153] Add missing space to generic badge template

---
 app/views/projects/badges/badge.svg.erb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app/views/projects/badges/badge.svg.erb b/app/views/projects/badges/badge.svg.erb
index 9d9a919f915fb7..a5fef4fc56faf2 100644
--- a/app/views/projects/badges/badge.svg.erb
+++ b/app/views/projects/badges/badge.svg.erb
@@ -12,7 +12,7 @@
     
     
+          d="M<%= badge.key_width %> 0 h<%= badge.value_width %> v20 H<%= badge.key_width %> z"/>
     
   
-- 
GitLab


From ee451ea5739c5d574a5c3c7db36c5a4c28206fd5 Mon Sep 17 00:00:00 2001
From: Yorick Peterse 
Date: Mon, 8 Aug 2016 17:42:26 +0200
Subject: [PATCH 104/153] Mention add_column_with_default in downtime guide

[ci skip]
---
 doc/development/what_requires_downtime.md | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/doc/development/what_requires_downtime.md b/doc/development/what_requires_downtime.md
index abd693cf72dd70..2574c2c04727c5 100644
--- a/doc/development/what_requires_downtime.md
+++ b/doc/development/what_requires_downtime.md
@@ -31,6 +31,14 @@ operation, even when using `ALGORITHM=INPLACE` and `LOCK=NONE`. This means
 downtime _may_ be required when modifying large tables as otherwise the
 operation could potentially take hours to complete.
 
+Adding a column with a default value _can_ be done without requiring downtime
+when using the migration helper method
+`Gitlab::Database::MigrationHelpers#add_column_with_default`. This method works
+similar to `add_column` except it updates existing rows in batches without
+blocking access to the table being modified. See ["Adding Columns With Default
+Values"](migration_style_guide.html#adding-columns-with-default-values) for more
+information on how to use this method.
+
 ## Dropping Columns
 
 On PostgreSQL you can safely remove an existing column without the need for
-- 
GitLab


From ef46221607442aaa5c2eb2c16b5f22f54abb9d3c Mon Sep 17 00:00:00 2001
From: "Z.J. van de Weg" 
Date: Wed, 10 Aug 2016 11:10:04 +0200
Subject: [PATCH 105/153] Remove duplicate link_to statements

---
 app/views/projects/new.html.haml | 18 ++++++------------
 1 file changed, 6 insertions(+), 12 deletions(-)

diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 432887c1adb0f2..adcc984f5067ce 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -49,21 +49,15 @@
                       = icon('github', text: 'GitHub')
                 %div
                   - if bitbucket_import_enabled?
-                    - if bitbucket_import_configured?
-                      = link_to status_import_bitbucket_path, class: 'btn import_bitbucket', "data-no-turbolink" => "true" do
-                        = icon('bitbucket', text: 'Bitbucket')
-                    - else
-                      = link_to status_import_bitbucket_path, class: 'how_to_import_link btn import_bitbucket', "data-no-turbolink" => "true" do
-                        = icon('bitbucket', text: 'Bitbucket')
+                    = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}", "data-no-turbolink" => "true" do
+                      = icon('bitbucket', text: 'Bitbucket')
+                    - unless bitbucket_import_configured?
                       = render 'bitbucket_import_modal'
                 %div
                   - if gitlab_import_enabled?
-                    - if gitlab_import_configured?
-                      = link_to status_import_gitlab_path, class: 'btn import_gitlab' do
-                        = icon('gitlab', text: 'GitLab.com')
-                    - else
-                      = link_to status_import_gitlab_path, class: 'how_to_import_link btn import_gitlab' do
-                        = icon('gitlab', text: 'GitLab.com')
+                    = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless bitbucket_import_configured?}" do
+                      = icon('gitlab', text: 'GitLab.com')
+                    - unless gitlab_import_configured?
                       = render 'gitlab_import_modal'
                 %div
                   - if gitorious_import_enabled?
-- 
GitLab


From 472a6a1c9deacafcba8569879df44039aa59a203 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A9my=20Coutable?= 
Date: Wed, 10 Aug 2016 12:16:03 +0200
Subject: [PATCH 106/153] Used cached value of project count to reduce DB load
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Rémy Coutable 
---
 CHANGELOG                                 | 1 +
 app/views/admin/dashboard/index.html.haml | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG b/CHANGELOG
index 42d32e53685423..f83d357e4c24ab 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -90,6 +90,7 @@ v 8.11.0 (unreleased)
   - Sort folders with submodules in Files view !5521
 
 v 8.10.5 (unreleased)
+  - Cache project count for 5 minutes to reduce DB load
 
 v 8.10.4
   - Don't close referenced upstream issues from a forked project.
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 452fc25ab07552..e6687f43816d81 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -112,7 +112,7 @@
           %h4 Projects
           .data
             = link_to admin_namespaces_projects_path do
-              %h1= number_with_delimiter(Project.count)
+              %h1= number_with_delimiter(Project.cached_count)
             %hr
             = link_to('New Project', new_project_path, class: "btn btn-new")
       .col-sm-4
-- 
GitLab


From 023d4812586faa24cce69715c606b4bf236956e9 Mon Sep 17 00:00:00 2001
From: Yorick Peterse 
Date: Wed, 10 Aug 2016 13:05:41 +0200
Subject: [PATCH 107/153] Removed extra newline from redis_spec.rb

---
 spec/lib/gitlab/redis_spec.rb | 1 -
 1 file changed, 1 deletion(-)

diff --git a/spec/lib/gitlab/redis_spec.rb b/spec/lib/gitlab/redis_spec.rb
index 879ed30841cc1d..e54f5ffb312493 100644
--- a/spec/lib/gitlab/redis_spec.rb
+++ b/spec/lib/gitlab/redis_spec.rb
@@ -67,7 +67,6 @@
       expect(subject).to receive(:fetch_config) { 'redis://myredis:6379' }
       expect(subject.send(:raw_config_hash)).to eq(url: 'redis://myredis:6379')
     end
-
   end
 
   describe '#fetch_config' do
-- 
GitLab


From 17dd3e89d5a43761e85d7af30ad083db6ca5834b Mon Sep 17 00:00:00 2001
From: Yorick Peterse 
Date: Wed, 10 Aug 2016 12:29:06 +0200
Subject: [PATCH 108/153] Remove trigram indexes for "ci_runners"

These indexes are only used when you search for runners in the admin
interface. This operation is so rarely used that it does not make sense
to slow down every update in order to update the GIN trigram indexes.

Removing these indexes should speed up queries such as those used for
updating the last contact time of CI runners. Locally the timings of
this query were reduced from ~50 ms to ~25 ms:

    UPDATE ci_runners SET updated_at = now(), contacted_at = now();
---
 CHANGELOG                                     |  1 +
 ...102349_remove_ci_runner_trigram_indexes.rb | 27 +++++++++++++++++++
 db/schema.rb                                  |  4 +--
 3 files changed, 29 insertions(+), 3 deletions(-)
 create mode 100644 db/migrate/20160810102349_remove_ci_runner_trigram_indexes.rb

diff --git a/CHANGELOG b/CHANGELOG
index 42d32e53685423..a317b69941b2c7 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -36,6 +36,7 @@ v 8.11.0 (unreleased)
   - Retrieve rendered HTML from cache in one request
   - Fix renaming repository when name contains invalid chararacters under project settings
   - Upgrade Grape from 0.13.0 to 0.15.0. !4601
+  - Trigram indexes for the "ci_runners" table have been removed to speed up UPDATE queries
   - Fix devise deprecation warnings.
   - Update version_sorter and use new interface for faster tag sorting
   - Optimize checking if a user has read access to a list of issues !5370
diff --git a/db/migrate/20160810102349_remove_ci_runner_trigram_indexes.rb b/db/migrate/20160810102349_remove_ci_runner_trigram_indexes.rb
new file mode 100644
index 00000000000000..0cfb637804bb56
--- /dev/null
+++ b/db/migrate/20160810102349_remove_ci_runner_trigram_indexes.rb
@@ -0,0 +1,27 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveCiRunnerTrigramIndexes < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  # Disabled for the "down" method so the indexes can be re-created concurrently.
+  disable_ddl_transaction!
+
+  def up
+    return unless Gitlab::Database.postgresql?
+
+    transaction do
+      execute 'DROP INDEX IF EXISTS index_ci_runners_on_token_trigram;'
+      execute 'DROP INDEX IF EXISTS index_ci_runners_on_description_trigram;'
+    end
+  end
+
+  def down
+    return unless Gitlab::Database.postgresql?
+
+    execute 'CREATE INDEX CONCURRENTLY index_ci_runners_on_token_trigram ON ci_runners USING gin(token gin_trgm_ops);'
+    execute 'CREATE INDEX CONCURRENTLY index_ci_runners_on_description_trigram ON ci_runners USING gin(description gin_trgm_ops);'
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 71980a6d51f9d1..b795eeaa23cb41 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 20160804150737) do
+ActiveRecord::Schema.define(version: 20160810102349) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
@@ -301,10 +301,8 @@
     t.boolean  "locked",       default: false, null: false
   end
 
-  add_index "ci_runners", ["description"], name: "index_ci_runners_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
   add_index "ci_runners", ["locked"], name: "index_ci_runners_on_locked", using: :btree
   add_index "ci_runners", ["token"], name: "index_ci_runners_on_token", using: :btree
-  add_index "ci_runners", ["token"], name: "index_ci_runners_on_token_trigram", using: :gin, opclasses: {"token"=>"gin_trgm_ops"}
 
   create_table "ci_services", force: :cascade do |t|
     t.string   "type"
-- 
GitLab


From 5689e8a0827eb9f09b071bbd4dc74dc2cb0a3e7d Mon Sep 17 00:00:00 2001
From: Paco Guzman 
Date: Wed, 10 Aug 2016 11:28:42 +0200
Subject: [PATCH 109/153] Avoid commit lookup on diff_helper

---
 CHANGELOG                                | 1 +
 app/helpers/diff_helper.rb               | 5 ++---
 app/views/projects/diffs/_file.html.haml | 2 +-
 3 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/CHANGELOG b/CHANGELOG
index 42d32e53685423..085a9f2ee75c87 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -80,6 +80,7 @@ v 8.11.0 (unreleased)
   - Sensible state specific default sort order for issues and merge requests !5453 (tomb0y)
   - Fix RequestProfiler::Middleware error when code is reloaded in development
   - Catch what warden might throw when profiling requests to re-throw it
+  - Avoid commit lookup on diff_helper passing existing local variable to the helper method
   - Add description to new_issue email and new_merge_request_email in text/plain content type. !5663 (dixpac)
   - Speed up and reduce memory usage of Commit#repo_changes, Repository#expire_avatar_cache and IrkerWorker
   - Add unfold links for Side-by-Side view. !5415 (Tim Masliuchenko)
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index f3c9ea074b470d..0725c3f4c56c81 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -109,11 +109,10 @@ def commit_for_diff(diff_file)
     end
   end
 
-  def diff_file_html_data(project, diff_file)
-    commit = commit_for_diff(diff_file)
+  def diff_file_html_data(project, diff_file_path, diff_commit_id)
     {
       blob_diff_path: namespace_project_blob_diff_path(project.namespace, project,
-                                                       tree_join(commit.id, diff_file.file_path)),
+                                                       tree_join(diff_commit_id, diff_file_path)),
       view: diff_view
     }
   end
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index f0a86fd6d40e05..8fbd89100ca5e2 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -1,4 +1,4 @@
-.diff-file.file-holder{id: "diff-#{index}", data: diff_file_html_data(project, diff_file)}
+.diff-file.file-holder{id: "diff-#{index}", data: diff_file_html_data(project, diff_file.file_path, diff_commit.id)}
   .file-title{id: "file-path-#{hexdigest(diff_file.file_path)}"}
     = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_commit, project: project, url: "#diff-#{index}"
 
-- 
GitLab


From 01c0039c55746e180a24aef1c9b3b8ee5afb53e9 Mon Sep 17 00:00:00 2001
From: Achilleas Pipinellis 
Date: Wed, 10 Aug 2016 14:28:56 +0300
Subject: [PATCH 110/153] Small refactor of doc/development/README.md

- Fix wrong doc_styleguide URL
- Clarify migrations refer to SQL
- Capitalize CSS
---
 doc/development/README.md | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

diff --git a/doc/development/README.md b/doc/development/README.md
index e64b23277718c0..bf67b5d8dff0ee 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -8,14 +8,13 @@
 
 ## Styleguides
 
-- [Documentation styleguide](development/doc_styleguide.md) Use this styleguide if you are
+- [Documentation styleguide](doc_styleguide.md) Use this styleguide if you are
   contributing to documentation.
-- [Migration Style Guide](migration_style_guide.md) for creating safe migrations
+- [SQL Migration Style Guide](migration_style_guide.md) for creating safe SQL migrations
 - [Testing standards and style guidelines](testing.md)
-- [UI guide](ui_guide.md) for building GitLab with existing css styles and elements
+- [UI guide](ui_guide.md) for building GitLab with existing CSS styles and elements
 - [SQL guidelines](sql.md) for SQL guidelines
 
-
 ## Process
 
 - [Code review guidelines](code_review.md) for reviewing code and having code reviewed.
-- 
GitLab


From 0012de8c8a6642db40c13273f32c396afaa629eb Mon Sep 17 00:00:00 2001
From: Jacob Vosmaer 
Date: Wed, 10 Aug 2016 16:48:21 +0200
Subject: [PATCH 111/153] Rename lfs_enabled helper method

---
 app/controllers/projects/lfs_api_controller.rb     | 2 +-
 app/controllers/projects/lfs_storage_controller.rb | 2 +-
 app/helpers/lfs_helper.rb                          | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/app/controllers/projects/lfs_api_controller.rb b/app/controllers/projects/lfs_api_controller.rb
index 1fa9cd8f47f83a..ece49dcd922572 100644
--- a/app/controllers/projects/lfs_api_controller.rb
+++ b/app/controllers/projects/lfs_api_controller.rb
@@ -1,7 +1,7 @@
 class Projects::LfsApiController < Projects::GitHttpClientController
   include LfsHelper
 
-  before_action :lfs_enabled!
+  before_action :require_lfs_enabled!
   before_action :lfs_check_access!, except: [:deprecated]
 
   def batch
diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb
index d0895e1324ce57..6bde0a527ffb7a 100644
--- a/app/controllers/projects/lfs_storage_controller.rb
+++ b/app/controllers/projects/lfs_storage_controller.rb
@@ -1,7 +1,7 @@
 class Projects::LfsStorageController < Projects::GitHttpClientController
   include LfsHelper
 
-  before_action :lfs_enabled!
+  before_action :require_lfs_enabled!
   before_action :lfs_check_access!
 
   def download
diff --git a/app/helpers/lfs_helper.rb b/app/helpers/lfs_helper.rb
index ae230ee18782d0..eb651e3687eb64 100644
--- a/app/helpers/lfs_helper.rb
+++ b/app/helpers/lfs_helper.rb
@@ -1,5 +1,5 @@
 module LfsHelper
-  def lfs_enabled!
+  def require_lfs_enabled!
     return if Gitlab.config.lfs.enabled
 
     render(
-- 
GitLab


From f817eecb22517ece0344977d00ecc7ddfff30594 Mon Sep 17 00:00:00 2001
From: Jacob Vosmaer 
Date: Wed, 10 Aug 2016 16:49:23 +0200
Subject: [PATCH 112/153] Use && and || instead of if

---
 .../projects/lfs_storage_controller.rb            | 15 +++------------
 1 file changed, 3 insertions(+), 12 deletions(-)

diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb
index 6bde0a527ffb7a..a80fa525631aa5 100644
--- a/app/controllers/projects/lfs_storage_controller.rb
+++ b/app/controllers/projects/lfs_storage_controller.rb
@@ -68,22 +68,13 @@ def tmp_filename
   end
 
   def store_file(oid, size, tmp_file)
+    # Define tmp_file_path early because we use it in "ensure"
     tmp_file_path = File.join("#{Gitlab.config.lfs.storage_path}/tmp/upload", tmp_file)
 
     object = LfsObject.find_or_create_by(oid: oid, size: size)
-    if object.file.exists?
-      success = true
-    else
-      success = move_tmp_file_to_storage(object, tmp_file_path)
-    end
-
-    if success
-      success = link_to_project(object)
-    end
-
-    success
+    file_exists = object.file.exists? || move_tmp_file_to_storage(object, tmp_file_path)
+    file_exists && link_to_project(object)
   ensure
-    # Ensure that the tmp file is removed
     FileUtils.rm_f(tmp_file_path)
   end
 
-- 
GitLab


From 26b98bfff8c5bb7048bcbec46e028e30c46bccc5 Mon Sep 17 00:00:00 2001
From: Jacob Vosmaer 
Date: Wed, 10 Aug 2016 17:40:20 +0200
Subject: [PATCH 113/153] Improve validation of X-Gitlab-Lfs-Tmp header

---
 .../projects/lfs_storage_controller.rb           | 10 +++-------
 spec/requests/lfs_http_spec.rb                   | 16 ++++++++++++++--
 2 files changed, 17 insertions(+), 9 deletions(-)

diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb
index a80fa525631aa5..69066cb40e6712 100644
--- a/app/controllers/projects/lfs_storage_controller.rb
+++ b/app/controllers/projects/lfs_storage_controller.rb
@@ -58,13 +58,9 @@ def size
 
   def tmp_filename
     name = request.headers['X-Gitlab-Lfs-Tmp']
-    if name.present?
-      name.gsub!(/^.*(\\|\/)/, '')
-      name = name.match(/[0-9a-f]{73}/)
-      name[0] if name
-    else
-      nil
-    end
+    return if name.include?('/')
+    return unless oid.present? && name.start_with?(oid)
+    name
   end
 
   def store_file(oid, size, tmp_file)
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index 26572699bf8a06..9db282a9f3d7c9 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -556,7 +556,7 @@
 
       context 'and request is sent with a malformed headers' do
         before do
-          put_finalize('cat /etc/passwd')
+          put_finalize('/etc/passwd')
         end
 
         it 'does not recognize it as a valid lfs command' do
@@ -588,7 +588,7 @@
 
       context 'and request is sent with a malformed headers' do
         before do
-          put_finalize('cat /etc/passwd')
+          put_finalize('/etc/passwd')
         end
 
         it 'does not recognize it as a valid lfs command' do
@@ -636,6 +636,18 @@
             it 'lfs object is linked to the project' do
               expect(lfs_object.projects.pluck(:id)).to include(project.id)
             end
+          en
+
+          context 'invalid tempfiles' do
+            it 'rejects slashes in the tempfile name (path traversal' do
+              put_finalize('foo/bar')
+              expect(response).to have_http_status(403)
+            end
+
+            it 'rejects tempfile names that do not start with the oid' do
+              put_finalize("foo#{sample_oid}")
+              expect(response).to have_http_status(403)
+            end
           end
         end
 
-- 
GitLab


From 4955a47cb1c52168114364e45a2fccf6bc105452 Mon Sep 17 00:00:00 2001
From: Stan Hu 
Date: Sat, 6 Aug 2016 07:25:51 -0700
Subject: [PATCH 114/153] Clean up project destruction

Instead of redirecting from the project service to the service and back to the model,
put all destruction code in the service. Also removes a possible source of failure
where run_after_commit may not destroy the project.
---
 app/controllers/projects_controller.rb    |  2 +-
 app/models/project.rb                     | 10 ----------
 app/services/delete_user_service.rb       |  2 +-
 app/services/destroy_group_service.rb     |  2 +-
 app/services/projects/destroy_service.rb  |  8 ++++++--
 lib/api/projects.rb                       |  2 +-
 spec/models/hooks/system_hook_spec.rb     |  2 +-
 spec/services/delete_user_service_spec.rb |  2 +-
 8 files changed, 12 insertions(+), 18 deletions(-)

diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index a6e1aa5ccc1f69..207f9d6a77fb09 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -125,7 +125,7 @@ def show
   def destroy
     return access_denied! unless can?(current_user, :remove_project, @project)
 
-    ::Projects::DestroyService.new(@project, current_user, {}).pending_delete!
+    ::Projects::DestroyService.new(@project, current_user, {}).async_execute
     flash[:alert] = "Project '#{@project.name}' will be deleted."
 
     redirect_to dashboard_projects_path
diff --git a/app/models/project.rb b/app/models/project.rb
index d306f86f783d80..3b1a53edc7542a 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -1161,16 +1161,6 @@ def wiki
     @wiki ||= ProjectWiki.new(self, self.owner)
   end
 
-  def schedule_delete!(user_id, params)
-    # Queue this task for after the commit, so once we mark pending_delete it will run
-    run_after_commit do
-      job_id = ProjectDestroyWorker.perform_async(id, user_id, params)
-      Rails.logger.info("User #{user_id} scheduled destruction of project #{path_with_namespace} with job ID #{job_id}")
-    end
-
-    update_attribute(:pending_delete, true)
-  end
-
   def running_or_pending_build_count(force: false)
     Rails.cache.fetch(['projects', id, 'running_or_pending_build_count'], force: force) do
       builds.running_or_pending.count(:all)
diff --git a/app/services/delete_user_service.rb b/app/services/delete_user_service.rb
index ce79287e35a492..2f237de813c6b3 100644
--- a/app/services/delete_user_service.rb
+++ b/app/services/delete_user_service.rb
@@ -18,7 +18,7 @@ def execute(user, options = {})
     user.personal_projects.each do |project|
       # Skip repository removal because we remove directory with namespace
       # that contain all this repositories
-      ::Projects::DestroyService.new(project, current_user, skip_repo: true).pending_delete!
+      ::Projects::DestroyService.new(project, current_user, skip_repo: true).async_execute
     end
 
     user.destroy
diff --git a/app/services/destroy_group_service.rb b/app/services/destroy_group_service.rb
index 3c42ac61be4e3e..a4ebccb5606509 100644
--- a/app/services/destroy_group_service.rb
+++ b/app/services/destroy_group_service.rb
@@ -9,7 +9,7 @@ def execute
     group.projects.each do |project|
       # Skip repository removal because we remove directory with namespace
       # that contain all this repositories
-      ::Projects::DestroyService.new(project, current_user, skip_repo: true).pending_delete!
+      ::Projects::DestroyService.new(project, current_user, skip_repo: true).async_execute
     end
 
     group.destroy
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index 882606e38d0e9c..8a53f65aec1a82 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -6,8 +6,12 @@ class DestroyError < StandardError; end
 
     DELETED_FLAG = '+deleted'
 
-    def pending_delete!
-      project.schedule_delete!(current_user.id, params)
+    def async_execute
+      project.transaction do
+        project.update_attribute(:pending_delete, true)
+        job_id = ProjectDestroyWorker.perform_async(project.id, current_user.id, params)
+        Rails.logger.info("User #{current_user.id} scheduled destruction of project #{project.path_with_namespace} with job ID #{job_id}")
+      end
     end
 
     def execute
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 8fed7db8803992..60cfc103afd27f 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -323,7 +323,7 @@ def map_public_to_visibility_level(attrs)
       #   DELETE /projects/:id
       delete ":id" do
         authorize! :remove_project, user_project
-        ::Projects::DestroyService.new(user_project, current_user, {}).pending_delete!
+        ::Projects::DestroyService.new(user_project, current_user, {}).async_execute
       end
 
       # Mark this project as forked from another
diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb
index 4078b9e4ff54a5..cbdf7eec082d48 100644
--- a/spec/models/hooks/system_hook_spec.rb
+++ b/spec/models/hooks/system_hook_spec.rb
@@ -38,7 +38,7 @@
     end
 
     it "project_destroy hook" do
-      Projects::DestroyService.new(project, user, {}).pending_delete!
+      Projects::DestroyService.new(project, user, {}).async_execute
 
       expect(WebMock).to have_requested(:post, system_hook.url).with(
         body: /project_destroy/,
diff --git a/spec/services/delete_user_service_spec.rb b/spec/services/delete_user_service_spec.rb
index a65938fa03b4ed..630458f9efcb4f 100644
--- a/spec/services/delete_user_service_spec.rb
+++ b/spec/services/delete_user_service_spec.rb
@@ -15,7 +15,7 @@
       end
 
       it 'will delete the project in the near future' do
-        expect_any_instance_of(Projects::DestroyService).to receive(:pending_delete!).once
+        expect_any_instance_of(Projects::DestroyService).to receive(:async_execute).once
 
         DeleteUserService.new(current_user).execute(user)
       end
-- 
GitLab


From 34d5426f0e17a9d0a2d2330b472114e7e457ae05 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A9my=20Coutable?= 
Date: Wed, 10 Aug 2016 18:44:44 +0200
Subject: [PATCH 115/153] Update CHANGELOG for 8.10.5
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Rémy Coutable 
---
 CHANGELOG | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/CHANGELOG b/CHANGELOG
index 2cff1ff2ee1486..5aa5cbec279942 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -90,8 +90,10 @@ v 8.11.0 (unreleased)
   - Fix importing GitLab projects with an invalid MR source project
   - Sort folders with submodules in Files view !5521
 
-v 8.10.5 (unreleased)
-  - Cache project count for 5 minutes to reduce DB load
+v 8.10.5
+  - Add a data migration to fix some missing timestamps in the members table. !5670
+  - Revert the "Defend against 'Host' header injection" change in the source NGINX templates. !5706
+  - Cache project count for 5 minutes to reduce DB load. !5746 & !5754
 
 v 8.10.4
   - Don't close referenced upstream issues from a forked project.
-- 
GitLab


From 29850364eccccc3ce7305f6706cea1d5d073de2e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A9my=20Coutable?= 
Date: Thu, 23 Jun 2016 17:14:31 +0200
Subject: [PATCH 116/153] New AccessRequests API endpoints for Group & Project
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Also, mutualize AccessRequests and Members endpoints for Group &
Project.
New API documentation for the AccessRequests endpoints.

Signed-off-by: Rémy Coutable 
---
 CHANGELOG                                   |   1 +
 app/models/members/project_member.rb        |   1 +
 app/models/project.rb                       |   4 +
 app/services/members/destroy_service.rb     |   5 +-
 doc/api/README.md                           |   6 +-
 doc/api/access_requests.md                  | 147 +++++++++
 doc/api/groups.md                           |  82 +----
 doc/api/members.md                          | 182 ++++++++++++
 doc/api/projects.md                         |  90 +-----
 lib/api/access_requests.rb                  |  91 ++++++
 lib/api/api.rb                              |   8 +-
 lib/api/entities.rb                         |  24 +-
 lib/api/group_members.rb                    |  87 ------
 lib/api/helpers.rb                          |  25 +-
 lib/api/helpers/members_helpers.rb          |  13 +
 lib/api/members.rb                          | 124 ++++++++
 lib/api/project_members.rb                  | 110 -------
 spec/models/member_spec.rb                  |   2 +-
 spec/models/members/project_member_spec.rb  |   3 +-
 spec/requests/api/access_requests_spec.rb   | 246 +++++++++++++++
 spec/requests/api/group_members_spec.rb     |  10 +-
 spec/requests/api/members_spec.rb           | 312 ++++++++++++++++++++
 spec/requests/api/project_members_spec.rb   |  31 +-
 spec/support/api/members_shared_examples.rb |  11 +
 24 files changed, 1189 insertions(+), 426 deletions(-)
 create mode 100644 doc/api/access_requests.md
 create mode 100644 doc/api/members.md
 create mode 100644 lib/api/access_requests.rb
 delete mode 100644 lib/api/group_members.rb
 create mode 100644 lib/api/helpers/members_helpers.rb
 create mode 100644 lib/api/members.rb
 delete mode 100644 lib/api/project_members.rb
 create mode 100644 spec/requests/api/access_requests_spec.rb
 create mode 100644 spec/requests/api/members_spec.rb
 create mode 100644 spec/support/api/members_shared_examples.rb

diff --git a/CHANGELOG b/CHANGELOG
index 88588059a357d4..898a21d221ae2f 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -7,6 +7,7 @@ v 8.11.0 (unreleased)
   - Improve diff performance by eliminating redundant checks for text blobs
   - Convert switch icon into icon font (ClemMakesApps)
   - API: Endpoints for enabling and disabling deploy keys
+  - API: List access requests, request access, approve, and deny access requests to a project or a group. !4833
   - Use long options for curl examples in documentation !5703 (winniehell)
   - Remove magic comments (`# encoding: UTF-8`) from Ruby files. !5456 (winniehell)
   - Add support for relative links starting with ./ or / to RelativeLinkFilter (winniehell)
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index f176feddbad75f..18e97c969d76d9 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -8,6 +8,7 @@ class ProjectMember < Member
   # Make sure project member points only to project as it source
   default_value_for :source_type, SOURCE_TYPE
   validates_format_of :source_type, with: /\AProject\z/
+  validates :access_level, inclusion: { in: Gitlab::Access.values }
   default_scope { where(source_type: SOURCE_TYPE) }
 
   scope :in_project, ->(project) { where(source_id: project.id) }
diff --git a/app/models/project.rb b/app/models/project.rb
index 3b1a53edc7542a..e0b28160937ad3 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -999,6 +999,10 @@ def project_member(user)
     project_members.find_by(user_id: user)
   end
 
+  def add_user(user, access_level, current_user = nil)
+    team.add_user(user, access_level, current_user)
+  end
+
   def default_branch
     @default_branch ||= repository.root_ref if repository.exists?
   end
diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb
index 15358f80208ea5..9e3f6af628daa4 100644
--- a/app/services/members/destroy_service.rb
+++ b/app/services/members/destroy_service.rb
@@ -2,8 +2,9 @@ module Members
   class DestroyService < BaseService
     attr_accessor :member, :current_user
 
-    def initialize(member, user)
-      @member, @current_user = member, user
+    def initialize(member, current_user)
+      @member = member
+      @current_user = current_user
     end
 
     def execute
diff --git a/doc/api/README.md b/doc/api/README.md
index a357af3831d7ad..f3117815c7cce9 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -16,6 +16,8 @@ following locations:
 - [Commits](commits.md)
 - [Deploy Keys](deploy_keys.md)
 - [Groups](groups.md)
+- [Group Access Requests](access_requests.md)
+- [Group Members](members.md)
 - [Issues](issues.md)
 - [Keys](keys.md)
 - [Labels](labels.md)
@@ -25,6 +27,8 @@ following locations:
 - [Namespaces](namespaces.md)
 - [Notes](notes.md) (comments)
 - [Projects](projects.md) including setting Webhooks
+- [Project Access Requests](access_requests.md)
+- [Project Members](members.md)
 - [Project Snippets](project_snippets.md)
 - [Repositories](repositories.md)
 - [Repository Files](repository_files.md)
@@ -154,7 +158,7 @@ be returned with status code `403`:
 
 ```json
 {
-  "message": "403 Forbidden: Must be admin to use sudo"
+  "message": "403 Forbidden - Must be admin to use sudo"
 }
 ```
 
diff --git a/doc/api/access_requests.md b/doc/api/access_requests.md
new file mode 100644
index 00000000000000..261585af282f74
--- /dev/null
+++ b/doc/api/access_requests.md
@@ -0,0 +1,147 @@
+# Group and project access requests
+
+ >**Note:** This feature was introduced in GitLab 8.11
+
+ **Valid access levels**
+
+ The access levels are defined in the `Gitlab::Access` module. Currently, these levels are recognized:
+
+```
+10 => Guest access
+20 => Reporter access
+30 => Developer access
+40 => Master access
+50 => Owner access # Only valid for groups
+```
+
+## List access requests for a group or project
+
+Gets a list of access requests viewable by the authenticated user.
+
+Returns `200` if the request succeeds.
+
+```
+GET /groups/:id/access_requests
+GET /projects/:id/access_requests
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id`      | integer/string | yes | The group/project ID or path |
+
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/access_requests
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/access_requests
+```
+
+Example response:
+
+```json
+[
+ {
+   "id": 1,
+   "username": "raymond_smith",
+   "name": "Raymond Smith",
+   "state": "active",
+   "created_at": "2012-10-22T14:13:35Z",
+   "requested_at": "2012-10-22T14:13:35Z"
+ },
+ {
+   "id": 2,
+   "username": "john_doe",
+   "name": "John Doe",
+   "state": "active",
+   "created_at": "2012-10-22T14:13:35Z",
+   "requested_at": "2012-10-22T14:13:35Z"
+ }
+]
+```
+
+## Request access to a group or project
+
+Requests access for the authenticated user to a group or project.
+
+Returns `201` if the request succeeds.
+
+```
+POST /groups/:id/access_requests
+POST /projects/:id/access_requests
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id`      | integer/string | yes | The group/project ID or path |
+
+```bash
+curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/access_requests
+curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/access_requests
+```
+
+Example response:
+
+```json
+{
+  "id": 1,
+  "username": "raymond_smith",
+  "name": "Raymond Smith",
+  "state": "active",
+  "created_at": "2012-10-22T14:13:35Z",
+  "requested_at": "2012-10-22T14:13:35Z"
+}
+```
+
+## Approve an access request
+
+Approves an access request for the given user.
+
+Returns `201` if the request succeeds.
+
+```
+PUT /groups/:id/access_requests/:user_id/approve
+PUT /projects/:id/access_requests/:user_id/approve
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id`      | integer/string | yes | The group/project ID or path |
+| `user_id` | integer | yes   | The user ID of the access requester |
+| `access_level` | integer | no | A valid access level (defaults: `30`, developer access level) |
+
+```bash
+curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/access_requests/:user_id/approve?access_level=20
+curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/access_requests/:user_id/approve?access_level=20
+```
+
+Example response:
+
+```json
+{
+  "id": 1,
+  "username": "raymond_smith",
+  "name": "Raymond Smith",
+  "state": "active",
+  "created_at": "2012-10-22T14:13:35Z",
+  "access_level": 20
+}
+```
+
+## Deny an access request
+
+Denies an access request for the given user.
+
+Returns `200` if the request succeeds.
+
+```
+DELETE /groups/:id/access_requests/:user_id
+DELETE /projects/:id/access_requests/:user_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id`      | integer/string | yes | The group/project ID or path |
+| `user_id` | integer | yes   | The user ID of the access requester |
+
+```bash
+curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/access_requests/:user_id
+curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/access_requests/:user_id
+```
diff --git a/doc/api/groups.md b/doc/api/groups.md
index fd665e967a9957..a898387eaa2a68 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -417,87 +417,7 @@ GET /groups?search=foobar
 
 ## Group members
 
-**Group access levels**
-
-The group access levels are defined in the `Gitlab::Access` module. Currently, these levels are recognized:
-
-```
-GUEST     = 10
-REPORTER  = 20
-DEVELOPER = 30
-MASTER    = 40
-OWNER     = 50
-```
-
-### List group members
-
-Get a list of group members viewable by the authenticated user.
-
-```
-GET /groups/:id/members
-```
-
-```json
-[
-  {
-    "id": 1,
-    "username": "raymond_smith",
-    "name": "Raymond Smith",
-    "state": "active",
-    "created_at": "2012-10-22T14:13:35Z",
-    "access_level": 30
-  },
-  {
-    "id": 2,
-    "username": "john_doe",
-    "name": "John Doe",
-    "state": "active",
-    "created_at": "2012-10-22T14:13:35Z",
-    "access_level": 30
-  }
-]
-```
-
-### Add group member
-
-Adds a user to the list of group members.
-
-```
-POST /groups/:id/members
-```
-
-Parameters:
-
-- `id` (required) - The ID or path of a group
-- `user_id` (required) - The ID of a user to add
-- `access_level` (required) - Project access level
-
-### Edit group team member
-
-Updates a group team member to a specified access level.
-
-```
-PUT /groups/:id/members/:user_id
-```
-
-Parameters:
-
-- `id` (required) - The ID of a group
-- `user_id` (required) - The ID of a group member
-- `access_level` (required) - Project access level
-
-### Remove user team member
-
-Removes user from user team.
-
-```
-DELETE /groups/:id/members/:user_id
-```
-
-Parameters:
-
-- `id` (required) - The ID or path of a user group
-- `user_id` (required) - The ID of a group member
+Please consult the [Group Members](members.md) documentation.
 
 ## Namespaces in groups
 
diff --git a/doc/api/members.md b/doc/api/members.md
new file mode 100644
index 00000000000000..4cfc27c300bc53
--- /dev/null
+++ b/doc/api/members.md
@@ -0,0 +1,182 @@
+# Group and project members
+
+**Valid access levels**
+
+The access levels are defined in the `Gitlab::Access` module. Currently, these levels are recognized:
+
+```
+10 => Guest access
+20 => Reporter access
+30 => Developer access
+40 => Master access
+50 => Owner access # Only valid for groups
+```
+
+## List all members of a group or project
+
+Gets a list of group or project members viewable by the authenticated user.
+
+Returns `200` if the request succeeds.
+
+```
+GET /groups/:id/members
+GET /projects/:id/members
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id`      | integer/string | yes | The group/project ID or path |
+| `query`   | string | no     | A query string to search for members |
+
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members
+```
+
+Example response:
+
+```json
+[
+  {
+    "id": 1,
+    "username": "raymond_smith",
+    "name": "Raymond Smith",
+    "state": "active",
+    "created_at": "2012-10-22T14:13:35Z",
+    "access_level": 30
+  },
+  {
+    "id": 2,
+    "username": "john_doe",
+    "name": "John Doe",
+    "state": "active",
+    "created_at": "2012-10-22T14:13:35Z",
+    "access_level": 30
+  }
+]
+```
+
+## Get a member of a group or project
+
+Gets a member of a group or project.
+
+Returns `200` if the request succeeds.
+
+```
+GET /groups/:id/members/:user_id
+GET /projects/:id/members/:user_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id`      | integer/string | yes | The group/project ID or path |
+| `user_id` | integer | yes   | The user ID of the member |
+
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members/:user_id
+```
+
+Example response:
+
+```json
+{
+  "id": 1,
+  "username": "raymond_smith",
+  "name": "Raymond Smith",
+  "state": "active",
+  "created_at": "2012-10-22T14:13:35Z",
+  "access_level": 30
+}
+```
+
+## Add a member to a group or project
+
+Adds a member to a group or project.
+
+Returns `201` if the request succeeds.
+
+```
+POST /groups/:id/members
+POST /projects/:id/members
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id`      | integer/string  | yes | The group/project ID or path |
+| `user_id` | integer         | yes | The user ID of the new member |
+| `access_level` | integer | yes | A valid access level |
+
+```bash
+curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id?access_level=30
+curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members/:user_id?access_level=30
+```
+
+Example response:
+
+```json
+{
+  "id": 1,
+  "username": "raymond_smith",
+  "name": "Raymond Smith",
+  "state": "active",
+  "created_at": "2012-10-22T14:13:35Z",
+  "access_level": 30
+}
+```
+
+## Edit a member of a group or project
+
+Updates a member of a group or project.
+
+Returns `200` if the request succeeds.
+
+```
+PUT /groups/:id/members/:user_id
+PUT /projects/:id/members/:user_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id`      | integer/string | yes | The group/project ID or path |
+| `user_id` | integer | yes   | The user ID of the member |
+| `access_level` | integer | yes | A valid access level |
+
+```bash
+curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id?access_level=40
+curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members/:user_id?access_level=40
+```
+
+Example response:
+
+```json
+{
+  "id": 1,
+  "username": "raymond_smith",
+  "name": "Raymond Smith",
+  "state": "active",
+  "created_at": "2012-10-22T14:13:35Z",
+  "access_level": 40
+}
+```
+
+## Remove a member from a group or project
+
+Removes a user from a group or project.
+
+Returns `200` if the request succeeds.
+
+```
+DELETE /groups/:id/members/:user_id
+DELETE /projects/:id/members/:user_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id`      | integer/string | yes | The group/project ID or path |
+| `user_id` | integer | yes   | The user ID of the member |
+
+```bash
+curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id
+curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members/:user_id
+```
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 727cb44f335074..37d97b2db44691 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -858,95 +858,9 @@ Parameters:
 In Markdown contexts, the link is automatically expanded when the format in `markdown` is used.
 
 
-## Team members
+## Project members
 
-### List project team members
-
-Get a list of a project's team members.
-
-```
-GET /projects/:id/members
-```
-
-Parameters:
-
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `query` (optional) - Query string to search for members
-
-### Get project team member
-
-Gets a project team member.
-
-```
-GET /projects/:id/members/:user_id
-```
-
-Parameters:
-
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `user_id` (required) - The ID of a user
-
-```json
-{
-  "id": 1,
-  "username": "john_smith",
-  "email": "john@example.com",
-  "name": "John Smith",
-  "state": "active",
-  "created_at": "2012-05-23T08:00:58Z",
-  "access_level": 40
-}
-```
-
-### Add project team member
-
-Adds a user to a project team. This is an idempotent method and can be called multiple times
-with the same parameters. Adding team membership to a user that is already a member does not
-affect the existing membership.
-
-```
-POST /projects/:id/members
-```
-
-Parameters:
-
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `user_id` (required) - The ID of a user to add
-- `access_level` (required) - Project access level
-
-### Edit project team member
-
-Updates a project team member to a specified access level.
-
-```
-PUT /projects/:id/members/:user_id
-```
-
-Parameters:
-
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `user_id` (required) - The ID of a team member
-- `access_level` (required) - Project access level
-
-### Remove project team member
-
-Removes a user from a project team.
-
-```
-DELETE /projects/:id/members/:user_id
-```
-
-Parameters:
-
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `user_id` (required) - The ID of a team member
-
-This method removes the project member if the user has the proper access rights to do so.
-It returns a status code 403 if the member does not have the proper rights to perform this action.
-In all other cases this method is idempotent and revoking team membership for a user who is not
-currently a team member is considered success.
-Please note that the returned JSON currently differs slightly. Thus you should not
-rely on the returned JSON structure.
+Please consult the [Project Members](members.md) documentation.
 
 ### Share project with group
 
diff --git a/lib/api/access_requests.rb b/lib/api/access_requests.rb
new file mode 100644
index 00000000000000..9c41d8aaa3e306
--- /dev/null
+++ b/lib/api/access_requests.rb
@@ -0,0 +1,91 @@
+module API
+  class AccessRequests < Grape::API
+    before { authenticate! }
+
+    helpers ::API::Helpers::MembersHelpers
+
+    %w[group project].each do |source_type|
+      resource source_type.pluralize do
+        # Get a list of group/project access requests viewable by the authenticated user.
+        #
+        # Parameters:
+        #   id (required) - The group/project ID
+        #
+        # Example Request:
+        #  GET /groups/:id/access_requests
+        #  GET /projects/:id/access_requests
+        get ":id/access_requests" do
+          source = find_source(source_type, params[:id])
+          authorize_admin_source!(source_type, source)
+
+          access_requesters = source.requesters
+          users = Kaminari.paginate_array(access_requesters.map(&:user))
+
+          present paginate(users), with: Entities::AccessRequester, source: source
+        end
+
+        # Request access to the group/project
+        #
+        # Parameters:
+        #   id (required) - The group/project ID
+        #
+        # Example Request:
+        #  POST /groups/:id/access_requests
+        #  POST /projects/:id/access_requests
+        post ":id/access_requests" do
+          source = find_source(source_type, params[:id])
+          access_requester = source.request_access(current_user)
+
+          if access_requester.persisted?
+            present access_requester.user, with: Entities::AccessRequester, access_requester: access_requester
+          else
+            render_validation_error!(access_requester)
+          end
+        end
+
+        # Approve a group/project access request
+        #
+        # Parameters:
+        #   id (required) - The group/project ID
+        #   user_id (required) - The user ID of the access requester
+        #   access_level (optional) - Access level
+        #
+        # Example Request:
+        #   PUT /groups/:id/access_requests/:user_id/approve
+        #   PUT /projects/:id/access_requests/:user_id/approve
+        put ':id/access_requests/:user_id/approve' do
+          required_attributes! [:user_id]
+          source = find_source(source_type, params[:id])
+          authorize_admin_source!(source_type, source)
+
+          member = source.requesters.find_by!(user_id: params[:user_id])
+          if params[:access_level]
+            member.update(access_level: params[:access_level])
+          end
+          member.accept_request
+
+          status :created
+          present member.user, with: Entities::Member, member: member
+        end
+
+        # Deny a group/project access request
+        #
+        # Parameters:
+        #   id (required) - The group/project ID
+        #   user_id (required) - The user ID of the access requester
+        #
+        # Example Request:
+        #   DELETE /groups/:id/access_requests/:user_id
+        #   DELETE /projects/:id/access_requests/:user_id
+        delete ":id/access_requests/:user_id" do
+          required_attributes! [:user_id]
+          source = find_source(source_type, params[:id])
+
+          access_requester = source.requesters.find_by!(user_id: params[:user_id])
+
+          ::Members::DestroyService.new(access_requester, current_user).execute
+        end
+      end
+    end
+  end
+end
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 6cd4a853dbe9f2..d43af3f24e9586 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -3,6 +3,10 @@ class API < Grape::API
     include APIGuard
     version 'v3', using: :path
 
+    rescue_from Gitlab::Access::AccessDeniedError do
+      rack_response({ 'message' => '403 Forbidden' }.to_json, 403)
+    end
+
     rescue_from ActiveRecord::RecordNotFound do
       rack_response({ 'message' => '404 Not found' }.to_json, 404)
     end
@@ -32,6 +36,7 @@ class API < Grape::API
     # Ensure the namespace is right, otherwise we might load Grape::API::Helpers
     helpers ::API::Helpers
 
+    mount ::API::AccessRequests
     mount ::API::AwardEmoji
     mount ::API::Branches
     mount ::API::Builds
@@ -40,19 +45,18 @@ class API < Grape::API
     mount ::API::DeployKeys
     mount ::API::Environments
     mount ::API::Files
-    mount ::API::GroupMembers
     mount ::API::Groups
     mount ::API::Internal
     mount ::API::Issues
     mount ::API::Keys
     mount ::API::Labels
     mount ::API::LicenseTemplates
+    mount ::API::Members
     mount ::API::MergeRequests
     mount ::API::Milestones
     mount ::API::Namespaces
     mount ::API::Notes
     mount ::API::ProjectHooks
-    mount ::API::ProjectMembers
     mount ::API::ProjectSnippets
     mount ::API::Projects
     mount ::API::Repositories
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index e5b00dc45a52ad..c5ff4557b4a2bc 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -91,9 +91,17 @@ class Project < Grape::Entity
       end
     end
 
-    class ProjectMember < UserBasic
+    class Member < UserBasic
       expose :access_level do |user, options|
-        options[:project].project_members.find_by(user_id: user.id).access_level
+        member = options[:member] || options[:source].members.find_by(user_id: user.id)
+        member.access_level
+      end
+    end
+
+    class AccessRequester < UserBasic
+      expose :requested_at do |user, options|
+        access_requester = options[:access_requester] || options[:source].requesters.find_by(user_id: user.id)
+        access_requester.requested_at
       end
     end
 
@@ -108,12 +116,6 @@ class GroupDetail < Group
       expose :shared_projects, using: Entities::Project
     end
 
-    class GroupMember < UserBasic
-      expose :access_level do |user, options|
-        options[:group].group_members.find_by(user_id: user.id).access_level
-      end
-    end
-
     class RepoBranch < Grape::Entity
       expose :name
 
@@ -325,7 +327,7 @@ class Namespace < Grape::Entity
       expose :id, :path, :kind
     end
 
-    class Member < Grape::Entity
+    class MemberAccess < Grape::Entity
       expose :access_level
       expose :notification_level do |member, options|
         if member.notification_setting
@@ -334,10 +336,10 @@ class Member < Grape::Entity
       end
     end
 
-    class ProjectAccess < Member
+    class ProjectAccess < MemberAccess
     end
 
-    class GroupAccess < Member
+    class GroupAccess < MemberAccess
     end
 
     class ProjectService < Grape::Entity
diff --git a/lib/api/group_members.rb b/lib/api/group_members.rb
deleted file mode 100644
index dbe5bb08d3ff05..00000000000000
--- a/lib/api/group_members.rb
+++ /dev/null
@@ -1,87 +0,0 @@
-module API
-  class GroupMembers < Grape::API
-    before { authenticate! }
-
-    resource :groups do
-      # Get a list of group members viewable by the authenticated user.
-      #
-      # Example Request:
-      #  GET /groups/:id/members
-      get ":id/members" do
-        group = find_group(params[:id])
-        users = group.users
-        present users, with: Entities::GroupMember, group: group
-      end
-
-      # Add a user to the list of group members
-      #
-      # Parameters:
-      #   id (required) - group id
-      #   user_id (required) - the users id
-      #   access_level (required) - Project access level
-      # Example Request:
-      #  POST /groups/:id/members
-      post ":id/members" do
-        group = find_group(params[:id])
-        authorize! :admin_group, group
-        required_attributes! [:user_id, :access_level]
-
-        unless validate_access_level?(params[:access_level])
-          render_api_error!("Wrong access level", 422)
-        end
-
-        if group.group_members.find_by(user_id: params[:user_id])
-          render_api_error!("Already exists", 409)
-        end
-
-        group.add_users([params[:user_id]], params[:access_level], current_user)
-        member = group.group_members.find_by(user_id: params[:user_id])
-        present member.user, with: Entities::GroupMember, group: group
-      end
-
-      # Update group member
-      #
-      # Parameters:
-      #   id (required) - The ID of a group
-      #   user_id (required) - The ID of a group member
-      #   access_level (required) - Project access level
-      # Example Request:
-      #   PUT /groups/:id/members/:user_id
-      put ':id/members/:user_id' do
-        group = find_group(params[:id])
-        authorize! :admin_group, group
-        required_attributes! [:access_level]
-
-        group_member = group.group_members.find_by(user_id: params[:user_id])
-        not_found!('User can not be found') if group_member.nil?
-
-        if group_member.update_attributes(access_level: params[:access_level])
-          @member = group_member.user
-          present @member, with: Entities::GroupMember, group: group
-        else
-          handle_member_errors group_member.errors
-        end
-      end
-
-      # Remove member.
-      #
-      # Parameters:
-      #   id (required) - group id
-      #   user_id (required) - the users id
-      #
-      # Example Request:
-      #   DELETE /groups/:id/members/:user_id
-      delete ":id/members/:user_id" do
-        group = find_group(params[:id])
-        authorize! :admin_group, group
-        member = group.group_members.find_by(user_id: params[:user_id])
-
-        if member.nil?
-          render_api_error!("404 Not Found - user_id:#{params[:user_id]} not a member of group #{group.name}", 404)
-        else
-          member.destroy
-        end
-      end
-    end
-  end
-end
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 130509cdad6ca3..f06c262fd4c79d 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -28,7 +28,7 @@ def current_user
 
       # If the sudo is the current user do nothing
       if identifier && !(@current_user.id == identifier || @current_user.username == identifier)
-        render_api_error!('403 Forbidden: Must be admin to use sudo', 403) unless @current_user.is_admin?
+        forbidden!('Must be admin to use sudo') unless @current_user.is_admin?
         @current_user = User.by_username_or_id(identifier)
         not_found!("No user id or username for: #{identifier}") if @current_user.nil?
       end
@@ -47,18 +47,18 @@ def sudo_identifier
       end
     end
 
+    # Deprecated
     def user_project
       @project ||= find_project(params[:id])
-      @project || not_found!("Project")
     end
 
     def find_project(id)
       project = Project.find_with_namespace(id) || Project.find_by(id: id)
 
-      if project && can?(current_user, :read_project, project)
+      if can?(current_user, :read_project, project)
         project
       else
-        nil
+        not_found!('Project')
       end
     end
 
@@ -89,11 +89,7 @@ def service_attributes
     end
 
     def find_group(id)
-      begin
-        group = Group.find(id)
-      rescue ActiveRecord::RecordNotFound
-        group = Group.find_by!(path: id)
-      end
+      group = Group.find_by(path: id) || Group.find_by(id: id)
 
       if can?(current_user, :read_group, group)
         group
@@ -135,7 +131,7 @@ def authenticated_as_admin!
     end
 
     def authorize!(action, subject)
-      forbidden! unless abilities.allowed?(current_user, action, subject)
+      forbidden! unless can?(current_user, action, subject)
     end
 
     def authorize_push_project
@@ -197,10 +193,6 @@ def validate_label_params(params)
       errors
     end
 
-    def validate_access_level?(level)
-      Gitlab::Access.options_with_owner.values.include? level.to_i
-    end
-
     # Checks the occurrences of datetime attributes, each attribute if present in the params hash must be in ISO 8601
     # format (YYYY-MM-DDTHH:MM:SSZ) or a Bad Request error is invoked.
     #
@@ -411,11 +403,6 @@ def secret_token
       File.read(Gitlab.config.gitlab_shell.secret_file).chomp
     end
 
-    def handle_member_errors(errors)
-      error!(errors[:access_level], 422) if errors[:access_level].any?
-      not_found!(errors)
-    end
-
     def send_git_blob(repository, blob)
       env['api.format'] = :txt
       content_type 'text/plain'
diff --git a/lib/api/helpers/members_helpers.rb b/lib/api/helpers/members_helpers.rb
new file mode 100644
index 00000000000000..90114f6f6677de
--- /dev/null
+++ b/lib/api/helpers/members_helpers.rb
@@ -0,0 +1,13 @@
+module API
+  module Helpers
+    module MembersHelpers
+      def find_source(source_type, id)
+        public_send("find_#{source_type}", id)
+      end
+
+      def authorize_admin_source!(source_type, source)
+        authorize! :"admin_#{source_type}", source
+      end
+    end
+  end
+end
diff --git a/lib/api/members.rb b/lib/api/members.rb
new file mode 100644
index 00000000000000..56f8b1ca39111a
--- /dev/null
+++ b/lib/api/members.rb
@@ -0,0 +1,124 @@
+module API
+  class Members < Grape::API
+    before { authenticate! }
+
+    helpers ::API::Helpers::MembersHelpers
+
+    %w[group project].each do |source_type|
+      resource source_type.pluralize do
+        # Get a list of group/project members viewable by the authenticated user.
+        #
+        # Parameters:
+        #   id (required) - The group/project ID
+        #   query         - Query string
+        #
+        # Example Request:
+        #   GET /groups/:id/members
+        #   GET /projects/:id/members
+        get ":id/members" do
+          source = find_source(source_type, params[:id])
+
+          members = source.members
+          members = members.joins(:user).merge(User.search(params[:query])) if params[:query]
+          users = Kaminari.paginate_array(members.map(&:user))
+
+          present paginate(users), with: Entities::Member, source: source
+        end
+
+        # Get a group/project member
+        #
+        # Parameters:
+        #   id (required) - The group/project ID
+        #   user_id (required) - The user ID of the member
+        #
+        # Example Request:
+        #   GET /groups/:id/members/:user_id
+        #   GET /projects/:id/members/:user_id
+        get ":id/members/:user_id" do
+          source = find_source(source_type, params[:id])
+
+          members = source.members
+          member = members.find_by!(user_id: params[:user_id])
+
+          present member.user, with: Entities::Member, member: member
+        end
+
+        # Add a new group/project member
+        #
+        # Parameters:
+        #   id (required) - The group/project ID
+        #   user_id (required) - The user ID of the new member
+        #   access_level (required) - A valid access level
+        #
+        # Example Request:
+        #   POST /groups/:id/members
+        #   POST /projects/:id/members
+        post ":id/members" do
+          source = find_source(source_type, params[:id])
+          authorize_admin_source!(source_type, source)
+          required_attributes! [:user_id, :access_level]
+
+          access_requester = source.requesters.find_by(user_id: params[:user_id])
+          if access_requester
+            # We pass current_user = access_requester so that the requester doesn't
+            # receive a "access denied" email
+            ::Members::DestroyService.new(access_requester, access_requester.user).execute
+          end
+
+          conflict!('Member already exists') if source.members.exists?(user_id: params[:user_id])
+
+          source.add_user(params[:user_id], params[:access_level], current_user)
+          member = source.members.find_by(user_id: params[:user_id])
+          if member
+            present member.user, with: Entities::Member, member: member
+          else
+            render_api_error!('400 Bad Request', 400)
+          end
+        end
+
+        # Update a group/project member
+        #
+        # Parameters:
+        #   id (required) - The group/project ID
+        #   user_id (required) - The user ID of the member
+        #   access_level (required) - A valid access level
+        #
+        # Example Request:
+        #   PUT /groups/:id/members/:user_id
+        #   PUT /projects/:id/members/:user_id
+        put ":id/members/:user_id" do
+          source = find_source(source_type, params[:id])
+          authorize_admin_source!(source_type, source)
+          required_attributes! [:user_id, :access_level]
+
+          member = source.members.find_by!(user_id: params[:user_id])
+
+          if member.update_attributes(access_level: params[:access_level])
+            present member.user, with: Entities::Member, member: member
+          else
+            render_validation_error!(member)
+          end
+        end
+
+        # Remove a group/project member
+        #
+        # Parameters:
+        #   id (required) - The group/project ID
+        #   user_id (required) - The user ID of the member
+        #
+        # Example Request:
+        #   DELETE /groups/:id/members/:user_id
+        #   DELETE /projects/:id/members/:user_id
+        delete ":id/members/:user_id" do
+          source = find_source(source_type, params[:id])
+          required_attributes! [:user_id]
+
+          member = source.members.find_by!(user_id: params[:user_id])
+
+          ::Members::DestroyService.new(member, current_user).execute
+          status :no_content
+        end
+      end
+    end
+  end
+end
diff --git a/lib/api/project_members.rb b/lib/api/project_members.rb
deleted file mode 100644
index 6a0b3e7d134321..00000000000000
--- a/lib/api/project_members.rb
+++ /dev/null
@@ -1,110 +0,0 @@
-module API
-  # Projects members API
-  class ProjectMembers < Grape::API
-    before { authenticate! }
-
-    resource :projects do
-      # Get a project team members
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   query         - Query string
-      # Example Request:
-      #   GET /projects/:id/members
-      get ":id/members" do
-        if params[:query].present?
-          @members = paginate user_project.users.where("username LIKE ?", "%#{params[:query]}%")
-        else
-          @members = paginate user_project.users
-        end
-        present @members, with: Entities::ProjectMember, project: user_project
-      end
-
-      # Get a project team members
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   user_id (required) - The ID of a user
-      # Example Request:
-      #   GET /projects/:id/members/:user_id
-      get ":id/members/:user_id" do
-        @member = user_project.users.find params[:user_id]
-        present @member, with: Entities::ProjectMember, project: user_project
-      end
-
-      # Add a new project team member
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   user_id (required) - The ID of a user
-      #   access_level (required) - Project access level
-      # Example Request:
-      #   POST /projects/:id/members
-      post ":id/members" do
-        authorize! :admin_project, user_project
-        required_attributes! [:user_id, :access_level]
-
-        # either the user is already a team member or a new one
-        project_member = user_project.project_member(params[:user_id])
-        if project_member.nil?
-          project_member = user_project.project_members.new(
-            user_id: params[:user_id],
-            access_level: params[:access_level]
-          )
-        end
-
-        if project_member.save
-          @member = project_member.user
-          present @member, with: Entities::ProjectMember, project: user_project
-        else
-          handle_member_errors project_member.errors
-        end
-      end
-
-      # Update project team member
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   user_id (required) - The ID of a team member
-      #   access_level (required) - Project access level
-      # Example Request:
-      #   PUT /projects/:id/members/:user_id
-      put ":id/members/:user_id" do
-        authorize! :admin_project, user_project
-        required_attributes! [:access_level]
-
-        project_member = user_project.project_members.find_by(user_id: params[:user_id])
-        not_found!("User can not be found") if project_member.nil?
-
-        if project_member.update_attributes(access_level: params[:access_level])
-          @member = project_member.user
-          present @member, with: Entities::ProjectMember, project: user_project
-        else
-          handle_member_errors project_member.errors
-        end
-      end
-
-      # Remove a team member from project
-      #
-      # Parameters:
-      #   id (required) - The ID of a project
-      #   user_id (required) - The ID of a team member
-      # Example Request:
-      #   DELETE /projects/:id/members/:user_id
-      delete ":id/members/:user_id" do
-        project_member = user_project.project_members.find_by(user_id: params[:user_id])
-
-        unless current_user.can?(:admin_project, user_project) ||
-                current_user.can?(:destroy_project_member, project_member)
-          forbidden!
-        end
-
-        if project_member.nil?
-          { message: "Access revoked", id: params[:user_id].to_i }
-        else
-          project_member.destroy
-        end
-      end
-    end
-  end
-end
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 44cd3c08718fd5..2277f4e13bfd84 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -10,7 +10,7 @@
 
     it { is_expected.to validate_presence_of(:user) }
     it { is_expected.to validate_presence_of(:source) }
-    it { is_expected.to validate_inclusion_of(:access_level).in_array(Gitlab::Access.values) }
+    it { is_expected.to validate_inclusion_of(:access_level).in_array(Gitlab::Access.all_values) }
 
     it_behaves_like 'an object with email-formated attributes', :invite_email do
       subject { build(:project_member) }
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index 28673de3189520..913d74645a7fd8 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -27,6 +27,7 @@
   describe 'validations' do
     it { is_expected.to allow_value('Project').for(:source_type) }
     it { is_expected.not_to allow_value('project').for(:source_type) }
+    it { is_expected.to validate_inclusion_of(:access_level).in_array(Gitlab::Access.values) }
   end
 
   describe 'modules' do
@@ -40,7 +41,7 @@
   end
 
   describe "#destroy" do
-    let(:owner)   { create(:project_member, access_level: ProjectMember::OWNER) }
+    let(:owner)   { create(:project_member, access_level: ProjectMember::MASTER) }
     let(:project) { owner.project }
     let(:master)  { create(:project_member, project: project) }
 
diff --git a/spec/requests/api/access_requests_spec.rb b/spec/requests/api/access_requests_spec.rb
new file mode 100644
index 00000000000000..d78494b76fac2a
--- /dev/null
+++ b/spec/requests/api/access_requests_spec.rb
@@ -0,0 +1,246 @@
+require 'spec_helper'
+
+describe API::AccessRequests, api: true  do
+  include ApiHelpers
+
+  let(:master) { create(:user) }
+  let(:developer) { create(:user) }
+  let(:access_requester) { create(:user) }
+  let(:stranger) { create(:user) }
+
+  let(:project) do
+    project = create(:project, :public, creator_id: master.id, namespace: master.namespace)
+    project.team << [developer, :developer]
+    project.team << [master, :master]
+    project.request_access(access_requester)
+    project
+  end
+
+  let(:group) do
+    group = create(:group, :public)
+    group.add_developer(developer)
+    group.add_owner(master)
+    group.request_access(access_requester)
+    group
+  end
+
+  shared_examples 'GET /:sources/:id/access_requests' do |source_type|
+    context "with :sources == #{source_type.pluralize}" do
+      it_behaves_like 'a 404 response when source is private' do
+        let(:route) { get api("/#{source_type.pluralize}/#{source.id}/access_requests", stranger) }
+      end
+
+      context 'when authenticated as a non-master/owner' do
+        %i[developer access_requester stranger].each do |type|
+          context "as a #{type}" do
+            it 'returns 403' do
+              user = public_send(type)
+              get api("/#{source_type.pluralize}/#{source.id}/access_requests", user)
+
+              expect(response).to have_http_status(403)
+            end
+          end
+        end
+      end
+
+      context 'when authenticated as a master/owner' do
+        it 'returns access requesters' do
+          get api("/#{source_type.pluralize}/#{source.id}/access_requests", master)
+
+          expect(response).to have_http_status(200)
+          expect(json_response).to be_an Array
+          expect(json_response.size).to eq(1)
+        end
+      end
+    end
+  end
+
+  shared_examples 'POST /:sources/:id/access_requests' do |source_type|
+    context "with :sources == #{source_type.pluralize}" do
+      it_behaves_like 'a 404 response when source is private' do
+        let(:route) { post api("/#{source_type.pluralize}/#{source.id}/access_requests", stranger) }
+      end
+
+      context 'when authenticated as a member' do
+        %i[developer master].each do |type|
+          context "as a #{type}" do
+            it 'returns 400' do
+              expect do
+                user = public_send(type)
+                post api("/#{source_type.pluralize}/#{source.id}/access_requests", user)
+
+                expect(response).to have_http_status(400)
+              end.not_to change { source.requesters.count }
+            end
+          end
+        end
+      end
+
+      context 'when authenticated as an access requester' do
+        it 'returns 400' do
+          expect do
+            post api("/#{source_type.pluralize}/#{source.id}/access_requests", access_requester)
+
+            expect(response).to have_http_status(400)
+          end.not_to change { source.requesters.count }
+        end
+      end
+
+      context 'when authenticated as a stranger' do
+        it 'returns 201' do
+          expect do
+            post api("/#{source_type.pluralize}/#{source.id}/access_requests", stranger)
+
+            expect(response).to have_http_status(201)
+          end.to change { source.requesters.count }.by(1)
+
+          # User attributes
+          expect(json_response['id']).to eq(stranger.id)
+          expect(json_response['name']).to eq(stranger.name)
+          expect(json_response['username']).to eq(stranger.username)
+          expect(json_response['state']).to eq(stranger.state)
+          expect(json_response['avatar_url']).to eq(stranger.avatar_url)
+          expect(json_response['web_url']).to eq(Gitlab::Routing.url_helpers.user_url(stranger))
+
+          # Member attributes
+          expect(json_response['requested_at']).to be_present
+        end
+      end
+    end
+  end
+
+  shared_examples 'PUT /:sources/:id/access_requests/:user_id/approve' do |source_type|
+    context "with :sources == #{source_type.pluralize}" do
+      it_behaves_like 'a 404 response when source is private' do
+        let(:route) { put api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}/approve", stranger) }
+      end
+
+      context 'when authenticated as a non-master/owner' do
+        %i[developer access_requester stranger].each do |type|
+          context "as a #{type}" do
+            it 'returns 403' do
+              user = public_send(type)
+              put api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}/approve", user)
+
+              expect(response).to have_http_status(403)
+            end
+          end
+        end
+      end
+
+      context 'when authenticated as a master/owner' do
+        it 'returns 201' do
+          expect do
+            put api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}/approve", master),
+                access_level: Member::MASTER
+
+            expect(response).to have_http_status(201)
+          end.to change { source.members.count }.by(1)
+          # User attributes
+          expect(json_response['id']).to eq(access_requester.id)
+          expect(json_response['name']).to eq(access_requester.name)
+          expect(json_response['username']).to eq(access_requester.username)
+          expect(json_response['state']).to eq(access_requester.state)
+          expect(json_response['avatar_url']).to eq(access_requester.avatar_url)
+          expect(json_response['web_url']).to eq(Gitlab::Routing.url_helpers.user_url(access_requester))
+
+          # Member attributes
+          expect(json_response['access_level']).to eq(Member::MASTER)
+        end
+
+        context 'user_id does not match an existing access requester' do
+          it 'returns 404' do
+            expect do
+              put api("/#{source_type.pluralize}/#{source.id}/access_requests/#{stranger.id}/approve", master)
+
+              expect(response).to have_http_status(404)
+            end.not_to change { source.members.count }
+          end
+        end
+      end
+    end
+  end
+
+  shared_examples 'DELETE /:sources/:id/access_requests/:user_id' do |source_type|
+    context "with :sources == #{source_type.pluralize}" do
+      it_behaves_like 'a 404 response when source is private' do
+        let(:route) { delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", stranger) }
+      end
+
+      context 'when authenticated as a non-master/owner' do
+        %i[developer stranger].each do |type|
+          context "as a #{type}" do
+            it 'returns 403' do
+              user = public_send(type)
+              delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", user)
+
+              expect(response).to have_http_status(403)
+            end
+          end
+        end
+      end
+
+      context 'when authenticated as the access requester' do
+        it 'returns 200' do
+          expect do
+            delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", access_requester)
+
+            expect(response).to have_http_status(200)
+          end.to change { source.requesters.count }.by(-1)
+        end
+      end
+
+      context 'when authenticated as a master/owner' do
+        it 'returns 200' do
+          expect do
+            delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", master)
+
+            expect(response).to have_http_status(200)
+          end.to change { source.requesters.count }.by(-1)
+        end
+
+        context 'user_id does not match an existing access requester' do
+          it 'returns 404' do
+            expect do
+              delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{stranger.id}", master)
+
+              expect(response).to have_http_status(404)
+            end.not_to change { source.requesters.count }
+          end
+        end
+      end
+    end
+  end
+
+  it_behaves_like 'GET /:sources/:id/access_requests', 'project' do
+    let(:source) { project }
+  end
+
+  it_behaves_like 'GET /:sources/:id/access_requests', 'group' do
+    let(:source) { group }
+  end
+
+  it_behaves_like 'POST /:sources/:id/access_requests', 'project' do
+    let(:source) { project }
+  end
+
+  it_behaves_like 'POST /:sources/:id/access_requests', 'group' do
+    let(:source) { group }
+  end
+
+  it_behaves_like 'PUT /:sources/:id/access_requests/:user_id/approve', 'project' do
+    let(:source) { project }
+  end
+
+  it_behaves_like 'PUT /:sources/:id/access_requests/:user_id/approve', 'group' do
+    let(:source) { group }
+  end
+
+  it_behaves_like 'DELETE /:sources/:id/access_requests/:user_id', 'project' do
+    let(:source) { project }
+  end
+
+  it_behaves_like 'DELETE /:sources/:id/access_requests/:user_id', 'group' do
+    let(:source) { group }
+  end
+end
diff --git a/spec/requests/api/group_members_spec.rb b/spec/requests/api/group_members_spec.rb
index 8bd6a8062ae774..cdb3d7ddf4afd9 100644
--- a/spec/requests/api/group_members_spec.rb
+++ b/spec/requests/api/group_members_spec.rb
@@ -96,9 +96,9 @@
         expect(response).to have_http_status(400)
       end
 
-      it "returns a 422 error when access level is not known" do
+      it "returns a 400 error when access level is not known" do
         post api("/groups/#{group_no_members.id}/members", owner), user_id: master.id, access_level: 1234
-        expect(response).to have_http_status(422)
+        expect(response).to have_http_status(400)
       end
     end
   end
@@ -156,12 +156,12 @@
         expect(response).to have_http_status(400)
       end
 
-      it 'returns a 422 error when access level is not known' do
+      it 'returns a 400 error when access level is not known' do
         put(
           api("/groups/#{group_with_members.id}/members/#{master.id}", owner),
           access_level: 1234
         )
-        expect(response).to have_http_status(422)
+        expect(response).to have_http_status(400)
       end
     end
   end
@@ -182,7 +182,7 @@
           delete api("/groups/#{group_with_members.id}/members/#{guest.id}", owner)
         end.to change { group_with_members.members.count }.by(-1)
 
-        expect(response).to have_http_status(200)
+        expect(response).to have_http_status(204)
       end
 
       it "returns a 404 error when user id is not known" do
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
new file mode 100644
index 00000000000000..b11a16487acb58
--- /dev/null
+++ b/spec/requests/api/members_spec.rb
@@ -0,0 +1,312 @@
+require 'spec_helper'
+
+describe API::Members, api: true  do
+  include ApiHelpers
+
+  let(:master) { create(:user) }
+  let(:developer) { create(:user) }
+  let(:access_requester) { create(:user) }
+  let(:stranger) { create(:user) }
+
+  let(:project) do
+    project = create(:project, :public, creator_id: master.id, namespace: master.namespace)
+    project.team << [developer, :developer]
+    project.team << [master, :master]
+    project.request_access(access_requester)
+    project
+  end
+
+  let!(:group) do
+    group = create(:group, :public)
+    group.add_developer(developer)
+    group.add_owner(master)
+    group.request_access(access_requester)
+    group
+  end
+
+  shared_examples 'GET /:sources/:id/members' do |source_type|
+    context "with :sources == #{source_type.pluralize}" do
+      it_behaves_like 'a 404 response when source is private' do
+        let(:route) { get api("/#{source_type.pluralize}/#{source.id}/members", stranger) }
+      end
+
+      context 'when authenticated as a non-member' do
+        %i[access_requester stranger].each do |type|
+          context "as a #{type}" do
+            it 'returns 200' do
+              user = public_send(type)
+              get api("/#{source_type.pluralize}/#{source.id}/members", user)
+
+              expect(response).to have_http_status(200)
+              expect(json_response.size).to eq(2)
+            end
+          end
+        end
+      end
+
+      it 'finds members with query string' do
+        get api("/#{source_type.pluralize}/#{source.id}/members", developer), query: master.username
+
+        expect(response).to have_http_status(200)
+        expect(json_response.count).to eq(1)
+        expect(json_response.first['username']).to eq(master.username)
+      end
+    end
+  end
+
+  shared_examples 'GET /:sources/:id/members/:user_id' do |source_type|
+    context "with :sources == #{source_type.pluralize}" do
+      it_behaves_like 'a 404 response when source is private' do
+        let(:route) { get api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger) }
+      end
+
+      context 'when authenticated as a non-member' do
+        %i[access_requester stranger].each do |type|
+          context "as a #{type}" do
+            it 'returns 200' do
+              user = public_send(type)
+              get api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user)
+
+              expect(response).to have_http_status(200)
+              # User attributes
+              expect(json_response['id']).to eq(developer.id)
+              expect(json_response['name']).to eq(developer.name)
+              expect(json_response['username']).to eq(developer.username)
+              expect(json_response['state']).to eq(developer.state)
+              expect(json_response['avatar_url']).to eq(developer.avatar_url)
+              expect(json_response['web_url']).to eq(Gitlab::Routing.url_helpers.user_url(developer))
+
+              # Member attributes
+              expect(json_response['access_level']).to eq(Member::DEVELOPER)
+            end
+          end
+        end
+      end
+    end
+  end
+
+  shared_examples 'POST /:sources/:id/members' do |source_type|
+    context "with :sources == #{source_type.pluralize}" do
+      it_behaves_like 'a 404 response when source is private' do
+        let(:route) { post api("/#{source_type.pluralize}/#{source.id}/members", stranger) }
+      end
+
+      context 'when authenticated as a non-member or member with insufficient rights' do
+        %i[access_requester stranger developer].each do |type|
+          context "as a #{type}" do
+            it 'returns 403' do
+              user = public_send(type)
+              post api("/#{source_type.pluralize}/#{source.id}/members", user)
+
+              expect(response).to have_http_status(403)
+            end
+          end
+        end
+      end
+
+      context 'when authenticated as a master/owner' do
+        context 'and new member is already a requester' do
+          it 'transforms the requester into a proper member' do
+            expect do
+              post api("/#{source_type.pluralize}/#{source.id}/members", master),
+                   user_id: access_requester.id, access_level: Member::MASTER
+
+              expect(response).to have_http_status(201)
+            end.to change { source.members.count }.by(1)
+            expect(source.requesters.count).to eq(0)
+            expect(json_response['id']).to eq(access_requester.id)
+            expect(json_response['access_level']).to eq(Member::MASTER)
+          end
+        end
+
+        it 'creates a new member' do
+          expect do
+            post api("/#{source_type.pluralize}/#{source.id}/members", master),
+                 user_id: stranger.id, access_level: Member::DEVELOPER
+
+            expect(response).to have_http_status(201)
+          end.to change { source.members.count }.by(1)
+          expect(json_response['id']).to eq(stranger.id)
+          expect(json_response['access_level']).to eq(Member::DEVELOPER)
+        end
+      end
+
+      it "returns #{source_type == 'project' ? 201 : 409} if member already exists" do
+        post api("/#{source_type.pluralize}/#{source.id}/members", master),
+             user_id: master.id, access_level: Member::MASTER
+
+        expect(response).to have_http_status(409)
+      end
+
+      it 'returns 400 when user_id is not given' do
+        post api("/#{source_type.pluralize}/#{source.id}/members", master),
+             access_level: Member::MASTER
+
+        expect(response).to have_http_status(400)
+      end
+
+      it 'returns 400 when access_level is not given' do
+        post api("/#{source_type.pluralize}/#{source.id}/members", master),
+             user_id: stranger.id
+
+        expect(response).to have_http_status(400)
+      end
+
+      it 'returns 400 when access_level is not valid' do
+        post api("/#{source_type.pluralize}/#{source.id}/members", master),
+             user_id: stranger.id, access_level: 1234
+
+        expect(response).to have_http_status(400)
+      end
+    end
+  end
+
+  shared_examples 'PUT /:sources/:id/members/:user_id' do |source_type|
+    context "with :sources == #{source_type.pluralize}" do
+      it_behaves_like 'a 404 response when source is private' do
+        let(:route) { put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger) }
+      end
+
+      context 'when authenticated as a non-member or member with insufficient rights' do
+        %i[access_requester stranger developer].each do |type|
+          context "as a #{type}" do
+            it 'returns 403' do
+              user = public_send(type)
+              put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user)
+
+              expect(response).to have_http_status(403)
+            end
+          end
+        end
+      end
+
+      context 'when authenticated as a master/owner' do
+        it 'updates the member' do
+          put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master),
+              access_level: Member::MASTER
+
+          expect(response).to have_http_status(200)
+          expect(json_response['id']).to eq(developer.id)
+          expect(json_response['access_level']).to eq(Member::MASTER)
+        end
+      end
+
+      it 'returns 409 if member does not exist' do
+        put api("/#{source_type.pluralize}/#{source.id}/members/123", master),
+            access_level: Member::MASTER
+
+        expect(response).to have_http_status(404)
+      end
+
+      it 'returns 400 when access_level is not given' do
+        put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master)
+
+        expect(response).to have_http_status(400)
+      end
+
+      it 'returns 400 when access level is not valid' do
+        put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master),
+            access_level: 1234
+
+        expect(response).to have_http_status(400)
+      end
+    end
+  end
+
+  shared_examples 'DELETE /:sources/:id/members/:user_id' do |source_type|
+    context "with :sources == #{source_type.pluralize}" do
+      it_behaves_like 'a 404 response when source is private' do
+        let(:route) { delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger) }
+      end
+
+      context 'when authenticated as a non-member or member with insufficient rights' do
+        %i[access_requester stranger].each do |type|
+          context "as a #{type}" do
+            it 'returns 403' do
+              user = public_send(type)
+              delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user)
+
+              expect(response).to have_http_status(403)
+            end
+          end
+        end
+      end
+
+      context 'when authenticated as a member and deleting themself' do
+        it 'deletes the member' do
+          expect do
+            delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", developer)
+
+            expect(response).to have_http_status(204)
+          end.to change { source.members.count }.by(-1)
+        end
+      end
+
+      context 'when authenticated as a master/owner' do
+        context 'and member is a requester' do
+          it 'returns 404' do
+            expect do
+              delete api("/#{source_type.pluralize}/#{source.id}/members/#{access_requester.id}", master)
+
+              expect(response).to have_http_status(404)
+            end.not_to change { source.requesters.count }
+          end
+        end
+
+        it 'deletes the member' do
+          expect do
+            delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master)
+
+            expect(response).to have_http_status(204)
+          end.to change { source.members.count }.by(-1)
+        end
+      end
+
+      it 'returns 409 if member does not exist' do
+        delete api("/#{source_type.pluralize}/#{source.id}/members/123", master)
+
+        expect(response).to have_http_status(404)
+      end
+    end
+  end
+
+  it_behaves_like 'GET /:sources/:id/members', 'project' do
+    let(:source) { project }
+  end
+
+  it_behaves_like 'GET /:sources/:id/members', 'group' do
+    let(:source) { group }
+  end
+
+  it_behaves_like 'GET /:sources/:id/members/:user_id', 'project' do
+    let(:source) { project }
+  end
+
+  it_behaves_like 'GET /:sources/:id/members/:user_id', 'group' do
+    let(:source) { group }
+  end
+
+  it_behaves_like 'POST /:sources/:id/members', 'project' do
+    let(:source) { project }
+  end
+
+  it_behaves_like 'POST /:sources/:id/members', 'group' do
+    let(:source) { group }
+  end
+
+  it_behaves_like 'PUT /:sources/:id/members/:user_id', 'project' do
+    let(:source) { project }
+  end
+
+  it_behaves_like 'PUT /:sources/:id/members/:user_id', 'group' do
+    let(:source) { group }
+  end
+
+  it_behaves_like 'DELETE /:sources/:id/members/:user_id', 'project' do
+    let(:source) { project }
+  end
+
+  it_behaves_like 'DELETE /:sources/:id/members/:user_id', 'group' do
+    let(:source) { group }
+  end
+end
diff --git a/spec/requests/api/project_members_spec.rb b/spec/requests/api/project_members_spec.rb
index 13cc0d81ac8ecb..195d5bb3f8fed7 100644
--- a/spec/requests/api/project_members_spec.rb
+++ b/spec/requests/api/project_members_spec.rb
@@ -62,7 +62,7 @@
       expect(json_response['access_level']).to eq(ProjectMember::DEVELOPER)
     end
 
-    it "returns a 201 status if user is already project member" do
+    it "returns a 409 status if user is already project member" do
       post api("/projects/#{project.id}/members", user),
            user_id: user2.id,
            access_level: ProjectMember::DEVELOPER
@@ -70,9 +70,7 @@
         post api("/projects/#{project.id}/members", user), user_id: user2.id, access_level: ProjectMember::DEVELOPER
       end.not_to change { ProjectMember.count }
 
-      expect(response).to have_http_status(201)
-      expect(json_response['username']).to eq(user2.username)
-      expect(json_response['access_level']).to eq(ProjectMember::DEVELOPER)
+      expect(response).to have_http_status(409)
     end
 
     it "returns a 400 error when user id is not given" do
@@ -85,9 +83,9 @@
       expect(response).to have_http_status(400)
     end
 
-    it "returns a 422 error when access level is not known" do
+    it "returns a 400 error when access level is not known" do
       post api("/projects/#{project.id}/members", user), user_id: user2.id, access_level: 1234
-      expect(response).to have_http_status(422)
+      expect(response).to have_http_status(400)
     end
   end
 
@@ -111,9 +109,9 @@
       expect(response).to have_http_status(400)
     end
 
-    it "returns a 422 error when access level is not known" do
+    it "returns a 400 error when access level is not known" do
       put api("/projects/#{project.id}/members/#{user3.id}", user), access_level: 123
-      expect(response).to have_http_status(422)
+      expect(response).to have_http_status(400)
     end
   end
 
@@ -129,27 +127,25 @@
       end.to change { ProjectMember.count }.by(-1)
     end
 
-    it "returns 200 if team member is not part of a project" do
+    it "returns 404 if team member is not part of a project" do
       delete api("/projects/#{project.id}/members/#{user3.id}", user)
       expect do
         delete api("/projects/#{project.id}/members/#{user3.id}", user)
       end.not_to change { ProjectMember.count }
-      expect(response).to have_http_status(200)
+      expect(response).to have_http_status(404)
     end
 
-    it "returns 200 if team member already removed" do
+    it "returns 404 if team member already removed" do
       delete api("/projects/#{project.id}/members/#{user3.id}", user)
       delete api("/projects/#{project.id}/members/#{user3.id}", user)
-      expect(response).to have_http_status(200)
+      expect(response).to have_http_status(404)
     end
 
-    it "returns 200 OK when the user was not member" do
+    it "returns 404 when the user was not member" do
       expect do
         delete api("/projects/#{project.id}/members/1000000", user)
       end.to change { ProjectMember.count }.by(0)
-      expect(response).to have_http_status(200)
-      expect(json_response['id']).to eq(1000000)
-      expect(json_response['message']).to eq('Access revoked')
+      expect(response).to have_http_status(404)
     end
 
     context 'when the user is not an admin or owner' do
@@ -158,8 +154,7 @@
           delete api("/projects/#{project.id}/members/#{user3.id}", user3)
         end.to change { ProjectMember.count }.by(-1)
 
-        expect(response).to have_http_status(200)
-        expect(json_response['id']).to eq(project_member2.id)
+        expect(response).to have_http_status(204)
       end
     end
   end
diff --git a/spec/support/api/members_shared_examples.rb b/spec/support/api/members_shared_examples.rb
new file mode 100644
index 00000000000000..dab71a35a552c5
--- /dev/null
+++ b/spec/support/api/members_shared_examples.rb
@@ -0,0 +1,11 @@
+shared_examples 'a 404 response when source is private' do
+  before do
+    source.update_column(:visibility_level, Gitlab::VisibilityLevel::PRIVATE)
+  end
+
+  it 'returns 404' do
+    route
+
+    expect(response).to have_http_status(404)
+  end
+end
-- 
GitLab


From 7c1b33b48f8c45b726a513b6809a85919d41c23c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A9my=20Coutable?= 
Date: Tue, 9 Aug 2016 12:14:11 +0200
Subject: [PATCH 117/153] Restore back-compatibility for current members API
 endpoints
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Rémy Coutable 
---
 lib/api/helpers.rb                        |  1 -
 lib/api/members.rb                        | 45 +++++++++++++++++++----
 spec/requests/api/group_members_spec.rb   | 10 ++---
 spec/requests/api/members_spec.rb         | 22 +++++------
 spec/requests/api/project_members_spec.rb | 31 +++++++++-------
 5 files changed, 72 insertions(+), 37 deletions(-)

diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index f06c262fd4c79d..d0469d6602d0df 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -47,7 +47,6 @@ def sudo_identifier
       end
     end
 
-    # Deprecated
     def user_project
       @project ||= find_project(params[:id])
     end
diff --git a/lib/api/members.rb b/lib/api/members.rb
index 56f8b1ca39111a..ccb7618360d96b 100644
--- a/lib/api/members.rb
+++ b/lib/api/members.rb
@@ -65,14 +65,29 @@ class Members < Grape::API
             ::Members::DestroyService.new(access_requester, access_requester.user).execute
           end
 
-          conflict!('Member already exists') if source.members.exists?(user_id: params[:user_id])
-
-          source.add_user(params[:user_id], params[:access_level], current_user)
           member = source.members.find_by(user_id: params[:user_id])
+
+          # This is to ensure back-compatibility but 409 behavior should be used
+          # for both project and group members in 9.0!
+          conflict!('Member already exists') if source_type == 'group' && member
+
+          unless member
+            source.add_user(params[:user_id], params[:access_level], current_user)
+            member = source.members.find_by(user_id: params[:user_id])
+          end
+
           if member
             present member.user, with: Entities::Member, member: member
           else
-            render_api_error!('400 Bad Request', 400)
+            # Since `source.add_user` doesn't return a member object, we have to
+            # build a new one and populate its errors in order to render them.
+            member = source.members.build(attributes_for_keys([:user_id, :access_level]))
+            member.valid? # populate the errors
+
+            # This is to ensure back-compatibility but 400 behavior should be used
+            # for all validation errors in 9.0!
+            render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level)
+            render_validation_error!(member)
           end
         end
 
@@ -96,6 +111,9 @@ class Members < Grape::API
           if member.update_attributes(access_level: params[:access_level])
             present member.user, with: Entities::Member, member: member
           else
+            # This is to ensure back-compatibility but 400 behavior should be used
+            # for all validation errors in 9.0!
+            render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level)
             render_validation_error!(member)
           end
         end
@@ -113,10 +131,23 @@ class Members < Grape::API
           source = find_source(source_type, params[:id])
           required_attributes! [:user_id]
 
-          member = source.members.find_by!(user_id: params[:user_id])
+          # This is to ensure back-compatibility but find_by! should be used
+          # in that casse in 9.0!
+          member = source.members.find_by(user_id: params[:user_id])
+
+          # This is to ensure back-compatibility but this should be removed in
+          # favor of find_by! in 9.0!
+          not_found!("Member: user_id:#{params[:user_id]}") if source_type == 'group' && member.nil?
+
+          # This is to ensure back-compatibility but 204 behavior should be used
+          # for all DELETE endpoints in 9.0!
+          if member.nil?
+            { message: "Access revoked", id: params[:user_id].to_i }
+          else
+            ::Members::DestroyService.new(member, current_user).execute
 
-          ::Members::DestroyService.new(member, current_user).execute
-          status :no_content
+            present member.user, with: Entities::Member, member: member
+          end
         end
       end
     end
diff --git a/spec/requests/api/group_members_spec.rb b/spec/requests/api/group_members_spec.rb
index cdb3d7ddf4afd9..8bd6a8062ae774 100644
--- a/spec/requests/api/group_members_spec.rb
+++ b/spec/requests/api/group_members_spec.rb
@@ -96,9 +96,9 @@
         expect(response).to have_http_status(400)
       end
 
-      it "returns a 400 error when access level is not known" do
+      it "returns a 422 error when access level is not known" do
         post api("/groups/#{group_no_members.id}/members", owner), user_id: master.id, access_level: 1234
-        expect(response).to have_http_status(400)
+        expect(response).to have_http_status(422)
       end
     end
   end
@@ -156,12 +156,12 @@
         expect(response).to have_http_status(400)
       end
 
-      it 'returns a 400 error when access level is not known' do
+      it 'returns a 422 error when access level is not known' do
         put(
           api("/groups/#{group_with_members.id}/members/#{master.id}", owner),
           access_level: 1234
         )
-        expect(response).to have_http_status(400)
+        expect(response).to have_http_status(422)
       end
     end
   end
@@ -182,7 +182,7 @@
           delete api("/groups/#{group_with_members.id}/members/#{guest.id}", owner)
         end.to change { group_with_members.members.count }.by(-1)
 
-        expect(response).to have_http_status(204)
+        expect(response).to have_http_status(200)
       end
 
       it "returns a 404 error when user id is not known" do
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index b11a16487acb58..a56ee30f7b15eb 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -135,7 +135,7 @@
         post api("/#{source_type.pluralize}/#{source.id}/members", master),
              user_id: master.id, access_level: Member::MASTER
 
-        expect(response).to have_http_status(409)
+        expect(response).to have_http_status(source_type == 'project' ? 201 : 409)
       end
 
       it 'returns 400 when user_id is not given' do
@@ -152,11 +152,11 @@
         expect(response).to have_http_status(400)
       end
 
-      it 'returns 400 when access_level is not valid' do
+      it 'returns 422 when access_level is not valid' do
         post api("/#{source_type.pluralize}/#{source.id}/members", master),
              user_id: stranger.id, access_level: 1234
 
-        expect(response).to have_http_status(400)
+        expect(response).to have_http_status(422)
       end
     end
   end
@@ -204,11 +204,11 @@
         expect(response).to have_http_status(400)
       end
 
-      it 'returns 400 when access level is not valid' do
+      it 'returns 422 when access level is not valid' do
         put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master),
             access_level: 1234
 
-        expect(response).to have_http_status(400)
+        expect(response).to have_http_status(422)
       end
     end
   end
@@ -237,18 +237,18 @@
           expect do
             delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", developer)
 
-            expect(response).to have_http_status(204)
+            expect(response).to have_http_status(200)
           end.to change { source.members.count }.by(-1)
         end
       end
 
       context 'when authenticated as a master/owner' do
         context 'and member is a requester' do
-          it 'returns 404' do
+          it "returns #{source_type == 'project' ? 200 : 404}" do
             expect do
               delete api("/#{source_type.pluralize}/#{source.id}/members/#{access_requester.id}", master)
 
-              expect(response).to have_http_status(404)
+              expect(response).to have_http_status(source_type == 'project' ? 200 : 404)
             end.not_to change { source.requesters.count }
           end
         end
@@ -257,15 +257,15 @@
           expect do
             delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master)
 
-            expect(response).to have_http_status(204)
+            expect(response).to have_http_status(200)
           end.to change { source.members.count }.by(-1)
         end
       end
 
-      it 'returns 409 if member does not exist' do
+      it "returns #{source_type == 'project' ? 200 : 404} if member does not exist" do
         delete api("/#{source_type.pluralize}/#{source.id}/members/123", master)
 
-        expect(response).to have_http_status(404)
+        expect(response).to have_http_status(source_type == 'project' ? 200 : 404)
       end
     end
   end
diff --git a/spec/requests/api/project_members_spec.rb b/spec/requests/api/project_members_spec.rb
index 195d5bb3f8fed7..061c7b78edbaa9 100644
--- a/spec/requests/api/project_members_spec.rb
+++ b/spec/requests/api/project_members_spec.rb
@@ -62,7 +62,7 @@
       expect(json_response['access_level']).to eq(ProjectMember::DEVELOPER)
     end
 
-    it "returns a 409 status if user is already project member" do
+    it "returns a 201 status if user is already project member" do
       post api("/projects/#{project.id}/members", user),
            user_id: user2.id,
            access_level: ProjectMember::DEVELOPER
@@ -70,7 +70,9 @@
         post api("/projects/#{project.id}/members", user), user_id: user2.id, access_level: ProjectMember::DEVELOPER
       end.not_to change { ProjectMember.count }
 
-      expect(response).to have_http_status(409)
+      expect(response).to have_http_status(201)
+      expect(json_response['username']).to eq(user2.username)
+      expect(json_response['access_level']).to eq(ProjectMember::DEVELOPER)
     end
 
     it "returns a 400 error when user id is not given" do
@@ -83,9 +85,9 @@
       expect(response).to have_http_status(400)
     end
 
-    it "returns a 400 error when access level is not known" do
+    it "returns a 422 error when access level is not known" do
       post api("/projects/#{project.id}/members", user), user_id: user2.id, access_level: 1234
-      expect(response).to have_http_status(400)
+      expect(response).to have_http_status(422)
     end
   end
 
@@ -109,9 +111,9 @@
       expect(response).to have_http_status(400)
     end
 
-    it "returns a 400 error when access level is not known" do
+    it "returns a 422 error when access level is not known" do
       put api("/projects/#{project.id}/members/#{user3.id}", user), access_level: 123
-      expect(response).to have_http_status(400)
+      expect(response).to have_http_status(422)
     end
   end
 
@@ -127,25 +129,27 @@
       end.to change { ProjectMember.count }.by(-1)
     end
 
-    it "returns 404 if team member is not part of a project" do
+    it "returns 200 if team member is not part of a project" do
       delete api("/projects/#{project.id}/members/#{user3.id}", user)
       expect do
         delete api("/projects/#{project.id}/members/#{user3.id}", user)
       end.not_to change { ProjectMember.count }
-      expect(response).to have_http_status(404)
+      expect(response).to have_http_status(200)
     end
 
-    it "returns 404 if team member already removed" do
+    it "returns 200 if team member already removed" do
       delete api("/projects/#{project.id}/members/#{user3.id}", user)
       delete api("/projects/#{project.id}/members/#{user3.id}", user)
-      expect(response).to have_http_status(404)
+      expect(response).to have_http_status(200)
     end
 
-    it "returns 404 when the user was not member" do
+    it "returns 200 OK when the user was not member" do
       expect do
         delete api("/projects/#{project.id}/members/1000000", user)
       end.to change { ProjectMember.count }.by(0)
-      expect(response).to have_http_status(404)
+      expect(response).to have_http_status(200)
+      expect(json_response['id']).to eq(1000000)
+      expect(json_response['message']).to eq('Access revoked')
     end
 
     context 'when the user is not an admin or owner' do
@@ -154,7 +158,8 @@
           delete api("/projects/#{project.id}/members/#{user3.id}", user3)
         end.to change { ProjectMember.count }.by(-1)
 
-        expect(response).to have_http_status(204)
+        expect(response).to have_http_status(200)
+        expect(json_response['id']).to eq(user3.id)
       end
     end
   end
-- 
GitLab


From 5010be7766d08a9adee51c7d16ba71c19ff7dede Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A9my=20Coutable?= 
Date: Tue, 9 Aug 2016 12:20:09 +0200
Subject: [PATCH 118/153] Improve the performance of the GET
 /:sources/:id/{access_requests,members} API endpoints
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The performance was greatly improved by removing two N+1 queries issues
for each endpoint.

For comparison:

1. `GET /projects/:id/members`

With two N+1 queries issues (one was already fxed in the following
snippet):

```
  ProjectMember Load (0.2ms)  SELECT "members".* FROM "members" WHERE
"members"."source_type" = $1 AND "members"."type" IN ('ProjectMember')
AND "members"."source_id" = $2 AND "members"."source_type" = $3 AND
"members"."type" IN ('ProjectMember') AND "members"."requested_at" IS
NULL  ORDER BY "members"."id" DESC  [["source_type", "Project"],
["source_id", 1], ["source_type", "Project"]]
  User Load (0.5ms)  SELECT "users".* FROM "users" WHERE "users"."id" IN
(5, 22, 16, 13)  ORDER BY "users"."id" DESC
  ActiveRecord::SchemaMigration Load (0.2ms)  SELECT
"schema_migrations".* FROM "schema_migrations"
  ProjectMember Load (0.3ms)  SELECT  "members".* FROM "members" WHERE
"members"."source_type" = $1 AND "members"."type" IN ('ProjectMember')
AND "members"."source_id" = $2 AND "members"."source_type" = $3 AND
"members"."type" IN ('ProjectMember') AND "members"."requested_at" IS
NULL AND "members"."user_id" = $4  ORDER BY "members"."id" DESC LIMIT 1
[["source_type", "Project"], ["source_id", 1], ["source_type",
"Project"], ["user_id", 5]]
  ProjectMember Load (0.3ms)  SELECT  "members".* FROM "members" WHERE
"members"."source_type" = $1 AND "members"."type" IN ('ProjectMember')
AND "members"."source_id" = $2 AND "members"."source_type" = $3 AND
"members"."type" IN ('ProjectMember') AND "members"."requested_at" IS
NULL AND "members"."user_id" = $4  ORDER BY "members"."id" DESC LIMIT 1
[["source_type", "Project"], ["source_id", 1], ["source_type",
"Project"], ["user_id", 22]]
  ProjectMember Load (0.3ms)  SELECT  "members".* FROM "members" WHERE
"members"."source_type" = $1 AND "members"."type" IN ('ProjectMember')
AND "members"."source_id" = $2 AND "members"."source_type" = $3 AND
"members"."type" IN ('ProjectMember') AND "members"."requested_at" IS
NULL AND "members"."user_id" = $4  ORDER BY "members"."id" DESC LIMIT 1
[["source_type", "Project"], ["source_id", 1], ["source_type",
"Project"], ["user_id", 16]]
  ProjectMember Load (0.3ms)  SELECT  "members".* FROM "members" WHERE
"members"."source_type" = $1 AND "members"."type" IN ('ProjectMember')
AND "members"."source_id" = $2 AND "members"."source_type" = $3 AND
"members"."type" IN ('ProjectMember') AND "members"."requested_at" IS
NULL AND "members"."user_id" = $4  ORDER BY "members"."id" DESC LIMIT 1
[["source_type", "Project"], ["source_id", 1], ["source_type",
"Project"], ["user_id", 13]]
```

Without the N+1 queries issues:

```
  ProjectMember Load (0.3ms)  SELECT  "members".* FROM "members" WHERE
"members"."source_type" = $1 AND "members"."type" IN ('ProjectMember')
AND "members"."source_id" = $2 AND "members"."source_type" = $3 AND
"members"."type" IN ('ProjectMember') AND "members"."requested_at" IS
NULL  ORDER BY "members"."id" DESC LIMIT 20 OFFSET 0  [["source_type",
"Project"], ["source_id", 1], ["source_type", "Project"]]
  User Load (0.5ms)  SELECT "users".* FROM "users" WHERE "users"."id" IN
(5, 22, 16, 13)  ORDER BY "users"."id" DESC
```

2. `GET /projects/:id/access_requests`

With two N+1 queries issues:

```
  ProjectMember Load (0.3ms)  SELECT "members".* FROM "members" WHERE
"members"."source_type" = $1 AND "members"."type" IN ('ProjectMember')
AND "members"."source_id" = $2 AND "members"."source_type" = $3 AND
"members"."type" IN ('ProjectMember') AND ("members"."requested_at" IS
NOT NULL)  ORDER BY "members"."id" DESC  [["source_type", "Project"],
["source_id", 8], ["source_type", "Project"]]
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" =
$1  ORDER BY "users"."id" DESC LIMIT 1  [["id", 24]]
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" =
$1  ORDER BY "users"."id" DESC LIMIT 1  [["id", 23]]
  ActiveRecord::SchemaMigration Load (0.2ms)  SELECT
"schema_migrations".* FROM "schema_migrations"
  ProjectMember Load (0.1ms)  SELECT  "members".* FROM "members" WHERE
"members"."source_type" = $1 AND "members"."type" IN ('ProjectMember')
AND "members"."source_id" = $2 AND "members"."source_type" = $3 AND
"members"."type" IN ('ProjectMember') AND ("members"."requested_at" IS
NOT NULL) AND "members"."user_id" = $4  ORDER BY "members"."id" DESC
LIMIT 1  [["source_type", "Project"], ["source_id", 8], ["source_type",
"Project"], ["user_id", 24]]
  ProjectMember Load (0.2ms)  SELECT  "members".* FROM "members" WHERE
"members"."source_type" = $1 AND "members"."type" IN ('ProjectMember')
AND "members"."source_id" = $2 AND "members"."source_type" = $3 AND
"members"."type" IN ('ProjectMember') AND ("members"."requested_at" IS
NOT NULL) AND "members"."user_id" = $4  ORDER BY "members"."id" DESC
LIMIT 1  [["source_type", "Project"], ["source_id", 8], ["source_type",
"Project"], ["user_id", 23]]
```

Without the N+1 queries issues:

```
  ProjectMember Load (0.3ms)  SELECT  "members".* FROM "members" WHERE
"members"."source_type" = $1 AND "members"."type" IN ('ProjectMember')
AND "members"."source_id" = $2 AND "members"."source_type" = $3 AND
"members"."type" IN ('ProjectMember') AND ("members"."requested_at" IS
NOT NULL)  ORDER BY "members"."id" DESC LIMIT 20 OFFSET 0
[["source_type", "Project"], ["source_id", 8], ["source_type",
"Project"]]
  User Load (0.5ms)  SELECT "users".* FROM "users" WHERE "users"."id" IN
(24, 23)  ORDER BY "users"."id" DESC
```

Signed-off-by: Rémy Coutable 
---
 lib/api/access_requests.rb | 5 ++---
 lib/api/entities.rb        | 4 ++--
 lib/api/members.rb         | 6 +++---
 3 files changed, 7 insertions(+), 8 deletions(-)

diff --git a/lib/api/access_requests.rb b/lib/api/access_requests.rb
index 9c41d8aaa3e306..d02b469dac8bb2 100644
--- a/lib/api/access_requests.rb
+++ b/lib/api/access_requests.rb
@@ -18,10 +18,9 @@ class AccessRequests < Grape::API
           source = find_source(source_type, params[:id])
           authorize_admin_source!(source_type, source)
 
-          access_requesters = source.requesters
-          users = Kaminari.paginate_array(access_requesters.map(&:user))
+          access_requesters = paginate(source.requesters.includes(:user))
 
-          present paginate(users), with: Entities::AccessRequester, source: source
+          present access_requesters.map(&:user), with: Entities::AccessRequester, access_requesters: access_requesters
         end
 
         # Request access to the group/project
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index c5ff4557b4a2bc..ae74d14a4bbfad 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -93,14 +93,14 @@ class Project < Grape::Entity
 
     class Member < UserBasic
       expose :access_level do |user, options|
-        member = options[:member] || options[:source].members.find_by(user_id: user.id)
+        member = options[:member] || options[:members].find { |m| m.user_id == user.id }
         member.access_level
       end
     end
 
     class AccessRequester < UserBasic
       expose :requested_at do |user, options|
-        access_requester = options[:access_requester] || options[:source].requesters.find_by(user_id: user.id)
+        access_requester = options[:access_requester] || options[:access_requesters].find { |m| m.user_id == user.id }
         access_requester.requested_at
       end
     end
diff --git a/lib/api/members.rb b/lib/api/members.rb
index ccb7618360d96b..2fae83f60b278a 100644
--- a/lib/api/members.rb
+++ b/lib/api/members.rb
@@ -18,11 +18,11 @@ class Members < Grape::API
         get ":id/members" do
           source = find_source(source_type, params[:id])
 
-          members = source.members
+          members = source.members.includes(:user)
           members = members.joins(:user).merge(User.search(params[:query])) if params[:query]
-          users = Kaminari.paginate_array(members.map(&:user))
+          members = paginate(members)
 
-          present paginate(users), with: Entities::Member, source: source
+          present members.map(&:user), with: Entities::Member, members: members
         end
 
         # Get a group/project member
-- 
GitLab


From 88877afd09c2d72a11f4f53ba5309e64da34fdd8 Mon Sep 17 00:00:00 2001
From: Clement Ho 
Date: Wed, 10 Aug 2016 14:15:22 -0500
Subject: [PATCH 119/153] Fix branches page dropdown sort initial state

---
 CHANGELOG                                       | 1 +
 app/controllers/projects/branches_controller.rb | 3 ++-
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG b/CHANGELOG
index 2cff1ff2ee1486..7d6df922a459d0 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -23,6 +23,7 @@ v 8.11.0 (unreleased)
   - Add "No one can push" as an option for protected branches. !5081
   - Improve performance of AutolinkFilter#text_parse by using XPath
   - Add experimental Redis Sentinel support !1877
+  - Fix branches page dropdown sort initial state (ClemMakesApps)
   - Environments have an url to link to
   - Update `timeago` plugin to use multiple string/locale settings
   - Remove unused images (ClemMakesApps)
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index e926043f3ebadb..48fe81b0d7456b 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -1,12 +1,13 @@
 class Projects::BranchesController < Projects::ApplicationController
   include ActionView::Helpers::SanitizeHelper
+  include SortingHelper
   # Authorize
   before_action :require_non_empty_project
   before_action :authorize_download_code!
   before_action :authorize_push_code!, only: [:new, :create, :destroy]
 
   def index
-    @sort = params[:sort].presence || 'name'
+    @sort = params[:sort].presence || sort_value_name
     @branches = BranchesFinder.new(@repository, params).execute
     @branches = Kaminari.paginate_array(@branches).page(params[:page])
 
-- 
GitLab


From e3292f1ede67891feddfd896f16cfe42d3851558 Mon Sep 17 00:00:00 2001
From: Clement Ho 
Date: Tue, 9 Aug 2016 15:54:58 -0500
Subject: [PATCH 120/153] Fix awardable button mutuality loading spinners

---
 CHANGELOG                                |  1 +
 app/assets/javascripts/awards_handler.js | 14 +-------------
 2 files changed, 2 insertions(+), 13 deletions(-)

diff --git a/CHANGELOG b/CHANGELOG
index 7bfeff2a4ec15a..3b57a471932f7a 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -16,6 +16,7 @@ v 8.11.0 (unreleased)
   - Expand commit message width in repo view (ClemMakesApps)
   - Cache highlighted diff lines for merge requests
   - Fix of 'Commits being passed to custom hooks are already reachable when using the UI'
+  - Fix awardable button mutuality loading spinners (ClemMakesApps)
   - Add support for using RequestStore within Sidekiq tasks via SIDEKIQ_REQUEST_STORE env variable
   - Optimize maximum user access level lookup in loading of notes
   - Add "No one can push" as an option for protected branches. !5081
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index ea683b31f75a2c..2c5b83e4f1e129 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -161,23 +161,11 @@
         $emojiButton = votesBlock.find("[data-emoji=" + mutualVote + "]").parent();
         isAlreadyVoted = $emojiButton.hasClass('active');
         if (isAlreadyVoted) {
-          this.showEmojiLoader($emojiButton);
-          return this.addAward(votesBlock, awardUrl, mutualVote, false, function() {
-            return $emojiButton.removeClass('is-loading');
-          });
+          this.addAward(votesBlock, awardUrl, mutualVote, false);
         }
       }
     };
 
-    AwardsHandler.prototype.showEmojiLoader = function($emojiButton) {
-      var $loader;
-      $loader = $emojiButton.find('.fa-spinner');
-      if (!$loader.length) {
-        $emojiButton.append('');
-      }
-      return $emojiButton.addClass('is-loading');
-    };
-
     AwardsHandler.prototype.isActive = function($emojiButton) {
       return $emojiButton.hasClass('active');
     };
-- 
GitLab


From 9a9f40276f23b6ec5db360ca63c110bd5e97bc44 Mon Sep 17 00:00:00 2001
From: Keith Pope 
Date: Thu, 4 Aug 2016 14:25:37 +0100
Subject: [PATCH 121/153] Update/Improve OAuth2 Client documentation

---
 CHANGELOG         |  1 +
 doc/api/oauth2.md | 50 +++++++++++++++++++++++++++++++++++------------
 2 files changed, 39 insertions(+), 12 deletions(-)

diff --git a/CHANGELOG b/CHANGELOG
index f83a86d75df86a..38a0450793d184 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -63,6 +63,7 @@ v 8.11.0 (unreleased)
   - Add GitLab Workhorse version to admin dashboard (Katarzyna Kobierska Ula Budziszewska)
   - Allow branch names ending with .json for graph and network page !5579 (winniehell)
   - Add the `sprockets-es6` gem
+  - Improve OAuth2 client documentation (muteor)
   - Multiple trigger variables show in separate lines (Katarzyna Kobierska Ula Budziszewska)
   - Profile requests when a header is passed
   - Avoid calculation of line_code and position for _line partial when showing diff notes on discussion tab.
diff --git a/doc/api/oauth2.md b/doc/api/oauth2.md
index 7ce89adc98b763..64bc2f7c575a14 100644
--- a/doc/api/oauth2.md
+++ b/doc/api/oauth2.md
@@ -1,38 +1,59 @@
 # GitLab as an OAuth2 client
 
-This document is about using other OAuth authentication service providers to sign into GitLab.
-If you want GitLab to be an OAuth authentication service provider to sign into other services please see the [Oauth2 provider documentation](../integration/oauth_provider.md).
+This document covers using the OAuth2 protocol to access GitLab.
 
-OAuth2 is a protocol that enables us to authenticate a user without requiring them to give their password. 
+If you want GitLab to be an OAuth authentication service provider to sign into other services please see the [Oauth2 provider documentation](../integration/oauth_provider.md).
 
-Before using the OAuth2 you should create an application in user's account. Each application gets a unique App ID and App Secret parameters. You should not share these.
+OAuth2 is a protocol that enables us to authenticate a user without requiring them to give their password to a third-party. 
 
 This functionality is based on [doorkeeper gem](https://github.com/doorkeeper-gem/doorkeeper)
 
 ## Web Application Flow
 
-This flow is using for authentication from third-party web sites and is probably used the most. 
-It basically consists of an exchange of an authorization token for an access token. For more detailed info, check out the [RFC spec here](http://tools.ietf.org/html/rfc6749#section-4.1)
+This is the most common type of flow and is used by server-side clients that wish to access GitLab on a user's behalf.
+
+>**Note:**
+This flow **should not** be used for client-side clients as you would leak your `client_secret`. Client-side clients should use the Implicit Grant (which is currently unsupported).
 
-This flow consists from 3 steps.
+For more detailed information, check out the [RFC spec](http://tools.ietf.org/html/rfc6749#section-4.1)
+
+In the following sections you will be introduced to the three steps needed for this flow.
 
 ### 1. Registering the client
 
-Create an application in user's account profile.
+First, you should create an application (`/profile/applications`) in your user's account.
+Each application gets a unique App ID and App Secret parameters. 
+
+>**Note:**
+**You should not share/leak your App ID or App Secret.**
 
 ### 2. Requesting authorization
 
-To request the authorization token, you should visit the `/oauth/authorize` endpoint. You can do that by visiting manually the URL:
+To request the authorization code, you should redirect the user to the `/oauth/authorize` endpoint:
+
+```
+https://gitlab.example.com/oauth/authorize?client_id=APP_ID&redirect_uri=REDIRECT_URI&response_type=code&state=your_unique_state_hash
+```
+
+This will ask the user to approve the applications access to their account and then redirect back to the `REDIRECT_URI` you provided.
+
+The redirect will include the GET `code` parameter, for example:
 
 ```
-http://localhost:3000/oauth/authorize?client_id=APP_ID&redirect_uri=REDIRECT_URI&response_type=code
+http://myapp.com/oauth/redirect?code=1234567890&state=your_unique_state_hash
 ```
 
-Where REDIRECT_URI is the URL in your app where users will be sent after authorization. 	
+You should then use the `code` to request an access token.
+
+>**Important:**
+It is highly recommended that you send a `state` value with the request to `/oauth/authorize` and 
+validate that value is returned and matches in the redirect request. 
+This is important to prevent [CSFR attacks](http://www.oauthsecurity.com/#user-content-authorization-code-flow), 
+`state` really should have been a requirement in the standard! 
 
 ### 3. Requesting the access token
 
-To request the access token, you should use the returned code and exchange it for an access token. To do that you can use any HTTP client. In this case, I used rest-client:
+Once you have the authorization code you can request an `access_token` using the code, to do that you can use any HTTP client. In the following example, we are using Ruby's `rest-client`:
 
 ```
 parameters = 'client_id=APP_ID&client_secret=APP_SECRET&code=RETURNED_CODE&grant_type=authorization_code&redirect_uri=REDIRECT_URI'
@@ -46,6 +67,8 @@ RestClient.post 'http://localhost:3000/oauth/token', parameters
  "refresh_token": "8257e65c97202ed1726cf9571600918f3bffb2544b26e00a61df9897668c33a1"
 }
 ```
+>**Note:**
+The `redirect_uri` must match the `redirect_uri` used in the original authorization request.
 
 You can now make requests to the API with the access token returned.
 
@@ -77,6 +100,9 @@ The credentials should only be used when there is a high degree of trust between
 client is part of the device operating system or a highly privileged application), and when other authorization grant types are not
 available (such as an authorization code).
 
+>**Important:**
+Never store the users credentials and only use this grant type when your client is deployed to a trusted environment, in 99% of cases [personal access tokens] are a better choice.
+
 Even though this grant type requires direct client access to the resource owner credentials, the resource owner credentials are used
 for a single request and are exchanged for an access token.  This grant type can eliminate the need for the client to store the
 resource owner credentials for future use, by exchanging the credentials with a long-lived access token or refresh token.
-- 
GitLab


From fb748daf538e43efcf8884f017391bcbfccf2ea2 Mon Sep 17 00:00:00 2001
From: Thomas Balthazar 
Date: Wed, 10 Aug 2016 12:25:01 +0200
Subject: [PATCH 122/153] Replace the tinder gem by bare HTTP requests

---
 CHANGELOG                                     |  1 +
 Gemfile                                       |  3 -
 Gemfile.lock                                  | 16 -----
 .../project_services/campfire_service.rb      | 51 +++++++++++++---
 .../project_services/campfire/rooms.json      | 22 +++++++
 .../project_services/campfire/rooms2.json     | 22 +++++++
 .../project_services/campfire_service_spec.rb | 58 +++++++++++++++++++
 7 files changed, 147 insertions(+), 26 deletions(-)
 create mode 100644 spec/fixtures/project_services/campfire/rooms.json
 create mode 100644 spec/fixtures/project_services/campfire/rooms2.json

diff --git a/CHANGELOG b/CHANGELOG
index 7bfeff2a4ec15a..77e9889dd264f3 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,6 +1,7 @@
 Please view this file on the master branch, on stable branches it's out of date.
 
 v 8.11.0 (unreleased)
+  - Remove the http_parser.rb dependency by removing the tinder gem. !5758 (tbalthazar)
   - Fix don't pass a local variable called `i` to a partial. !20510 (herminiotorres)
   - Fix rename `add_users_into_project` and `projects_ids`. !20512 (herminiotorres)
   - Fix the title of the toggle dropdown button. !5515 (herminiotorres)
diff --git a/Gemfile b/Gemfile
index 104929665e8f9a..5c781cda74845d 100644
--- a/Gemfile
+++ b/Gemfile
@@ -163,9 +163,6 @@ gem 'redis-rails', '~> 4.0.0'
 gem 'redis', '~> 3.2'
 gem 'connection_pool', '~> 2.0'
 
-# Campfire integration
-gem 'tinder', '~> 1.10.0'
-
 # HipChat integration
 gem 'hipchat', '~> 1.5.0'
 
diff --git a/Gemfile.lock b/Gemfile.lock
index 87f08d6f372d07..ff4c03ac61ea82 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -335,7 +335,6 @@ GEM
       activesupport (>= 2)
       nokogiri (~> 1.4)
     htmlentities (4.3.4)
-    http_parser.rb (0.5.3)
     httparty (0.13.7)
       json (~> 1.8)
       multi_xml (>= 0.5.2)
@@ -672,7 +671,6 @@ GEM
       redis-namespace (>= 1.5.2)
       rufus-scheduler (>= 2.0.24)
       sidekiq (>= 4.0.0)
-    simple_oauth (0.1.9)
     simplecov (0.12.0)
       docile (~> 1.1.0)
       json (>= 1.8, < 3)
@@ -742,21 +740,8 @@ GEM
     tilt (2.0.5)
     timecop (0.8.1)
     timfel-krb5-auth (0.8.3)
-    tinder (1.10.1)
-      eventmachine (~> 1.0)
-      faraday (~> 0.9.0)
-      faraday_middleware (~> 0.9)
-      hashie (>= 1.0)
-      json (~> 1.8.0)
-      mime-types
-      multi_json (~> 1.7)
-      twitter-stream (~> 0.1)
     turbolinks (2.5.3)
       coffee-rails
-    twitter-stream (0.1.16)
-      eventmachine (>= 0.12.8)
-      http_parser.rb (~> 0.5.1)
-      simple_oauth (~> 0.1.4)
     tzinfo (1.2.2)
       thread_safe (~> 0.1)
     u2f (0.2.1)
@@ -981,7 +966,6 @@ DEPENDENCIES
   teaspoon-jasmine (~> 2.2.0)
   test_after_commit (~> 0.4.2)
   thin (~> 1.7.0)
-  tinder (~> 1.10.0)
   turbolinks (~> 2.5.0)
   u2f (~> 0.2.1)
   uglifier (~> 2.7.2)
diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb
index 511b2eac792dbd..5af93860d09df9 100644
--- a/app/models/project_services/campfire_service.rb
+++ b/app/models/project_services/campfire_service.rb
@@ -1,4 +1,6 @@
 class CampfireService < Service
+  include HTTParty
+
   prop_accessor :token, :subdomain, :room
   validates :token, presence: true, if: :activated?
 
@@ -29,18 +31,53 @@ def supported_events
   def execute(data)
     return unless supported_events.include?(data[:object_kind])
 
-    room = gate.find_room_by_name(self.room)
-    return true unless room
-
+    self.class.base_uri base_uri
     message = build_message(data)
-
-    room.speak(message)
+    speak(self.room, message, auth)
   end
 
   private
 
-  def gate
-    @gate ||= Tinder::Campfire.new(subdomain, token: token)
+  def base_uri
+    @base_uri ||= "https://#{subdomain}.campfirenow.com"
+  end
+
+  def auth
+    # use a dummy password, as explained in the Campfire API doc:
+    # https://github.com/basecamp/campfire-api#authentication
+    @auth ||= {
+      basic_auth: {
+        username: token,
+        password: 'X'
+      }
+    }
+  end
+
+  # Post a message into a room, returns the message Hash in case of success.
+  # Returns nil otherwise.
+  # https://github.com/basecamp/campfire-api/blob/master/sections/messages.md#create-message
+  def speak(room_name, message, auth)
+    room = rooms(auth).find { |r| r["name"] == room_name }
+    return nil unless room
+
+    path = "/room/#{room["id"]}/speak.json"
+    body = {
+      body: {
+        message: {
+          type: 'TextMessage',
+          body: message
+        }
+      }
+    }
+    res = self.class.post(path, auth.merge(body))
+    res.code == 201 ? res : nil
+  end
+
+  # Returns a list of rooms, or [].
+  # https://github.com/basecamp/campfire-api/blob/master/sections/rooms.md#get-rooms
+  def rooms(auth)
+    res = self.class.get("/rooms.json", auth) 
+    res.code == 200 ? res["rooms"] : []
   end
 
   def build_message(push)
diff --git a/spec/fixtures/project_services/campfire/rooms.json b/spec/fixtures/project_services/campfire/rooms.json
new file mode 100644
index 00000000000000..71e9645c955f88
--- /dev/null
+++ b/spec/fixtures/project_services/campfire/rooms.json
@@ -0,0 +1,22 @@
+{
+  "rooms": [
+    {
+      "name": "test-room",
+      "locked": false,
+      "created_at": "2009/01/07 20:43:11 +0000",
+      "updated_at": "2009/03/18 14:31:39 +0000",
+      "topic": "The room topic\n",
+      "id": 123,
+      "membership_limit": 4
+    },
+    {
+      "name": "another room",
+      "locked": true,
+      "created_at": "2009/03/18 14:30:42 +0000",
+      "updated_at": "2013/01/27 14:14:27 +0000",
+      "topic": "Comment, ideas, GitHub notifications for eCommittee App",
+      "id": 456,
+      "membership_limit": 4
+    }
+  ]
+}
diff --git a/spec/fixtures/project_services/campfire/rooms2.json b/spec/fixtures/project_services/campfire/rooms2.json
new file mode 100644
index 00000000000000..3d5f635d8b39a8
--- /dev/null
+++ b/spec/fixtures/project_services/campfire/rooms2.json
@@ -0,0 +1,22 @@
+{
+  "rooms": [
+    {
+      "name": "test-room-not-found",
+      "locked": false,
+      "created_at": "2009/01/07 20:43:11 +0000",
+      "updated_at": "2009/03/18 14:31:39 +0000",
+      "topic": "The room topic\n",
+      "id": 123,
+      "membership_limit": 4
+    },
+    {
+      "name": "another room",
+      "locked": true,
+      "created_at": "2009/03/18 14:30:42 +0000",
+      "updated_at": "2013/01/27 14:14:27 +0000",
+      "topic": "Comment, ideas, GitHub notifications for eCommittee App",
+      "id": 456,
+      "membership_limit": 4
+    }
+  ]
+}
diff --git a/spec/models/project_services/campfire_service_spec.rb b/spec/models/project_services/campfire_service_spec.rb
index 3e6da42803b800..1adf93258f3512 100644
--- a/spec/models/project_services/campfire_service_spec.rb
+++ b/spec/models/project_services/campfire_service_spec.rb
@@ -39,4 +39,62 @@
       it { is_expected.not_to validate_presence_of(:token) }
     end
   end
+
+  describe "#execute" do
+    let(:user)    { create(:user) }
+    let(:project) { create(:project) }
+
+    before do
+      @campfire_service = CampfireService.new
+      allow(@campfire_service).to receive_messages(
+        project_id: project.id,
+        project: project,
+        service_hook: true,
+        token: 'verySecret',
+        subdomain: 'project-name',
+        room: 'test-room'
+      )
+      @sample_data = Gitlab::PushDataBuilder.build_sample(project, user)
+      @rooms_url = 'https://verySecret:X@project-name.campfirenow.com/rooms.json'
+      @headers = { 'Content-Type' => 'application/json; charset=utf-8' }
+    end
+
+    it "calls Campfire API to get a list of rooms and speak in a room" do
+      # make sure a valid list of rooms is returned
+      body = File.read(Rails.root + 'spec/fixtures/project_services/campfire/rooms.json')
+      WebMock.stub_request(:get, @rooms_url).to_return(
+        body: body,
+        status: 200,
+        headers: @headers
+      )
+      # stub the speak request with the room id found in the previous request's response
+      speak_url = 'https://verySecret:X@project-name.campfirenow.com/room/123/speak.json'
+      WebMock.stub_request(:post, speak_url)
+
+      @campfire_service.execute(@sample_data)
+
+      expect(WebMock).to have_requested(:get, @rooms_url).once
+      expect(WebMock).to have_requested(:post, speak_url).with(
+        body: /#{project.path}.*#{@sample_data[:before]}.*#{@sample_data[:after]}/
+      ).once
+    end
+
+    it "calls Campfire API to get a list of rooms but shouldn't speak in a room" do
+      # return a list of rooms that do not contain a room named 'test-room'
+      body = File.read(Rails.root + 'spec/fixtures/project_services/campfire/rooms2.json')
+      WebMock.stub_request(:get, @rooms_url).to_return(
+        body: body,
+        status: 200,
+        headers: @headers
+      )
+      # we want to make sure no request is sent to the /speak endpoint, here is a basic
+      # regexp that matches this endpoint
+      speak_url = 'https://verySecret:X@project-name.campfirenow.com/room/.*/speak.json'
+
+      @campfire_service.execute(@sample_data)
+
+      expect(WebMock).to have_requested(:get, @rooms_url).once
+      expect(WebMock).not_to have_requested(:post, /#{speak_url}/)
+    end
+  end
 end
-- 
GitLab


From 115c00fd7e1efb249bd603d20d50a8e23ca45ee7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A9my=20Coutable?= 
Date: Wed, 10 Aug 2016 19:06:07 +0200
Subject: [PATCH 123/153] Fix doc linting errors and remove useless API specs
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Rémy Coutable 
---
 doc/api/access_requests.md                |  16 +-
 doc/api/members.md                        |  20 +--
 spec/requests/api/group_members_spec.rb   | 199 ----------------------
 spec/requests/api/project_members_spec.rb | 166 ------------------
 4 files changed, 18 insertions(+), 383 deletions(-)
 delete mode 100644 spec/requests/api/group_members_spec.rb
 delete mode 100644 spec/requests/api/project_members_spec.rb

diff --git a/doc/api/access_requests.md b/doc/api/access_requests.md
index 261585af282f74..ea308b54d62d6d 100644
--- a/doc/api/access_requests.md
+++ b/doc/api/access_requests.md
@@ -30,8 +30,8 @@ GET /projects/:id/access_requests
 | `id`      | integer/string | yes | The group/project ID or path |
 
 ```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/access_requests
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/access_requests
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/access_requests
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/access_requests
 ```
 
 Example response:
@@ -73,8 +73,8 @@ POST /projects/:id/access_requests
 | `id`      | integer/string | yes | The group/project ID or path |
 
 ```bash
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/access_requests
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/access_requests
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/access_requests
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/access_requests
 ```
 
 Example response:
@@ -108,8 +108,8 @@ PUT /projects/:id/access_requests/:user_id/approve
 | `access_level` | integer | no | A valid access level (defaults: `30`, developer access level) |
 
 ```bash
-curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/access_requests/:user_id/approve?access_level=20
-curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/access_requests/:user_id/approve?access_level=20
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/access_requests/:user_id/approve?access_level=20
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/access_requests/:user_id/approve?access_level=20
 ```
 
 Example response:
@@ -142,6 +142,6 @@ DELETE /projects/:id/access_requests/:user_id
 | `user_id` | integer | yes   | The user ID of the access requester |
 
 ```bash
-curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/access_requests/:user_id
-curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/access_requests/:user_id
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/access_requests/:user_id
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/access_requests/:user_id
 ```
diff --git a/doc/api/members.md b/doc/api/members.md
index 4cfc27c300bc53..d002e6eaf89ec2 100644
--- a/doc/api/members.md
+++ b/doc/api/members.md
@@ -29,8 +29,8 @@ GET /projects/:id/members
 | `query`   | string | no     | A query string to search for members |
 
 ```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members
 ```
 
 Example response:
@@ -73,8 +73,8 @@ GET /projects/:id/members/:user_id
 | `user_id` | integer | yes   | The user ID of the member |
 
 ```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members/:user_id
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members/:user_id
 ```
 
 Example response:
@@ -108,8 +108,8 @@ POST /projects/:id/members
 | `access_level` | integer | yes | A valid access level |
 
 ```bash
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id?access_level=30
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members/:user_id?access_level=30
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id?access_level=30
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members/:user_id?access_level=30
 ```
 
 Example response:
@@ -143,8 +143,8 @@ PUT /projects/:id/members/:user_id
 | `access_level` | integer | yes | A valid access level |
 
 ```bash
-curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id?access_level=40
-curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members/:user_id?access_level=40
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id?access_level=40
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members/:user_id?access_level=40
 ```
 
 Example response:
@@ -177,6 +177,6 @@ DELETE /projects/:id/members/:user_id
 | `user_id` | integer | yes   | The user ID of the member |
 
 ```bash
-curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id
-curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members/:user_id
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members/:user_id
 ```
diff --git a/spec/requests/api/group_members_spec.rb b/spec/requests/api/group_members_spec.rb
deleted file mode 100644
index 8bd6a8062ae774..00000000000000
--- a/spec/requests/api/group_members_spec.rb
+++ /dev/null
@@ -1,199 +0,0 @@
-require 'spec_helper'
-
-describe API::API, api: true  do
-  include ApiHelpers
-
-  let(:owner) { create(:user) }
-  let(:reporter) { create(:user) }
-  let(:developer) { create(:user) }
-  let(:master) { create(:user) }
-  let(:guest) { create(:user) }
-  let(:stranger) { create(:user) }
-
-  let!(:group_with_members) do
-    group = create(:group, :private)
-    group.add_users([reporter.id], GroupMember::REPORTER)
-    group.add_users([developer.id], GroupMember::DEVELOPER)
-    group.add_users([master.id], GroupMember::MASTER)
-    group.add_users([guest.id], GroupMember::GUEST)
-    group
-  end
-
-  let!(:group_no_members) { create(:group) }
-
-  before do
-    group_with_members.add_owner owner
-    group_no_members.add_owner owner
-  end
-
-  describe "GET /groups/:id/members" do
-    context "when authenticated as user that is part or the group" do
-      it "each user: returns an array of members groups of group3" do
-        [owner, master, developer, reporter, guest].each do |user|
-          get api("/groups/#{group_with_members.id}/members", user)
-          expect(response).to have_http_status(200)
-          expect(json_response).to be_an Array
-          expect(json_response.size).to eq(5)
-          expect(json_response.find { |e| e['id'] == owner.id }['access_level']).to eq(GroupMember::OWNER)
-          expect(json_response.find { |e| e['id'] == reporter.id }['access_level']).to eq(GroupMember::REPORTER)
-          expect(json_response.find { |e| e['id'] == developer.id }['access_level']).to eq(GroupMember::DEVELOPER)
-          expect(json_response.find { |e| e['id'] == master.id }['access_level']).to eq(GroupMember::MASTER)
-          expect(json_response.find { |e| e['id'] == guest.id }['access_level']).to eq(GroupMember::GUEST)
-        end
-      end
-
-      it 'users not part of the group should get access error' do
-        get api("/groups/#{group_with_members.id}/members", stranger)
-
-        expect(response).to have_http_status(404)
-      end
-    end
-  end
-
-  describe "POST /groups/:id/members" do
-    context "when not a member of the group" do
-      it "does not add guest as member of group_no_members when adding being done by person outside the group" do
-        post api("/groups/#{group_no_members.id}/members", reporter), user_id: guest.id, access_level: GroupMember::MASTER
-        expect(response).to have_http_status(403)
-      end
-    end
-
-    context "when a member of the group" do
-      it "returns ok and add new member" do
-        new_user = create(:user)
-
-        expect do
-          post api("/groups/#{group_no_members.id}/members", owner), user_id: new_user.id, access_level: GroupMember::MASTER
-        end.to change { group_no_members.members.count }.by(1)
-
-        expect(response).to have_http_status(201)
-        expect(json_response['name']).to eq(new_user.name)
-        expect(json_response['access_level']).to eq(GroupMember::MASTER)
-      end
-
-      it "does not allow guest to modify group members" do
-        new_user = create(:user)
-
-        expect do
-          post api("/groups/#{group_with_members.id}/members", guest), user_id: new_user.id, access_level: GroupMember::MASTER
-        end.not_to change { group_with_members.members.count }
-
-        expect(response).to have_http_status(403)
-      end
-
-      it "returns error if member already exists" do
-        post api("/groups/#{group_with_members.id}/members", owner), user_id: master.id, access_level: GroupMember::MASTER
-        expect(response).to have_http_status(409)
-      end
-
-      it "returns a 400 error when user id is not given" do
-        post api("/groups/#{group_no_members.id}/members", owner), access_level: GroupMember::MASTER
-        expect(response).to have_http_status(400)
-      end
-
-      it "returns a 400 error when access level is not given" do
-        post api("/groups/#{group_no_members.id}/members", owner), user_id: master.id
-        expect(response).to have_http_status(400)
-      end
-
-      it "returns a 422 error when access level is not known" do
-        post api("/groups/#{group_no_members.id}/members", owner), user_id: master.id, access_level: 1234
-        expect(response).to have_http_status(422)
-      end
-    end
-  end
-
-  describe 'PUT /groups/:id/members/:user_id' do
-    context 'when not a member of the group' do
-      it 'returns a 409 error if the user is not a group member' do
-        put(
-          api("/groups/#{group_no_members.id}/members/#{developer.id}",
-              owner), access_level: GroupMember::MASTER
-        )
-        expect(response).to have_http_status(404)
-      end
-    end
-
-    context 'when a member of the group' do
-      it 'returns ok and update member access level' do
-        put(
-          api("/groups/#{group_with_members.id}/members/#{reporter.id}",
-              owner),
-          access_level: GroupMember::MASTER
-        )
-
-        expect(response).to have_http_status(200)
-
-        get api("/groups/#{group_with_members.id}/members", owner)
-        json_reporter = json_response.find do |e|
-          e['id'] == reporter.id
-        end
-
-        expect(json_reporter['access_level']).to eq(GroupMember::MASTER)
-      end
-
-      it 'does not allow guest to modify group members' do
-        put(
-          api("/groups/#{group_with_members.id}/members/#{developer.id}",
-              guest),
-          access_level: GroupMember::MASTER
-        )
-
-        expect(response).to have_http_status(403)
-
-        get api("/groups/#{group_with_members.id}/members", owner)
-        json_developer = json_response.find do |e|
-          e['id'] == developer.id
-        end
-
-        expect(json_developer['access_level']).to eq(GroupMember::DEVELOPER)
-      end
-
-      it 'returns a 400 error when access level is not given' do
-        put(
-          api("/groups/#{group_with_members.id}/members/#{master.id}", owner)
-        )
-        expect(response).to have_http_status(400)
-      end
-
-      it 'returns a 422 error when access level is not known' do
-        put(
-          api("/groups/#{group_with_members.id}/members/#{master.id}", owner),
-          access_level: 1234
-        )
-        expect(response).to have_http_status(422)
-      end
-    end
-  end
-
-  describe 'DELETE /groups/:id/members/:user_id' do
-    context 'when not a member of the group' do
-      it "does not delete guest's membership of group_with_members" do
-        random_user = create(:user)
-        delete api("/groups/#{group_with_members.id}/members/#{owner.id}", random_user)
-
-        expect(response).to have_http_status(404)
-      end
-    end
-
-    context "when a member of the group" do
-      it "deletes guest's membership of group" do
-        expect do
-          delete api("/groups/#{group_with_members.id}/members/#{guest.id}", owner)
-        end.to change { group_with_members.members.count }.by(-1)
-
-        expect(response).to have_http_status(200)
-      end
-
-      it "returns a 404 error when user id is not known" do
-        delete api("/groups/#{group_with_members.id}/members/1328", owner)
-        expect(response).to have_http_status(404)
-      end
-
-      it "does not allow guest to modify group members" do
-        delete api("/groups/#{group_with_members.id}/members/#{master.id}", guest)
-        expect(response).to have_http_status(403)
-      end
-    end
-  end
-end
diff --git a/spec/requests/api/project_members_spec.rb b/spec/requests/api/project_members_spec.rb
deleted file mode 100644
index 061c7b78edbaa9..00000000000000
--- a/spec/requests/api/project_members_spec.rb
+++ /dev/null
@@ -1,166 +0,0 @@
-require 'spec_helper'
-
-describe API::API, api: true  do
-  include ApiHelpers
-  let(:user) { create(:user) }
-  let(:user2) { create(:user) }
-  let(:user3) { create(:user) }
-  let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
-  let(:project_member) { create(:project_member, :master, user: user, project: project) }
-  let(:project_member2) { create(:project_member, :developer, user: user3, project: project) }
-
-  describe "GET /projects/:id/members" do
-    before { project_member }
-    before { project_member2 }
-
-    it "returns project team members" do
-      get api("/projects/#{project.id}/members", user)
-      expect(response).to have_http_status(200)
-      expect(json_response).to be_an Array
-      expect(json_response.count).to eq(2)
-      expect(json_response.map { |u| u['username'] }).to include user.username
-    end
-
-    it "finds team members with query string" do
-      get api("/projects/#{project.id}/members", user), query: user.username
-      expect(response).to have_http_status(200)
-      expect(json_response).to be_an Array
-      expect(json_response.count).to eq(1)
-      expect(json_response.first['username']).to eq(user.username)
-    end
-
-    it "returns a 404 error if id not found" do
-      get api("/projects/9999/members", user)
-      expect(response).to have_http_status(404)
-    end
-  end
-
-  describe "GET /projects/:id/members/:user_id" do
-    before { project_member }
-
-    it "returns project team member" do
-      get api("/projects/#{project.id}/members/#{user.id}", user)
-      expect(response).to have_http_status(200)
-      expect(json_response['username']).to eq(user.username)
-      expect(json_response['access_level']).to eq(ProjectMember::MASTER)
-    end
-
-    it "returns a 404 error if user id not found" do
-      get api("/projects/#{project.id}/members/1234", user)
-      expect(response).to have_http_status(404)
-    end
-  end
-
-  describe "POST /projects/:id/members" do
-    it "adds user to project team" do
-      expect do
-        post api("/projects/#{project.id}/members", user), user_id: user2.id, access_level: ProjectMember::DEVELOPER
-      end.to change { ProjectMember.count }.by(1)
-
-      expect(response).to have_http_status(201)
-      expect(json_response['username']).to eq(user2.username)
-      expect(json_response['access_level']).to eq(ProjectMember::DEVELOPER)
-    end
-
-    it "returns a 201 status if user is already project member" do
-      post api("/projects/#{project.id}/members", user),
-           user_id: user2.id,
-           access_level: ProjectMember::DEVELOPER
-      expect do
-        post api("/projects/#{project.id}/members", user), user_id: user2.id, access_level: ProjectMember::DEVELOPER
-      end.not_to change { ProjectMember.count }
-
-      expect(response).to have_http_status(201)
-      expect(json_response['username']).to eq(user2.username)
-      expect(json_response['access_level']).to eq(ProjectMember::DEVELOPER)
-    end
-
-    it "returns a 400 error when user id is not given" do
-      post api("/projects/#{project.id}/members", user), access_level: ProjectMember::MASTER
-      expect(response).to have_http_status(400)
-    end
-
-    it "returns a 400 error when access level is not given" do
-      post api("/projects/#{project.id}/members", user), user_id: user2.id
-      expect(response).to have_http_status(400)
-    end
-
-    it "returns a 422 error when access level is not known" do
-      post api("/projects/#{project.id}/members", user), user_id: user2.id, access_level: 1234
-      expect(response).to have_http_status(422)
-    end
-  end
-
-  describe "PUT /projects/:id/members/:user_id" do
-    before { project_member2 }
-
-    it "updates project team member" do
-      put api("/projects/#{project.id}/members/#{user3.id}", user), access_level: ProjectMember::MASTER
-      expect(response).to have_http_status(200)
-      expect(json_response['username']).to eq(user3.username)
-      expect(json_response['access_level']).to eq(ProjectMember::MASTER)
-    end
-
-    it "returns a 404 error if user_id is not found" do
-      put api("/projects/#{project.id}/members/1234", user), access_level: ProjectMember::MASTER
-      expect(response).to have_http_status(404)
-    end
-
-    it "returns a 400 error when access level is not given" do
-      put api("/projects/#{project.id}/members/#{user3.id}", user)
-      expect(response).to have_http_status(400)
-    end
-
-    it "returns a 422 error when access level is not known" do
-      put api("/projects/#{project.id}/members/#{user3.id}", user), access_level: 123
-      expect(response).to have_http_status(422)
-    end
-  end
-
-  describe "DELETE /projects/:id/members/:user_id" do
-    before do
-      project_member
-      project_member2
-    end
-
-    it "removes user from project team" do
-      expect do
-        delete api("/projects/#{project.id}/members/#{user3.id}", user)
-      end.to change { ProjectMember.count }.by(-1)
-    end
-
-    it "returns 200 if team member is not part of a project" do
-      delete api("/projects/#{project.id}/members/#{user3.id}", user)
-      expect do
-        delete api("/projects/#{project.id}/members/#{user3.id}", user)
-      end.not_to change { ProjectMember.count }
-      expect(response).to have_http_status(200)
-    end
-
-    it "returns 200 if team member already removed" do
-      delete api("/projects/#{project.id}/members/#{user3.id}", user)
-      delete api("/projects/#{project.id}/members/#{user3.id}", user)
-      expect(response).to have_http_status(200)
-    end
-
-    it "returns 200 OK when the user was not member" do
-      expect do
-        delete api("/projects/#{project.id}/members/1000000", user)
-      end.to change { ProjectMember.count }.by(0)
-      expect(response).to have_http_status(200)
-      expect(json_response['id']).to eq(1000000)
-      expect(json_response['message']).to eq('Access revoked')
-    end
-
-    context 'when the user is not an admin or owner' do
-      it 'can leave the project' do
-        expect do
-          delete api("/projects/#{project.id}/members/#{user3.id}", user3)
-        end.to change { ProjectMember.count }.by(-1)
-
-        expect(response).to have_http_status(200)
-        expect(json_response['id']).to eq(user3.id)
-      end
-    end
-  end
-end
-- 
GitLab


From 96ebc8c4f7223091d97c38c442d68c8058d26261 Mon Sep 17 00:00:00 2001
From: bogdanvlviv 
Date: Wed, 10 Aug 2016 00:23:25 +0300
Subject: [PATCH 124/153] Use `File::exist?` instead of `File::exists?`

Since version ruby-2.2.0, method `File::exists?` is deprecated.
---
 CHANGELOG                             |  1 +
 lib/backup/files.rb                   |  2 +-
 lib/backup/manager.rb                 |  2 +-
 lib/backup/repository.rb              |  8 ++++----
 lib/tasks/gitlab/check.rake           | 20 ++++++++++----------
 lib/tasks/gitlab/shell.rake           |  2 +-
 lib/tasks/spinach.rake                |  2 +-
 spec/tasks/gitlab/backup_rake_spec.rb |  2 +-
 8 files changed, 20 insertions(+), 19 deletions(-)

diff --git a/CHANGELOG b/CHANGELOG
index 3b567d9ec2c4ed..cb0d1fc5839e18 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -95,6 +95,7 @@ v 8.11.0 (unreleased)
   - Avoid to show the original password field when password is automatically set. !5712 (duduribeiro)
   - Fix importing GitLab projects with an invalid MR source project
   - Sort folders with submodules in Files view !5521
+  - Each `File::exists?` replaced to `File::exist?` because of deprecate since ruby version 2.2.0
 
 v 8.10.5
   - Add a data migration to fix some missing timestamps in the members table. !5670
diff --git a/lib/backup/files.rb b/lib/backup/files.rb
index 654b4d1c8962dc..cedbb289f6a80b 100644
--- a/lib/backup/files.rb
+++ b/lib/backup/files.rb
@@ -27,7 +27,7 @@ def restore
 
     def backup_existing_files_dir
       timestamped_files_path = File.join(files_parent_dir, "#{name}.#{Time.now.to_i}")
-      if File.exists?(app_files_dir)
+      if File.exist?(app_files_dir)
         FileUtils.mv(app_files_dir, File.expand_path(timestamped_files_path))
       end
     end
diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb
index 2ff3e3bdfb017b..0dfffaf0bc6d6d 100644
--- a/lib/backup/manager.rb
+++ b/lib/backup/manager.rb
@@ -114,7 +114,7 @@ def unpack
 
       tar_file = ENV["BACKUP"].nil? ? File.join("#{file_list.first}_gitlab_backup.tar") : File.join(ENV["BACKUP"] + "_gitlab_backup.tar")
 
-      unless File.exists?(tar_file)
+      unless File.exist?(tar_file)
         puts "The specified backup doesn't exist!"
         exit 1
       end
diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb
index 1f5917b8127536..f117fc3d37def0 100644
--- a/lib/backup/repository.rb
+++ b/lib/backup/repository.rb
@@ -28,7 +28,7 @@ def dump
 
         wiki = ProjectWiki.new(project)
 
-        if File.exists?(path_to_repo(wiki))
+        if File.exist?(path_to_repo(wiki))
           $progress.print " * #{wiki.path_with_namespace} ... "
           if wiki.repository.empty?
             $progress.puts " [SKIPPED]".color(:cyan)
@@ -49,7 +49,7 @@ def dump
 
     def restore
       Gitlab.config.repositories.storages.each do |name, path|
-        next unless File.exists?(path)
+        next unless File.exist?(path)
 
         # Move repos dir to 'repositories.old' dir
         bk_repos_path = File.join(path, '..', 'repositories.old.' + Time.now.to_i.to_s)
@@ -63,7 +63,7 @@ def restore
 
         project.ensure_dir_exist
 
-        if File.exists?(path_to_bundle(project))
+        if File.exist?(path_to_bundle(project))
           FileUtils.mkdir_p(path_to_repo(project))
           cmd = %W(tar -xf #{path_to_bundle(project)} -C #{path_to_repo(project)})
         else
@@ -80,7 +80,7 @@ def restore
 
         wiki = ProjectWiki.new(project)
 
-        if File.exists?(path_to_bundle(wiki))
+        if File.exist?(path_to_bundle(wiki))
           $progress.print " * #{wiki.path_with_namespace} ... "
 
           # If a wiki bundle exists, first remove the empty repo
diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index 0894994200f2a3..5f4a6bbfa35327 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -64,7 +64,7 @@ namespace :gitlab do
           for_more_information(
             see_installation_guide_section "GitLab"
           )
-       end
+        end
       end
     end
 
@@ -73,7 +73,7 @@ namespace :gitlab do
 
       database_config_file = Rails.root.join("config", "database.yml")
 
-      if File.exists?(database_config_file)
+      if File.exist?(database_config_file)
         puts "yes".color(:green)
       else
         puts "no".color(:red)
@@ -94,7 +94,7 @@ namespace :gitlab do
 
       gitlab_config_file = Rails.root.join("config", "gitlab.yml")
 
-      if File.exists?(gitlab_config_file)
+      if File.exist?(gitlab_config_file)
         puts "yes".color(:green)
       else
         puts "no".color(:red)
@@ -113,7 +113,7 @@ namespace :gitlab do
       print "GitLab config outdated? ... "
 
       gitlab_config_file = Rails.root.join("config", "gitlab.yml")
-      unless File.exists?(gitlab_config_file)
+      unless File.exist?(gitlab_config_file)
         puts "can't check because of previous errors".color(:magenta)
       end
 
@@ -144,7 +144,7 @@ namespace :gitlab do
 
       script_path = "/etc/init.d/gitlab"
 
-      if File.exists?(script_path)
+      if File.exist?(script_path)
         puts "yes".color(:green)
       else
         puts "no".color(:red)
@@ -169,7 +169,7 @@ namespace :gitlab do
       recipe_path = Rails.root.join("lib/support/init.d/", "gitlab")
       script_path = "/etc/init.d/gitlab"
 
-      unless File.exists?(script_path)
+      unless File.exist?(script_path)
         puts "can't check because of previous errors".color(:magenta)
         return
       end
@@ -361,7 +361,7 @@ namespace :gitlab do
       Gitlab.config.repositories.storages.each do |name, repo_base_path|
         print "#{name}... "
 
-        if File.exists?(repo_base_path)
+        if File.exist?(repo_base_path)
           puts "yes".color(:green)
         else
           puts "no".color(:red)
@@ -385,7 +385,7 @@ namespace :gitlab do
       Gitlab.config.repositories.storages.each do |name, repo_base_path|
         print "#{name}... "
 
-        unless File.exists?(repo_base_path)
+        unless File.exist?(repo_base_path)
           puts "can't check because of previous errors".color(:magenta)
           return
         end
@@ -408,7 +408,7 @@ namespace :gitlab do
       Gitlab.config.repositories.storages.each do |name, repo_base_path|
         print "#{name}... "
 
-        unless File.exists?(repo_base_path)
+        unless File.exist?(repo_base_path)
           puts "can't check because of previous errors".color(:magenta)
           return
         end
@@ -438,7 +438,7 @@ namespace :gitlab do
       Gitlab.config.repositories.storages.each do |name, repo_base_path|
         print "#{name}... "
 
-        unless File.exists?(repo_base_path)
+        unless File.exist?(repo_base_path)
           puts "can't check because of previous errors".color(:magenta)
           return
         end
diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake
index ba93945bd03187..bb7eb852f1b860 100644
--- a/lib/tasks/gitlab/shell.rake
+++ b/lib/tasks/gitlab/shell.rake
@@ -90,7 +90,7 @@ namespace :gitlab do
     task build_missing_projects: :environment do
       Project.find_each(batch_size: 1000) do |project|
         path_to_repo = project.repository.path_to_repo
-        if File.exists?(path_to_repo)
+        if File.exist?(path_to_repo)
           print '-'
         else
           if Gitlab::Shell.new.add_repository(project.repository_storage_path,
diff --git a/lib/tasks/spinach.rake b/lib/tasks/spinach.rake
index c0f860a82d2aac..8dbfa7751dcf25 100644
--- a/lib/tasks/spinach.rake
+++ b/lib/tasks/spinach.rake
@@ -46,7 +46,7 @@ def run_spinach_tests(tags)
   success = run_spinach_command(%W(--tags #{tags}))
   3.times do |_|
     break if success
-    break unless File.exists?('tmp/spinach-rerun.txt')
+    break unless File.exist?('tmp/spinach-rerun.txt')
 
     tests = File.foreach('tmp/spinach-rerun.txt').map(&:chomp)
     puts ''
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index baf78208ec5415..548e7780c362f5 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -42,7 +42,7 @@ def reenable_backup_sub_tasks
       before do
         allow(Dir).to receive(:glob).and_return([])
         allow(Dir).to receive(:chdir)
-        allow(File).to receive(:exists?).and_return(true)
+        allow(File).to receive(:exist?).and_return(true)
         allow(Kernel).to receive(:system).and_return(true)
         allow(FileUtils).to receive(:cp_r).and_return(true)
         allow(FileUtils).to receive(:mv).and_return(true)
-- 
GitLab


From 7d39bc879d6dc4c0111b3c4ecb2d879034d8d36f Mon Sep 17 00:00:00 2001
From: Yorick Peterse 
Date: Wed, 10 Aug 2016 17:26:52 +0200
Subject: [PATCH 125/153] Remove various redundant indexes

One can see which indexes are used in PostgreSQL by running the
following query:

    SELECT relname as table_name, indexrelname as index_name, idx_scan, idx_tup_read, idx_tup_fetch, pg_size_pretty(pg_relation_size(indexrelname::regclass))
    FROM pg_stat_all_indexes
    WHERE schemaname = 'public'
    AND "idx_scan" = 0
    ORDER BY pg_relation_size(indexrelname::regclass) desc;

Using this query I built a list of indexes that could be potentially
removed. After checking every single one by hand to make sure they
really aren't used I only found 1 index that _would_ be used. This was a
GitLab GEO index (EE) specific that's currently not used simply because
the table is empty.

Apart from this one index all indexes could be removed. The migration
also takes care of 6 composite indexes that can be replaced with a
single column index, which in most cases was already present.

For more information see gitlab-org/gitlab-ce#20767.
---
 CHANGELOG                                     |   1 +
 ...20160810142633_remove_redundant_indexes.rb | 112 ++++++++++++++++++
 db/schema.rb                                  |  56 +--------
 3 files changed, 117 insertions(+), 52 deletions(-)
 create mode 100644 db/migrate/20160810142633_remove_redundant_indexes.rb

diff --git a/CHANGELOG b/CHANGELOG
index 3b567d9ec2c4ed..848080e5ed7a99 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -26,6 +26,7 @@ v 8.11.0 (unreleased)
   - Add experimental Redis Sentinel support !1877
   - Fix branches page dropdown sort initial state (ClemMakesApps)
   - Environments have an url to link to
+  - Various redundant database indexes have been removed
   - Update `timeago` plugin to use multiple string/locale settings
   - Remove unused images (ClemMakesApps)
   - Limit git rev-list output count to one in forced push check
diff --git a/db/migrate/20160810142633_remove_redundant_indexes.rb b/db/migrate/20160810142633_remove_redundant_indexes.rb
new file mode 100644
index 00000000000000..8641c6ffa8f9fd
--- /dev/null
+++ b/db/migrate/20160810142633_remove_redundant_indexes.rb
@@ -0,0 +1,112 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveRedundantIndexes < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  disable_ddl_transaction!
+
+  def up
+    indexes = [
+      [:ci_taggings, 'ci_taggings_idx'],
+      [:audit_events, 'index_audit_events_on_author_id'],
+      [:audit_events, 'index_audit_events_on_type'],
+      [:ci_builds, 'index_ci_builds_on_erased_by_id'],
+      [:ci_builds, 'index_ci_builds_on_project_id_and_commit_id'],
+      [:ci_builds, 'index_ci_builds_on_type'],
+      [:ci_commits, 'index_ci_commits_on_project_id'],
+      [:ci_commits, 'index_ci_commits_on_project_id_and_committed_at'],
+      [:ci_commits, 'index_ci_commits_on_project_id_and_committed_at_and_id'],
+      [:ci_commits, 'index_ci_commits_on_project_id_and_sha'],
+      [:ci_commits, 'index_ci_commits_on_sha'],
+      [:ci_events, 'index_ci_events_on_created_at'],
+      [:ci_events, 'index_ci_events_on_is_admin'],
+      [:ci_events, 'index_ci_events_on_project_id'],
+      [:ci_jobs, 'index_ci_jobs_on_deleted_at'],
+      [:ci_jobs, 'index_ci_jobs_on_project_id'],
+      [:ci_projects, 'index_ci_projects_on_gitlab_id'],
+      [:ci_projects, 'index_ci_projects_on_shared_runners_enabled'],
+      [:ci_services, 'index_ci_services_on_project_id'],
+      [:ci_sessions, 'index_ci_sessions_on_session_id'],
+      [:ci_sessions, 'index_ci_sessions_on_updated_at'],
+      [:ci_tags, 'index_ci_tags_on_name'],
+      [:ci_triggers, 'index_ci_triggers_on_deleted_at'],
+      [:identities, 'index_identities_on_created_at_and_id'],
+      [:issues, 'index_issues_on_title'],
+      [:keys, 'index_keys_on_created_at_and_id'],
+      [:members, 'index_members_on_created_at_and_id'],
+      [:members, 'index_members_on_type'],
+      [:milestones, 'index_milestones_on_created_at_and_id'],
+      [:namespaces, 'index_namespaces_on_visibility_level'],
+      [:projects, 'index_projects_on_builds_enabled_and_shared_runners_enabled'],
+      [:services, 'index_services_on_category'],
+      [:services, 'index_services_on_created_at_and_id'],
+      [:services, 'index_services_on_default'],
+      [:snippets, 'index_snippets_on_created_at'],
+      [:snippets, 'index_snippets_on_created_at_and_id'],
+      [:todos, 'index_todos_on_state'],
+      [:web_hooks, 'index_web_hooks_on_created_at_and_id'],
+
+      # These indexes _may_ be used but they can be replaced by other existing
+      # indexes.
+
+      # There's already a composite index on (project_id, iid) which means that
+      # a separate index for _just_ project_id is not needed.
+      [:issues, 'index_issues_on_project_id'],
+
+      # These are all composite indexes for the columns (created_at, id). In all
+      # these cases there's already a standalone index for "created_at" which
+      # can be used instead.
+      #
+      # Because the "id" column of these composite indexes is never needed (due
+      # to "id" already being indexed as its a primary key) these composite
+      # indexes are useless.
+      [:issues, 'index_issues_on_created_at_and_id'],
+      [:merge_requests, 'index_merge_requests_on_created_at_and_id'],
+      [:namespaces, 'index_namespaces_on_created_at_and_id'],
+      [:notes, 'index_notes_on_created_at_and_id'],
+      [:projects, 'index_projects_on_created_at_and_id'],
+      [:users, 'index_users_on_created_at_and_id'],
+    ]
+
+    transaction do
+      indexes.each do |(table, index)|
+        remove_index(table, name: index) if index_exists_by_name?(table, index)
+      end
+    end
+
+    add_concurrent_index(:users, :created_at)
+    add_concurrent_index(:projects, :created_at)
+    add_concurrent_index(:namespaces, :created_at)
+  end
+
+  def down
+    # We're only restoring the composite indexes that could be replaced with
+    # individual ones, just in case somebody would ever want to revert.
+    transaction do
+      remove_index(:users, :created_at)
+      remove_index(:projects, :created_at)
+      remove_index(:namespaces, :created_at)
+    end
+
+    [:issues, :merge_requests, :namespaces, :notes, :projects, :users].each do |table|
+      add_concurrent_index(table, [:created_at, :id],
+                           name: "index_#{table}_on_created_at_and_id")
+    end
+  end
+
+  # Rails' index_exists? doesn't work when you only give it a table and index
+  # name. As such we have to use some extra code to check if an index exists for
+  # a given name.
+  def index_exists_by_name?(table, index)
+    indexes_for_table[table].include?(index)
+  end
+
+  def indexes_for_table
+    @indexes_for_table ||= Hash.new do |hash, table_name|
+      hash[table_name] = indexes(table_name).map(&:name)
+    end
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index b795eeaa23cb41..059c923bb9348c 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 20160810102349) do
+ActiveRecord::Schema.define(version: 20160810142633) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
@@ -102,9 +102,7 @@
     t.datetime "updated_at"
   end
 
-  add_index "audit_events", ["author_id"], name: "index_audit_events_on_author_id", using: :btree
   add_index "audit_events", ["entity_id", "entity_type"], name: "index_audit_events_on_entity_id_and_entity_type", using: :btree
-  add_index "audit_events", ["type"], name: "index_audit_events_on_type", using: :btree
 
   create_table "award_emoji", force: :cascade do |t|
     t.string   "name"
@@ -179,13 +177,10 @@
   add_index "ci_builds", ["commit_id", "type", "name", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_name_and_ref", using: :btree
   add_index "ci_builds", ["commit_id", "type", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_ref", using: :btree
   add_index "ci_builds", ["commit_id"], name: "index_ci_builds_on_commit_id", using: :btree
-  add_index "ci_builds", ["erased_by_id"], name: "index_ci_builds_on_erased_by_id", using: :btree
   add_index "ci_builds", ["gl_project_id"], name: "index_ci_builds_on_gl_project_id", using: :btree
-  add_index "ci_builds", ["project_id", "commit_id"], name: "index_ci_builds_on_project_id_and_commit_id", using: :btree
   add_index "ci_builds", ["project_id"], name: "index_ci_builds_on_project_id", using: :btree
   add_index "ci_builds", ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree
   add_index "ci_builds", ["status"], name: "index_ci_builds_on_status", using: :btree
-  add_index "ci_builds", ["type"], name: "index_ci_builds_on_type", using: :btree
 
   create_table "ci_commits", force: :cascade do |t|
     t.integer  "project_id"
@@ -209,11 +204,6 @@
   add_index "ci_commits", ["gl_project_id", "sha"], name: "index_ci_commits_on_gl_project_id_and_sha", using: :btree
   add_index "ci_commits", ["gl_project_id", "status"], name: "index_ci_commits_on_gl_project_id_and_status", using: :btree
   add_index "ci_commits", ["gl_project_id"], name: "index_ci_commits_on_gl_project_id", using: :btree
-  add_index "ci_commits", ["project_id", "committed_at", "id"], name: "index_ci_commits_on_project_id_and_committed_at_and_id", using: :btree
-  add_index "ci_commits", ["project_id", "committed_at"], name: "index_ci_commits_on_project_id_and_committed_at", using: :btree
-  add_index "ci_commits", ["project_id", "sha"], name: "index_ci_commits_on_project_id_and_sha", using: :btree
-  add_index "ci_commits", ["project_id"], name: "index_ci_commits_on_project_id", using: :btree
-  add_index "ci_commits", ["sha"], name: "index_ci_commits_on_sha", using: :btree
   add_index "ci_commits", ["status"], name: "index_ci_commits_on_status", using: :btree
   add_index "ci_commits", ["user_id"], name: "index_ci_commits_on_user_id", using: :btree
 
@@ -226,10 +216,6 @@
     t.datetime "updated_at"
   end
 
-  add_index "ci_events", ["created_at"], name: "index_ci_events_on_created_at", using: :btree
-  add_index "ci_events", ["is_admin"], name: "index_ci_events_on_is_admin", using: :btree
-  add_index "ci_events", ["project_id"], name: "index_ci_events_on_project_id", using: :btree
-
   create_table "ci_jobs", force: :cascade do |t|
     t.integer  "project_id",                          null: false
     t.text     "commands"
@@ -244,9 +230,6 @@
     t.datetime "deleted_at"
   end
 
-  add_index "ci_jobs", ["deleted_at"], name: "index_ci_jobs_on_deleted_at", using: :btree
-  add_index "ci_jobs", ["project_id"], name: "index_ci_jobs_on_project_id", using: :btree
-
   create_table "ci_projects", force: :cascade do |t|
     t.string   "name"
     t.integer  "timeout",                  default: 3600,  null: false
@@ -270,9 +253,6 @@
     t.text     "generated_yaml_config"
   end
 
-  add_index "ci_projects", ["gitlab_id"], name: "index_ci_projects_on_gitlab_id", using: :btree
-  add_index "ci_projects", ["shared_runners_enabled"], name: "index_ci_projects_on_shared_runners_enabled", using: :btree
-
   create_table "ci_runner_projects", force: :cascade do |t|
     t.integer  "runner_id",     null: false
     t.integer  "project_id"
@@ -314,8 +294,6 @@
     t.text     "properties"
   end
 
-  add_index "ci_services", ["project_id"], name: "index_ci_services_on_project_id", using: :btree
-
   create_table "ci_sessions", force: :cascade do |t|
     t.string   "session_id", null: false
     t.text     "data"
@@ -323,9 +301,6 @@
     t.datetime "updated_at"
   end
 
-  add_index "ci_sessions", ["session_id"], name: "index_ci_sessions_on_session_id", using: :btree
-  add_index "ci_sessions", ["updated_at"], name: "index_ci_sessions_on_updated_at", using: :btree
-
   create_table "ci_taggings", force: :cascade do |t|
     t.integer  "tag_id"
     t.integer  "taggable_id"
@@ -336,7 +311,6 @@
     t.datetime "created_at"
   end
 
-  add_index "ci_taggings", ["tag_id", "taggable_id", "taggable_type", "context", "tagger_id", "tagger_type"], name: "ci_taggings_idx", unique: true, using: :btree
   add_index "ci_taggings", ["taggable_id", "taggable_type", "context"], name: "index_ci_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree
 
   create_table "ci_tags", force: :cascade do |t|
@@ -344,8 +318,6 @@
     t.integer "taggings_count", default: 0
   end
 
-  add_index "ci_tags", ["name"], name: "index_ci_tags_on_name", unique: true, using: :btree
-
   create_table "ci_trigger_requests", force: :cascade do |t|
     t.integer  "trigger_id", null: false
     t.text     "variables"
@@ -363,7 +335,6 @@
     t.integer  "gl_project_id"
   end
 
-  add_index "ci_triggers", ["deleted_at"], name: "index_ci_triggers_on_deleted_at", using: :btree
   add_index "ci_triggers", ["gl_project_id"], name: "index_ci_triggers_on_gl_project_id", using: :btree
 
   create_table "ci_variables", force: :cascade do |t|
@@ -469,7 +440,6 @@
     t.datetime "updated_at"
   end
 
-  add_index "identities", ["created_at", "id"], name: "index_identities_on_created_at_and_id", using: :btree
   add_index "identities", ["user_id"], name: "index_identities_on_user_id", using: :btree
 
   create_table "issues", force: :cascade do |t|
@@ -495,16 +465,13 @@
   add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
   add_index "issues", ["author_id"], name: "index_issues_on_author_id", using: :btree
   add_index "issues", ["confidential"], name: "index_issues_on_confidential", using: :btree
-  add_index "issues", ["created_at", "id"], name: "index_issues_on_created_at_and_id", using: :btree
   add_index "issues", ["created_at"], name: "index_issues_on_created_at", using: :btree
   add_index "issues", ["deleted_at"], name: "index_issues_on_deleted_at", using: :btree
   add_index "issues", ["description"], name: "index_issues_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
   add_index "issues", ["due_date"], name: "index_issues_on_due_date", using: :btree
   add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree
   add_index "issues", ["project_id", "iid"], name: "index_issues_on_project_id_and_iid", unique: true, using: :btree
-  add_index "issues", ["project_id"], name: "index_issues_on_project_id", using: :btree
   add_index "issues", ["state"], name: "index_issues_on_state", using: :btree
-  add_index "issues", ["title"], name: "index_issues_on_title", using: :btree
   add_index "issues", ["title"], name: "index_issues_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
 
   create_table "keys", force: :cascade do |t|
@@ -518,7 +485,6 @@
     t.boolean  "public",      default: false, null: false
   end
 
-  add_index "keys", ["created_at", "id"], name: "index_keys_on_created_at_and_id", using: :btree
   add_index "keys", ["fingerprint"], name: "index_keys_on_fingerprint", unique: true, using: :btree
   add_index "keys", ["user_id"], name: "index_keys_on_user_id", using: :btree
 
@@ -583,11 +549,9 @@
   end
 
   add_index "members", ["access_level"], name: "index_members_on_access_level", using: :btree
-  add_index "members", ["created_at", "id"], name: "index_members_on_created_at_and_id", using: :btree
   add_index "members", ["invite_token"], name: "index_members_on_invite_token", unique: true, using: :btree
   add_index "members", ["requested_at"], name: "index_members_on_requested_at", using: :btree
   add_index "members", ["source_id", "source_type"], name: "index_members_on_source_id_and_source_type", using: :btree
-  add_index "members", ["type"], name: "index_members_on_type", using: :btree
   add_index "members", ["user_id"], name: "index_members_on_user_id", using: :btree
 
   create_table "merge_request_diffs", force: :cascade do |t|
@@ -634,7 +598,6 @@
 
   add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
   add_index "merge_requests", ["author_id"], name: "index_merge_requests_on_author_id", using: :btree
-  add_index "merge_requests", ["created_at", "id"], name: "index_merge_requests_on_created_at_and_id", using: :btree
   add_index "merge_requests", ["created_at"], name: "index_merge_requests_on_created_at", using: :btree
   add_index "merge_requests", ["deleted_at"], name: "index_merge_requests_on_deleted_at", using: :btree
   add_index "merge_requests", ["description"], name: "index_merge_requests_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
@@ -657,7 +620,6 @@
     t.integer  "iid"
   end
 
-  add_index "milestones", ["created_at", "id"], name: "index_milestones_on_created_at_and_id", using: :btree
   add_index "milestones", ["description"], name: "index_milestones_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
   add_index "milestones", ["due_date"], name: "index_milestones_on_due_date", using: :btree
   add_index "milestones", ["project_id", "iid"], name: "index_milestones_on_project_id_and_iid", unique: true, using: :btree
@@ -679,14 +641,13 @@
     t.boolean  "request_access_enabled", default: true,  null: false
   end
 
-  add_index "namespaces", ["created_at", "id"], name: "index_namespaces_on_created_at_and_id", using: :btree
+  add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree
   add_index "namespaces", ["name"], name: "index_namespaces_on_name", unique: true, using: :btree
   add_index "namespaces", ["name"], name: "index_namespaces_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"}
   add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree
   add_index "namespaces", ["path"], name: "index_namespaces_on_path", unique: true, using: :btree
   add_index "namespaces", ["path"], name: "index_namespaces_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"}
   add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree
-  add_index "namespaces", ["visibility_level"], name: "index_namespaces_on_visibility_level", using: :btree
 
   create_table "notes", force: :cascade do |t|
     t.text     "note"
@@ -709,7 +670,6 @@
 
   add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree
   add_index "notes", ["commit_id"], name: "index_notes_on_commit_id", using: :btree
-  add_index "notes", ["created_at", "id"], name: "index_notes_on_created_at_and_id", using: :btree
   add_index "notes", ["created_at"], name: "index_notes_on_created_at", using: :btree
   add_index "notes", ["line_code"], name: "index_notes_on_line_code", using: :btree
   add_index "notes", ["note"], name: "index_notes_on_note_trigram", using: :gin, opclasses: {"note"=>"gin_trgm_ops"}
@@ -849,9 +809,8 @@
     t.boolean  "request_access_enabled",             default: true,      null: false
   end
 
-  add_index "projects", ["builds_enabled", "shared_runners_enabled"], name: "index_projects_on_builds_enabled_and_shared_runners_enabled", using: :btree
   add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
-  add_index "projects", ["created_at", "id"], name: "index_projects_on_created_at_and_id", using: :btree
+  add_index "projects", ["created_at"], name: "index_projects_on_created_at", using: :btree
   add_index "projects", ["creator_id"], name: "index_projects_on_creator_id", using: :btree
   add_index "projects", ["description"], name: "index_projects_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
   add_index "projects", ["last_activity_at"], name: "index_projects_on_last_activity_at", using: :btree
@@ -937,9 +896,6 @@
     t.boolean  "wiki_page_events",      default: true
   end
 
-  add_index "services", ["category"], name: "index_services_on_category", using: :btree
-  add_index "services", ["created_at", "id"], name: "index_services_on_created_at_and_id", using: :btree
-  add_index "services", ["default"], name: "index_services_on_default", using: :btree
   add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree
   add_index "services", ["template"], name: "index_services_on_template", using: :btree
 
@@ -956,8 +912,6 @@
   end
 
   add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree
-  add_index "snippets", ["created_at", "id"], name: "index_snippets_on_created_at_and_id", using: :btree
-  add_index "snippets", ["created_at"], name: "index_snippets_on_created_at", using: :btree
   add_index "snippets", ["file_name"], name: "index_snippets_on_file_name_trigram", using: :gin, opclasses: {"file_name"=>"gin_trgm_ops"}
   add_index "snippets", ["project_id"], name: "index_snippets_on_project_id", using: :btree
   add_index "snippets", ["title"], name: "index_snippets_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
@@ -1026,7 +980,6 @@
   add_index "todos", ["commit_id"], name: "index_todos_on_commit_id", using: :btree
   add_index "todos", ["note_id"], name: "index_todos_on_note_id", using: :btree
   add_index "todos", ["project_id"], name: "index_todos_on_project_id", using: :btree
-  add_index "todos", ["state"], name: "index_todos_on_state", using: :btree
   add_index "todos", ["target_type", "target_id"], name: "index_todos_on_target_type_and_target_id", using: :btree
   add_index "todos", ["user_id"], name: "index_todos_on_user_id", using: :btree
 
@@ -1106,7 +1059,7 @@
   add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
   add_index "users", ["authentication_token"], name: "index_users_on_authentication_token", unique: true, using: :btree
   add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree
-  add_index "users", ["created_at", "id"], name: "index_users_on_created_at_and_id", using: :btree
+  add_index "users", ["created_at"], name: "index_users_on_created_at", using: :btree
   add_index "users", ["current_sign_in_at"], name: "index_users_on_current_sign_in_at", using: :btree
   add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree
   add_index "users", ["email"], name: "index_users_on_email_trigram", using: :gin, opclasses: {"email"=>"gin_trgm_ops"}
@@ -1146,7 +1099,6 @@
     t.string   "token"
   end
 
-  add_index "web_hooks", ["created_at", "id"], name: "index_web_hooks_on_created_at_and_id", using: :btree
   add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree
 
   add_foreign_key "personal_access_tokens", "users"
-- 
GitLab


From 68cea38e94886e69099a3faa0d1e2fbde2e71899 Mon Sep 17 00:00:00 2001
From: Elliot 
Date: Thu, 11 Aug 2016 13:15:46 +0200
Subject: [PATCH 126/153] Fix front-end for branches that happen to contain
 urlencoding escape characters (e.g. %)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Rémy Coutable 
---
 CHANGELOG                      |  1 +
 lib/extracts_path.rb           |  2 +-
 spec/lib/extracts_path_spec.rb | 19 ++++++++++++++++---
 3 files changed, 18 insertions(+), 4 deletions(-)

diff --git a/CHANGELOG b/CHANGELOG
index 3b567d9ec2c4ed..8fb2194f6127b5 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -5,6 +5,7 @@ v 8.11.0 (unreleased)
   - Fix rename `add_users_into_project` and `projects_ids`. !20512 (herminiotorres)
   - Fix the title of the toggle dropdown button. !5515 (herminiotorres)
   - Improve diff performance by eliminating redundant checks for text blobs
+  - Ensure that branch names containing escapable characters (e.g. %20) aren't unescaped indiscriminately. !5770 (ewiltshi)
   - Convert switch icon into icon font (ClemMakesApps)
   - API: Endpoints for enabling and disabling deploy keys
   - Use long options for curl examples in documentation !5703 (winniehell)
diff --git a/lib/extracts_path.rb b/lib/extracts_path.rb
index 51e46da82ccd24..84688f6646eb84 100644
--- a/lib/extracts_path.rb
+++ b/lib/extracts_path.rb
@@ -94,7 +94,7 @@ def assign_ref_vars
     @options = params.select {|key, value| allowed_options.include?(key) && !value.blank? }
     @options = HashWithIndifferentAccess.new(@options)
 
-    @id = Addressable::URI.unescape(get_id)
+    @id = Addressable::URI.normalize_component(get_id)
     @ref, @path = extract_ref(@id)
     @repo = @project.repository
     if @options[:extended_sha1].blank?
diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb
index b12a7b98d4d6bb..36c77206a3f935 100644
--- a/spec/lib/extracts_path_spec.rb
+++ b/spec/lib/extracts_path_spec.rb
@@ -30,15 +30,28 @@
       expect(@logs_path).to eq("/#{@project.path_with_namespace}/refs/#{ref}/logs_tree/files/ruby/popen.rb")
     end
 
-    context 'escaped sequences in ref' do
-      let(:ref) { "improve%2Fawesome" }
+    context 'escaped slash character in ref' do
+      let(:ref) { 'improve%2Fawesome' }
 
-      it "id has no escape sequences" do
+      it 'has no escape sequences in @ref or @logs_path' do
         assign_ref_vars
+
         expect(@ref).to eq('improve/awesome')
         expect(@logs_path).to eq("/#{@project.path_with_namespace}/refs/#{ref}/logs_tree/files/ruby/popen.rb")
       end
     end
+
+    context 'ref contains %20' do
+      let(:ref) { 'foo%20bar' }
+
+      it 'is not converted to a space in @id' do
+        @project.repository.add_branch(@project.owner, 'foo%20bar', 'master')
+
+        assign_ref_vars
+
+        expect(@id).to start_with('foo%20bar/')
+      end
+    end
   end
 
   describe '#extract_ref' do
-- 
GitLab


From 7d65d2ec2b808a796efdada83cd0ec3613ca693b Mon Sep 17 00:00:00 2001
From: Yorick Peterse 
Date: Thu, 11 Aug 2016 14:31:19 +0200
Subject: [PATCH 127/153] Corrected links/usernames in performance guide

This fixes two broken links in the performance guide and removes the
mention of Josh as he no longer works for GitLab.

[ci skip]
---
 doc/development/performance.md | 9 ++++-----
 1 file changed, 4 insertions(+), 5 deletions(-)

diff --git a/doc/development/performance.md b/doc/development/performance.md
index fb37b3a889c732..7ff603e2c4a91a 100644
--- a/doc/development/performance.md
+++ b/doc/development/performance.md
@@ -15,8 +15,8 @@ The process of solving performance problems is roughly as follows:
 3. Add your findings based on the measurement period (screenshots of graphs,
    timings, etc) to the issue mentioned in step 1.
 4. Solve the problem.
-5. Create a merge request, assign the "performance" label and ping the right
-   people (e.g. [@yorickpeterse][yorickpeterse] and [@joshfng][joshfng]).
+5. Create a merge request, assign the "Performance" label and assign it to
+   [@yorickpeterse][yorickpeterse] for reviewing.
 6. Once a change has been deployed make sure to _again_ measure for at least 24
    hours to see if your changes have any impact on the production environment.
 7. Repeat until you're done.
@@ -36,8 +36,8 @@ graphs/dashboards.
 
 GitLab provides two built-in tools to aid the process of improving performance:
 
-* [Sherlock](doc/development/profiling.md#sherlock)
-* [GitLab Performance Monitoring](doc/monitoring/performance/monitoring.md)
+* [Sherlock](profiling.md#sherlock)
+* [GitLab Performance Monitoring](../monitoring/performance/monitoring.md)
 
 GitLab employees can use GitLab.com's performance monitoring systems located at
 , this requires you to log in using your
@@ -254,5 +254,4 @@ referencing an object directly may even slow code down.
 
 [#15607]: https://gitlab.com/gitlab-org/gitlab-ce/issues/15607
 [yorickpeterse]: https://gitlab.com/u/yorickpeterse
-[joshfng]: https://gitlab.com/u/joshfng
 [anti-pattern]: https://en.wikipedia.org/wiki/Anti-pattern
-- 
GitLab


From 6e6ad3e2fc386044134cbb33395cceb97e913fa0 Mon Sep 17 00:00:00 2001
From: Jacob Vosmaer 
Date: Thu, 11 Aug 2016 15:05:49 +0200
Subject: [PATCH 128/153] Fix typo

---
 spec/requests/lfs_http_spec.rb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index 9db282a9f3d7c9..4c9b4a8ba422ef 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -636,7 +636,7 @@
             it 'lfs object is linked to the project' do
               expect(lfs_object.projects.pluck(:id)).to include(project.id)
             end
-          en
+          end
 
           context 'invalid tempfiles' do
             it 'rejects slashes in the tempfile name (path traversal' do
-- 
GitLab


From 39203f1adfc6fee3eca50f0cab99ffc597865200 Mon Sep 17 00:00:00 2001
From: Kamil Trzcinski 
Date: Thu, 11 Aug 2016 15:22:35 +0200
Subject: [PATCH 129/153] Pre-create all builds for Pipeline when a trigger is
 received

This change simplifies a Pipeline processing by introducing a special new status: created.
This status is used for all builds that are created for a pipeline.
We are then processing next stages and queueing some of the builds (created -> pending) or skipping them (created -> skipped).
This makes it possible to simplify and solve a few ordering problems with how previously builds were scheduled.
This also allows us to visualise a full pipeline (with created builds).

This also removes an after_touch used for updating a pipeline state parameters.
Right now in various places we explicitly call a reload_status! on pipeline to force it to be updated and saved.
---
 CHANGELOG                                     |   1 +
 app/controllers/projects/builds_controller.rb |   2 +-
 app/controllers/projects/commit_controller.rb |   4 +-
 .../projects/merge_requests_controller.rb     |   4 +-
 .../projects/pipelines_controller.rb          |   2 +-
 app/models/ci/build.rb                        |  12 +-
 app/models/ci/pipeline.rb                     |  84 ++---
 app/models/commit_status.rb                   |  31 +-
 app/models/concerns/statuseable.rb            |  31 +-
 app/services/ci/create_builds_service.rb      |  62 ----
 .../ci/create_pipeline_builds_service.rb      |  42 +++
 app/services/ci/create_pipeline_service.rb    |  95 ++++--
 .../ci/create_trigger_request_service.rb      |  17 +-
 app/services/ci/process_pipeline_service.rb   |  77 +++++
 app/services/create_commit_builds_service.rb  |  69 ----
 app/services/git_push_service.rb              |   2 +-
 app/services/git_tag_push_service.rb          |   2 +-
 .../projects/ci/pipelines/_pipeline.html.haml |   2 +-
 app/views/projects/commit/_pipeline.html.haml |   4 +-
 ...160716115711_add_queued_at_to_ci_builds.rb |   9 +
 db/schema.rb                                  |   1 +
 features/steps/shared/builds.rb               |   8 +-
 lib/ci/gitlab_ci_yaml_processor.rb            |   2 +-
 spec/factories/ci/builds.rb                   |   5 +
 spec/factories/ci/pipelines.rb                |   2 +
 spec/factories/commit_statuses.rb             |  24 ++
 .../merge_requests/created_from_fork_spec.rb  |  12 +-
 spec/features/pipelines_spec.rb               |  24 +-
 spec/lib/ci/charts_spec.rb                    |   1 +
 spec/lib/ci/gitlab_ci_yaml_processor_spec.rb  |  14 +-
 spec/lib/gitlab/badge/build_spec.rb           |   7 +-
 spec/models/build_spec.rb                     |  47 +++
 spec/models/ci/pipeline_spec.rb               | 316 +-----------------
 .../project_services/hipchat_service_spec.rb  |   3 +-
 spec/requests/api/builds_spec.rb              |   4 +
 spec/requests/api/commits_spec.rb             |  13 +
 spec/requests/api/triggers_spec.rb            |   3 +-
 spec/requests/ci/api/builds_spec.rb           | 163 +++++----
 spec/requests/ci/api/triggers_spec.rb         |   3 +-
 .../services/ci/create_builds_service_spec.rb |  32 --
 .../ci/create_pipeline_service_spec.rb        | 214 ++++++++++++
 .../ci/create_trigger_request_service_spec.rb |   5 +-
 .../ci/image_for_build_service_spec.rb        |   6 +-
 .../ci/process_pipeline_service_spec.rb       | 288 ++++++++++++++++
 .../create_commit_builds_service_spec.rb      | 241 -------------
 .../merge_when_build_succeeds_service_spec.rb |  10 +-
 spec/workers/post_receive_spec.rb             |   8 +-
 47 files changed, 1042 insertions(+), 966 deletions(-)
 delete mode 100644 app/services/ci/create_builds_service.rb
 create mode 100644 app/services/ci/create_pipeline_builds_service.rb
 create mode 100644 app/services/ci/process_pipeline_service.rb
 delete mode 100644 app/services/create_commit_builds_service.rb
 create mode 100644 db/migrate/20160716115711_add_queued_at_to_ci_builds.rb
 delete mode 100644 spec/services/ci/create_builds_service_spec.rb
 create mode 100644 spec/services/ci/create_pipeline_service_spec.rb
 create mode 100644 spec/services/ci/process_pipeline_service_spec.rb
 delete mode 100644 spec/services/create_commit_builds_service_spec.rb

diff --git a/CHANGELOG b/CHANGELOG
index a978ae90dac7af..67d2fa4e30daa5 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -17,6 +17,7 @@ v 8.11.0 (unreleased)
   - Cache the commit author in RequestStore to avoid extra lookups in PostReceive
   - Expand commit message width in repo view (ClemMakesApps)
   - Cache highlighted diff lines for merge requests
+  - Pre-create all builds for a Pipeline when the new Pipeline is created !5295
   - Fix of 'Commits being passed to custom hooks are already reachable when using the UI'
   - Fix awardable button mutuality loading spinners (ClemMakesApps)
   - Add support for using RequestStore within Sidekiq tasks via SIDEKIQ_REQUEST_STORE env variable
diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb
index 553b62741a5de0..12195c3cbb8270 100644
--- a/app/controllers/projects/builds_controller.rb
+++ b/app/controllers/projects/builds_controller.rb
@@ -6,7 +6,7 @@ class Projects::BuildsController < Projects::ApplicationController
 
   def index
     @scope = params[:scope]
-    @all_builds = project.builds
+    @all_builds = project.builds.relevant
     @builds = @all_builds.order('created_at DESC')
     @builds =
       case @scope
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index fdfe7c65b7b0bc..f44e9bb3fd7d1c 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -134,8 +134,8 @@ def define_note_vars
   end
 
   def define_status_vars
-    @statuses = CommitStatus.where(pipeline: pipelines)
-    @builds = Ci::Build.where(pipeline: pipelines)
+    @statuses = CommitStatus.where(pipeline: pipelines).relevant
+    @builds = Ci::Build.where(pipeline: pipelines).relevant
   end
 
   def assign_change_commit_vars(mr_source_branch)
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 2cf6a2dd1b3967..139680d2df9e08 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -160,7 +160,7 @@ def new
     @diff_notes_disabled = true
 
     @pipeline = @merge_request.pipeline
-    @statuses = @pipeline.statuses if @pipeline
+    @statuses = @pipeline.statuses.relevant if @pipeline
 
     @note_counts = Note.where(commit_id: @commits.map(&:id)).
       group(:commit_id).count
@@ -362,7 +362,7 @@ def define_show_vars
     @commits_count = @merge_request.commits.count
 
     @pipeline = @merge_request.pipeline
-    @statuses = @pipeline.statuses if @pipeline
+    @statuses = @pipeline.statuses.relevant if @pipeline
 
     if @merge_request.locked_long_ago?
       @merge_request.unlock_mr
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 487963fdcd7b71..b0c72cfe4b4fd6 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -19,7 +19,7 @@ def new
   end
 
   def create
-    @pipeline = Ci::CreatePipelineService.new(project, current_user, create_params).execute
+    @pipeline = Ci::CreatePipelineService.new(project, current_user, create_params).execute(ignore_skip_ci: true, save_on_errors: false)
     unless @pipeline.persisted?
       render 'new'
       return
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 08f396210c9361..88a340379b890e 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -16,7 +16,7 @@ class Build < CommitStatus
     scope :with_artifacts_not_expired, ->() { with_artifacts.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) }
     scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) }
     scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) }
-    scope :manual_actions, ->() { where(when: :manual) }
+    scope :manual_actions, ->() { where(when: :manual).relevant }
 
     mount_uploader :artifacts_file, ArtifactUploader
     mount_uploader :artifacts_metadata, ArtifactUploader
@@ -65,17 +65,11 @@ def retry(build, user = nil)
       end
     end
 
-    state_machine :status, initial: :pending do
+    state_machine :status do
       after_transition pending: :running do |build|
         build.execute_hooks
       end
 
-      # We use around_transition to create builds for next stage as soon as possible, before the `after_*` is executed
-      around_transition any => [:success, :failed, :canceled] do |build, block|
-        block.call
-        build.pipeline.create_next_builds(build) if build.pipeline
-      end
-
       after_transition any => [:success, :failed, :canceled] do |build|
         build.update_coverage
         build.execute_hooks
@@ -461,7 +455,7 @@ def predefined_variables
 
     def build_attributes_from_config
       return {} unless pipeline.config_processor
-      
+
       pipeline.config_processor.build_attributes(name)
     end
   end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index bce6a992af6220..718fe3290c1a17 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -13,11 +13,10 @@ class Pipeline < ActiveRecord::Base
     has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest', foreign_key: :commit_id
 
     validates_presence_of :sha
+    validates_presence_of :ref
     validates_presence_of :status
     validate :valid_commit_sha
 
-    # Invalidate object and save if when touched
-    after_touch :update_state
     after_save :keep_around_commits
 
     # ref can't be HEAD or SHA, can only be branch/tag name
@@ -90,12 +89,16 @@ def cancelable?
 
     def cancel_running
       builds.running_or_pending.each(&:cancel)
+
+      reload_status!
     end
 
     def retry_failed(user)
       builds.latest.failed.select(&:retryable?).each do |build|
         Ci::Build.retry(build, user)
       end
+
+      reload_status!
     end
 
     def latest?
@@ -109,37 +112,6 @@ def triggered?
       trigger_requests.any?
     end
 
-    def create_builds(user, trigger_request = nil)
-      ##
-      # We persist pipeline only if there are builds available
-      #
-      return unless config_processor
-
-      build_builds_for_stages(config_processor.stages, user,
-                              'success', trigger_request) && save
-    end
-
-    def create_next_builds(build)
-      return unless config_processor
-
-      # don't create other builds if this one is retried
-      latest_builds = builds.latest
-      return unless latest_builds.exists?(build.id)
-
-      # get list of stages after this build
-      next_stages = config_processor.stages.drop_while { |stage| stage != build.stage }
-      next_stages.delete(build.stage)
-
-      # get status for all prior builds
-      prior_builds = latest_builds.where.not(stage: next_stages)
-      prior_status = prior_builds.status
-
-      # build builds for next stage that has builds available
-      # and save pipeline if we have builds
-      build_builds_for_stages(next_stages, build.user, prior_status,
-                              build.trigger_request) && save
-    end
-
     def retried
       @retried ||= (statuses.order(id: :desc) - statuses.latest)
     end
@@ -151,6 +123,14 @@ def coverage
       end
     end
 
+    def config_builds_attributes
+      return [] unless config_processor
+
+      config_processor.
+        builds_for_ref(ref, tag?, trigger_requests.first).
+        sort_by { |build| build[:stage_idx] }
+    end
+
     def has_warnings?
       builds.latest.ignored.any?
     end
@@ -182,10 +162,6 @@ def ci_yaml_file
       end
     end
 
-    def skip_ci?
-      git_commit_message =~ /\[(ci skip|skip ci)\]/i if git_commit_message
-    end
-
     def environments
       builds.where.not(environment: nil).success.pluck(:environment).uniq
     end
@@ -207,39 +183,33 @@ def notes
       Note.for_commit_id(sha)
     end
 
+    def process!
+      Ci::ProcessPipelineService.new(project, user).execute(self)
+      reload_status!
+    end
+
     def predefined_variables
       [
         { key: 'CI_PIPELINE_ID', value: id.to_s, public: true }
       ]
     end
 
-    private
-
-    def build_builds_for_stages(stages, user, status, trigger_request)
-      ##
-      # Note that `Array#any?` implements a short circuit evaluation, so we
-      # build builds only for the first stage that has builds available.
-      #
-      stages.any? do |stage|
-        CreateBuildsService.new(self).
-          execute(stage, user, status, trigger_request).
-          any?(&:active?)
-      end
-    end
-
-    def update_state
+    def reload_status!
       statuses.reload
-      self.status = if yaml_errors.blank?
-                      statuses.latest.status || 'skipped'
-                    else
-                      'failed'
-                    end
+      self.status =
+        if yaml_errors.blank?
+          statuses.latest.status || 'skipped'
+        else
+          'failed'
+        end
       self.started_at = statuses.started_at
       self.finished_at = statuses.finished_at
       self.duration = statuses.latest.duration
       save
     end
 
+    private
+
     def keep_around_commits
       return unless project
 
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 2d185c28809d69..20713314a252ad 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -5,7 +5,7 @@ class CommitStatus < ActiveRecord::Base
   self.table_name = 'ci_builds'
 
   belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id
-  belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id, touch: true
+  belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id
   belongs_to :user
 
   delegate :commit, to: :pipeline
@@ -25,28 +25,36 @@ class CommitStatus < ActiveRecord::Base
   scope :ordered, -> { order(:name) }
   scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) }
 
-  state_machine :status, initial: :pending do
+  state_machine :status do
     event :queue do
-      transition skipped: :pending
+      transition [:created, :skipped] => :pending
     end
 
     event :run do
       transition pending: :running
     end
 
+    event :skip do
+      transition [:created, :pending] => :skipped
+    end
+
     event :drop do
-      transition [:pending, :running] => :failed
+      transition [:created, :pending, :running] => :failed
     end
 
     event :success do
-      transition [:pending, :running] => :success
+      transition [:created, :pending, :running] => :success
     end
 
     event :cancel do
-      transition [:pending, :running] => :canceled
+      transition [:created, :pending, :running] => :canceled
+    end
+
+    after_transition created: [:pending, :running] do |commit_status|
+      commit_status.update_attributes queued_at: Time.now
     end
 
-    after_transition pending: :running do |commit_status|
+    after_transition [:created, :pending] => :running do |commit_status|
       commit_status.update_attributes started_at: Time.now
     end
 
@@ -54,13 +62,20 @@ class CommitStatus < ActiveRecord::Base
       commit_status.update_attributes finished_at: Time.now
     end
 
-    after_transition [:pending, :running] => :success do |commit_status|
+    after_transition [:created, :pending, :running] => :success do |commit_status|
       MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.pipeline.project, nil).trigger(commit_status)
     end
 
     after_transition any => :failed do |commit_status|
       MergeRequests::AddTodoWhenBuildFailsService.new(commit_status.pipeline.project, nil).execute(commit_status)
     end
+
+    # We use around_transition to process pipeline on next stages as soon as possible, before the `after_*` is executed
+    around_transition any => [:success, :failed, :canceled] do |commit_status, block|
+      block.call
+
+      commit_status.pipeline.process! if commit_status.pipeline
+    end
   end
 
   delegate :sha, :short_sha, to: :pipeline
diff --git a/app/models/concerns/statuseable.rb b/app/models/concerns/statuseable.rb
index 44c6b30f2788a8..5d4b0a868998c8 100644
--- a/app/models/concerns/statuseable.rb
+++ b/app/models/concerns/statuseable.rb
@@ -1,18 +1,22 @@
 module Statuseable
   extend ActiveSupport::Concern
 
-  AVAILABLE_STATUSES = %w(pending running success failed canceled skipped)
+  AVAILABLE_STATUSES = %w[created pending running success failed canceled skipped]
+  STARTED_STATUSES = %w[running success failed skipped]
+  ACTIVE_STATUSES = %w[pending running]
+  COMPLETED_STATUSES = %w[success failed canceled]
 
   class_methods do
     def status_sql
-      builds = all.select('count(*)').to_sql
-      success = all.success.select('count(*)').to_sql
-      ignored = all.ignored.select('count(*)').to_sql if all.respond_to?(:ignored)
+      scope = all.relevant
+      builds = scope.select('count(*)').to_sql
+      success = scope.success.select('count(*)').to_sql
+      ignored = scope.ignored.select('count(*)').to_sql if scope.respond_to?(:ignored)
       ignored ||= '0'
-      pending = all.pending.select('count(*)').to_sql
-      running = all.running.select('count(*)').to_sql
-      canceled = all.canceled.select('count(*)').to_sql
-      skipped = all.skipped.select('count(*)').to_sql
+      pending = scope.pending.select('count(*)').to_sql
+      running = scope.running.select('count(*)').to_sql
+      canceled = scope.canceled.select('count(*)').to_sql
+      skipped = scope.skipped.select('count(*)').to_sql
 
       deduce_status = "(CASE
         WHEN (#{builds})=0 THEN NULL
@@ -48,7 +52,8 @@ def finished_at
   included do
     validates :status, inclusion: { in: AVAILABLE_STATUSES }
 
-    state_machine :status, initial: :pending do
+    state_machine :status, initial: :created do
+      state :created, value: 'created'
       state :pending, value: 'pending'
       state :running, value: 'running'
       state :failed, value: 'failed'
@@ -57,6 +62,8 @@ def finished_at
       state :skipped, value: 'skipped'
     end
 
+    scope :created, -> { where(status: 'created') }
+    scope :relevant, -> { where.not(status: 'created') }
     scope :running, -> { where(status: 'running') }
     scope :pending, -> { where(status: 'pending') }
     scope :success, -> { where(status: 'success') }
@@ -68,14 +75,14 @@ def finished_at
   end
 
   def started?
-    !pending? && !canceled? && started_at
+    STARTED_STATUSES.include?(status) && started_at
   end
 
   def active?
-    running? || pending?
+    ACTIVE_STATUSES.include?(status)
   end
 
   def complete?
-    canceled? || success? || failed?
+    COMPLETED_STATUSES.include?(status)
   end
 end
diff --git a/app/services/ci/create_builds_service.rb b/app/services/ci/create_builds_service.rb
deleted file mode 100644
index 4946f7076fdd51..00000000000000
--- a/app/services/ci/create_builds_service.rb
+++ /dev/null
@@ -1,62 +0,0 @@
-module Ci
-  class CreateBuildsService
-    def initialize(pipeline)
-      @pipeline = pipeline
-      @config = pipeline.config_processor
-    end
-
-    def execute(stage, user, status, trigger_request = nil)
-      builds_attrs = @config.builds_for_stage_and_ref(stage, @pipeline.ref, @pipeline.tag, trigger_request)
-
-      # check when to create next build
-      builds_attrs = builds_attrs.select do |build_attrs|
-        case build_attrs[:when]
-        when 'on_success'
-          status == 'success'
-        when 'on_failure'
-          status == 'failed'
-        when 'always', 'manual'
-          %w(success failed).include?(status)
-        end
-      end
-
-      # don't create the same build twice
-      builds_attrs.reject! do |build_attrs|
-        @pipeline.builds.find_by(ref: @pipeline.ref,
-                                 tag: @pipeline.tag,
-                                 trigger_request: trigger_request,
-                                 name: build_attrs[:name])
-      end
-
-      builds_attrs.map do |build_attrs|
-        build_attrs.slice!(:name,
-                           :commands,
-                           :tag_list,
-                           :options,
-                           :allow_failure,
-                           :stage,
-                           :stage_idx,
-                           :environment,
-                           :when,
-                           :yaml_variables)
-
-        build_attrs.merge!(pipeline: @pipeline,
-                           ref: @pipeline.ref,
-                           tag: @pipeline.tag,
-                           trigger_request: trigger_request,
-                           user: user,
-                           project: @pipeline.project)
-
-        # TODO: The proper implementation for this is in
-        # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5295
-        build_attrs[:status] = 'skipped' if build_attrs[:when] == 'manual'
-
-        ##
-        # We do not persist new builds here.
-        # Those will be persisted when @pipeline is saved.
-        #
-        @pipeline.builds.new(build_attrs)
-      end
-    end
-  end
-end
diff --git a/app/services/ci/create_pipeline_builds_service.rb b/app/services/ci/create_pipeline_builds_service.rb
new file mode 100644
index 00000000000000..005014fa1ded2e
--- /dev/null
+++ b/app/services/ci/create_pipeline_builds_service.rb
@@ -0,0 +1,42 @@
+module Ci
+  class CreatePipelineBuildsService < BaseService
+    attr_reader :pipeline
+
+    def execute(pipeline)
+      @pipeline = pipeline
+
+      new_builds.map do |build_attributes|
+        create_build(build_attributes)
+      end
+    end
+
+    private
+
+    def create_build(build_attributes)
+      build_attributes = build_attributes.merge(
+        pipeline: pipeline,
+        project: pipeline.project,
+        ref: pipeline.ref,
+        tag: pipeline.tag,
+        user: current_user,
+        trigger_request: trigger_request
+      )
+      pipeline.builds.create(build_attributes)
+    end
+
+    def new_builds
+      @new_builds ||= pipeline.config_builds_attributes.
+        reject { |build| existing_build_names.include?(build[:name]) }
+    end
+
+    def existing_build_names
+      @existing_build_names ||= pipeline.builds.pluck(:name)
+    end
+
+    def trigger_request
+      return @trigger_request if defined?(@trigger_request)
+
+      @trigger_request ||= pipeline.trigger_requests.first
+    end
+  end
+end
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index be91bf0db85a30..7398fd8e10af2b 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -1,49 +1,100 @@
 module Ci
   class CreatePipelineService < BaseService
-    def execute
-      pipeline = project.pipelines.new(params)
-      pipeline.user = current_user
+    attr_reader :pipeline
 
-      unless ref_names.include?(params[:ref])
-        pipeline.errors.add(:base, 'Reference not found')
-        return pipeline
+    def execute(ignore_skip_ci: false, save_on_errors: true, trigger_request: nil)
+      @pipeline = Ci::Pipeline.new(
+        project: project,
+        ref: ref,
+        sha: sha,
+        before_sha: before_sha,
+        tag: tag?,
+        trigger_requests: Array(trigger_request),
+        user: current_user
+      )
+
+      unless project.builds_enabled?
+        return error('Pipeline is disabled')
+      end
+
+      unless trigger_request || can?(current_user, :create_pipeline, project)
+        return error('Insufficient permissions to create a new pipeline')
       end
 
-      if commit
-        pipeline.sha = commit.id
-      else
-        pipeline.errors.add(:base, 'Commit not found')
-        return pipeline
+      unless branch? || tag?
+        return error('Reference not found')
       end
 
-      unless can?(current_user, :create_pipeline, project)
-        pipeline.errors.add(:base, 'Insufficient permissions to create a new pipeline')
-        return pipeline
+      unless commit
+        return error('Commit not found')
       end
 
       unless pipeline.config_processor
-        pipeline.errors.add(:base, pipeline.yaml_errors || 'Missing .gitlab-ci.yml file')
-        return pipeline
+        unless pipeline.ci_yaml_file
+          return error('Missing .gitlab-ci.yml file')
+        end
+        return error(pipeline.yaml_errors, save: save_on_errors)
       end
 
-      pipeline.save!
+      if !ignore_skip_ci && skip_ci?
+        return error('Creation of pipeline is skipped', save: save_on_errors)
+      end
 
-      unless pipeline.create_builds(current_user)
-        pipeline.errors.add(:base, 'No builds for this pipeline.')
+      unless pipeline.config_builds_attributes.present?
+        return error('No builds for this pipeline.')
       end
 
       pipeline.save
+      pipeline.process!
       pipeline
     end
 
     private
 
-    def ref_names
-      @ref_names ||= project.repository.ref_names
+    def skip_ci?
+      pipeline.git_commit_message =~ /\[(ci skip|skip ci)\]/i if pipeline.git_commit_message
     end
 
     def commit
-      @commit ||= project.commit(params[:ref])
+      @commit ||= project.commit(origin_sha || origin_ref)
+    end
+
+    def sha
+      commit.try(:id)
+    end
+
+    def before_sha
+      params[:checkout_sha] || params[:before] || Gitlab::Git::BLANK_SHA
+    end
+
+    def origin_sha
+      params[:checkout_sha] || params[:after]
+    end
+
+    def origin_ref
+      params[:ref]
+    end
+
+    def branch?
+      project.repository.ref_exists?(Gitlab::Git::BRANCH_REF_PREFIX + ref)
+    end
+
+    def tag?
+      project.repository.ref_exists?(Gitlab::Git::TAG_REF_PREFIX + ref)
+    end
+
+    def ref
+      Gitlab::Git.ref_name(origin_ref)
+    end
+
+    def valid_sha?
+      origin_sha && origin_sha != Gitlab::Git::BLANK_SHA
+    end
+
+    def error(message, save: false)
+      pipeline.errors.add(:base, message)
+      pipeline.reload_status! if save
+      pipeline
     end
   end
 end
diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb
index 1e629cf119aa51..6af3c1ca5b130d 100644
--- a/app/services/ci/create_trigger_request_service.rb
+++ b/app/services/ci/create_trigger_request_service.rb
@@ -1,20 +1,11 @@
 module Ci
   class CreateTriggerRequestService
     def execute(project, trigger, ref, variables = nil)
-      commit = project.commit(ref)
-      return unless commit
+      trigger_request = trigger.trigger_requests.create(variables: variables)
 
-      # check if ref is tag
-      tag = project.repository.find_tag(ref).present?
-
-      pipeline = project.pipelines.create(sha: commit.sha, ref: ref, tag: tag)
-
-      trigger_request = trigger.trigger_requests.create!(
-        variables: variables,
-        pipeline: pipeline,
-      )
-
-      if pipeline.create_builds(nil, trigger_request)
+      pipeline = Ci::CreatePipelineService.new(project, nil, ref: ref).
+        execute(ignore_skip_ci: true, trigger_request: trigger_request)
+      if pipeline.persisted?
         trigger_request
       end
     end
diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb
new file mode 100644
index 00000000000000..86c4823d18a453
--- /dev/null
+++ b/app/services/ci/process_pipeline_service.rb
@@ -0,0 +1,77 @@
+module Ci
+  class ProcessPipelineService < BaseService
+    attr_reader :pipeline
+
+    def execute(pipeline)
+      @pipeline = pipeline
+
+      # This method will ensure that our pipeline does have all builds for all stages created
+      if created_builds.empty?
+        create_builds!
+      end
+
+      new_builds =
+        stage_indexes_of_created_builds.map do |index|
+          process_stage(index)
+        end
+
+      # Return a flag if a when builds got enqueued
+      new_builds.flatten.any?
+    end
+
+    private
+
+    def create_builds!
+      Ci::CreatePipelineBuildsService.new(project, current_user).execute(pipeline)
+    end
+
+    def process_stage(index)
+      current_status = status_for_prior_stages(index)
+
+      created_builds_in_stage(index).select do |build|
+        process_build(build, current_status)
+      end
+    end
+
+    def process_build(build, current_status)
+      return false unless Statuseable::COMPLETED_STATUSES.include?(current_status)
+
+      if valid_statuses_for_when(build.when).include?(current_status)
+        build.queue
+        true
+      else
+        build.skip
+        false
+      end
+    end
+
+    def valid_statuses_for_when(value)
+      case value
+      when 'on_success'
+        %w[success]
+      when 'on_failure'
+        %w[failed]
+      when 'always'
+        %w[success failed]
+      else
+        []
+      end
+    end
+
+    def status_for_prior_stages(index)
+      pipeline.builds.where('stage_idx < ?', index).latest.status || 'success'
+    end
+
+    def stage_indexes_of_created_builds
+      created_builds.order(:stage_idx).pluck('distinct stage_idx')
+    end
+
+    def created_builds_in_stage(index)
+      created_builds.where(stage_idx: index)
+    end
+
+    def created_builds
+      pipeline.builds.created
+    end
+  end
+end
diff --git a/app/services/create_commit_builds_service.rb b/app/services/create_commit_builds_service.rb
deleted file mode 100644
index 0b66b854deabd2..00000000000000
--- a/app/services/create_commit_builds_service.rb
+++ /dev/null
@@ -1,69 +0,0 @@
-class CreateCommitBuildsService
-  def execute(project, user, params)
-    return unless project.builds_enabled?
-
-    before_sha = params[:checkout_sha] || params[:before]
-    sha = params[:checkout_sha] || params[:after]
-    origin_ref = params[:ref]
-
-    ref = Gitlab::Git.ref_name(origin_ref)
-    tag = Gitlab::Git.tag_ref?(origin_ref)
-
-    # Skip branch removal
-    if sha == Gitlab::Git::BLANK_SHA
-      return false
-    end
-
-    @pipeline = Ci::Pipeline.new(
-      project: project,
-      sha: sha,
-      ref: ref,
-      before_sha: before_sha,
-      tag: tag,
-      user: user)
-
-    ##
-    # Skip creating pipeline if no gitlab-ci.yml is found
-    #
-    unless @pipeline.ci_yaml_file
-      return false
-    end
-
-    ##
-    # Skip creating builds for commits that have [ci skip]
-    # but save pipeline object
-    #
-    if @pipeline.skip_ci?
-      return save_pipeline!
-    end
-
-    ##
-    # Skip creating builds when CI config is invalid
-    # but save pipeline object
-    #
-    unless @pipeline.config_processor
-      return save_pipeline!
-    end
-
-    ##
-    # Skip creating pipeline object if there are no builds for it.
-    #
-    unless @pipeline.create_builds(user)
-      @pipeline.errors.add(:base, 'No builds created')
-      return false
-    end
-
-    save_pipeline!
-  end
-
-  private
-
-  ##
-  # Create a new pipeline and touch object to calculate status
-  #
-  def save_pipeline!
-    @pipeline.save!
-    @pipeline.touch
-    @pipeline
-  end
-end
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index 3f6a177bf3aff3..6f521462cf33cb 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -69,7 +69,7 @@ def update_merge_requests
     SystemHooksService.new.execute_hooks(build_push_data_system_hook.dup, :push_hooks)
     @project.execute_hooks(build_push_data.dup, :push_hooks)
     @project.execute_services(build_push_data.dup, :push_hooks)
-    CreateCommitBuildsService.new.execute(@project, current_user, build_push_data)
+    Ci::CreatePipelineService.new(project, current_user, build_push_data).execute
     ProjectCacheWorker.perform_async(@project.id)
   end
 
diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb
index 969530c4fdc383..d2b52f16fa8dc0 100644
--- a/app/services/git_tag_push_service.rb
+++ b/app/services/git_tag_push_service.rb
@@ -11,7 +11,7 @@ def execute
     SystemHooksService.new.execute_hooks(build_system_push_data.dup, :tag_push_hooks)
     project.execute_hooks(@push_data.dup, :tag_push_hooks)
     project.execute_services(@push_data.dup, :tag_push_hooks)
-    CreateCommitBuildsService.new.execute(project, current_user, @push_data)
+    Ci::CreatePipelineService.new(project, current_user, @push_data).execute
     ProjectCacheWorker.perform_async(project.id)
 
     true
diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml
index 9a594877803aba..78709a92aedf40 100644
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ b/app/views/projects/ci/pipelines/_pipeline.html.haml
@@ -33,7 +33,7 @@
           Cant find HEAD commit for this branch
 
 
-    - stages_status = pipeline.statuses.latest.stages_status
+    - stages_status = pipeline.statuses.relevant.latest.stages_status
     - stages.each do |stage|
       %td.stage-cell
         - status = stages_status[stage]
diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml
index 540689f4a61477..640abdb993fa09 100644
--- a/app/views/projects/commit/_pipeline.html.haml
+++ b/app/views/projects/commit/_pipeline.html.haml
@@ -46,5 +46,5 @@
         - if pipeline.project.build_coverage_enabled?
           %th Coverage
         %th
-    - pipeline.statuses.stages.each do |stage|
-      = render 'projects/commit/ci_stage', stage: stage, statuses: pipeline.statuses.where(stage: stage)
+    - pipeline.statuses.relevant.stages.each do |stage|
+      = render 'projects/commit/ci_stage', stage: stage, statuses: pipeline.statuses.relevant.where(stage: stage)
diff --git a/db/migrate/20160716115711_add_queued_at_to_ci_builds.rb b/db/migrate/20160716115711_add_queued_at_to_ci_builds.rb
new file mode 100644
index 00000000000000..756910a1fa0cdb
--- /dev/null
+++ b/db/migrate/20160716115711_add_queued_at_to_ci_builds.rb
@@ -0,0 +1,9 @@
+class AddQueuedAtToCiBuilds < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  def change
+    add_column :ci_builds, :queued_at, :timestamp
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index b795eeaa23cb41..e0c6178a5a0a7e 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -172,6 +172,7 @@
     t.integer  "artifacts_size"
     t.string   "when"
     t.text     "yaml_variables"
+    t.datetime "queued_at"
   end
 
   add_index "ci_builds", ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree
diff --git a/features/steps/shared/builds.rb b/features/steps/shared/builds.rb
index 4d6b258f5778d1..c7f61da05fab02 100644
--- a/features/steps/shared/builds.rb
+++ b/features/steps/shared/builds.rb
@@ -10,20 +10,22 @@ module SharedBuilds
   end
 
   step 'project has a recent build' do
-    @pipeline = create(:ci_pipeline, project: @project, sha: @project.commit.sha, ref: 'master')
+    @pipeline = create(:ci_empty_pipeline, project: @project, sha: @project.commit.sha, ref: 'master')
     @build = create(:ci_build_with_coverage, pipeline: @pipeline)
+    @pipeline.reload_status!
   end
 
   step 'recent build is successful' do
-    @build.update(status: 'success')
+    @build.success
   end
 
   step 'recent build failed' do
-    @build.update(status: 'failed')
+    @build.drop
   end
 
   step 'project has another build that is running' do
     create(:ci_build, pipeline: @pipeline, name: 'second build', status: 'running')
+    @pipeline.reload_status!
   end
 
   step 'I visit recent build details page' do
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
index a2e8bd22a52578..47efd5bd9f264e 100644
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -62,7 +62,7 @@ def build_attributes(name)
         #  - before script should be a concatenated command
         commands: [job[:before_script] || @before_script, job[:script]].flatten.compact.join("\n"),
         tag_list: job[:tags] || [],
-        name: job[:name],
+        name: job[:name].to_s,
         allow_failure: job[:allow_failure] || false,
         when: job[:when] || 'on_success',
         environment: job[:environment],
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index 1b32d560b1620e..0c93bbdfe26ebf 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -7,6 +7,7 @@
     stage_idx 0
     ref 'master'
     tag false
+    status 'pending'
     created_at 'Di 29. Okt 09:50:00 CET 2013'
     started_at 'Di 29. Okt 09:51:28 CET 2013'
     finished_at 'Di 29. Okt 09:53:28 CET 2013'
@@ -45,6 +46,10 @@
       status 'pending'
     end
 
+    trait :created do
+      status 'created'
+    end
+
     trait :manual do
       status 'skipped'
       self.when 'manual'
diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb
index a039bef6f3c44e..04d66020c87b74 100644
--- a/spec/factories/ci/pipelines.rb
+++ b/spec/factories/ci/pipelines.rb
@@ -18,7 +18,9 @@
 
 FactoryGirl.define do
   factory :ci_empty_pipeline, class: Ci::Pipeline do
+    ref 'master'
     sha '97de212e80737a608d939f648d959671fb0a0142'
+    status 'pending'
 
     project factory: :empty_project
 
diff --git a/spec/factories/commit_statuses.rb b/spec/factories/commit_statuses.rb
index 1e5c479616c50b..995f2080f1008e 100644
--- a/spec/factories/commit_statuses.rb
+++ b/spec/factories/commit_statuses.rb
@@ -7,6 +7,30 @@
     started_at 'Tue, 26 Jan 2016 08:21:42 +0100'
     finished_at 'Tue, 26 Jan 2016 08:23:42 +0100'
 
+    trait :success do
+      status 'success'
+    end
+
+    trait :failed do
+      status 'failed'
+    end
+
+    trait :canceled do
+      status 'canceled'
+    end
+
+    trait :running do
+      status 'running'
+    end
+
+    trait :pending do
+      status 'pending'
+    end
+
+    trait :created do
+      status 'created'
+    end
+
     after(:build) do |build, evaluator|
       build.project = build.pipeline.project
     end
diff --git a/spec/features/merge_requests/created_from_fork_spec.rb b/spec/features/merge_requests/created_from_fork_spec.rb
index f676200ecf3363..4d5d4aa121add2 100644
--- a/spec/features/merge_requests/created_from_fork_spec.rb
+++ b/spec/features/merge_requests/created_from_fork_spec.rb
@@ -29,12 +29,16 @@
     include WaitForAjax
 
     given(:pipeline) do
-      create(:ci_pipeline_with_two_job, project: fork_project,
-                                        sha: merge_request.diff_head_sha,
-                                        ref: merge_request.source_branch)
+      create(:ci_pipeline,
+             project: fork_project,
+             sha: merge_request.diff_head_sha,
+             ref: merge_request.source_branch)
     end
 
-    background { pipeline.create_builds(user) }
+    background do
+      create(:ci_build, pipeline: pipeline, name: 'rspec')
+      create(:ci_build, pipeline: pipeline, name: 'spinach')
+    end
 
     scenario 'user visits a pipelines page', js: true do
       visit_merge_request(merge_request)
diff --git a/spec/features/pipelines_spec.rb b/spec/features/pipelines_spec.rb
index eace76c370f15c..f88b8f8e60b0ef 100644
--- a/spec/features/pipelines_spec.rb
+++ b/spec/features/pipelines_spec.rb
@@ -33,7 +33,10 @@
     context 'cancelable pipeline' do
       let!(:running) { create(:ci_build, :running, pipeline: pipeline, stage: 'test', commands: 'test') }
 
-      before { visit namespace_project_pipelines_path(project.namespace, project) }
+      before do
+        pipeline.reload_status!
+        visit namespace_project_pipelines_path(project.namespace, project)
+      end
 
       it { expect(page).to have_link('Cancel') }
       it { expect(page).to have_selector('.ci-running') }
@@ -49,7 +52,10 @@
     context 'retryable pipelines' do
       let!(:failed) { create(:ci_build, :failed, pipeline: pipeline, stage: 'test', commands: 'test') }
 
-      before { visit namespace_project_pipelines_path(project.namespace, project) }
+      before do
+        pipeline.reload_status!
+        visit namespace_project_pipelines_path(project.namespace, project)
+      end
 
       it { expect(page).to have_link('Retry') }
       it { expect(page).to have_selector('.ci-failed') }
@@ -80,7 +86,10 @@
       context 'when running' do
         let!(:running) { create(:generic_commit_status, status: 'running', pipeline: pipeline, stage: 'test') }
 
-        before { visit namespace_project_pipelines_path(project.namespace, project) }
+        before do
+          pipeline.reload_status!
+          visit namespace_project_pipelines_path(project.namespace, project)
+        end
 
         it 'is not cancelable' do
           expect(page).not_to have_link('Cancel')
@@ -92,9 +101,12 @@
       end
 
       context 'when failed' do
-        let!(:running) { create(:generic_commit_status, status: 'failed', pipeline: pipeline, stage: 'test') }
+        let!(:failed) { create(:generic_commit_status, status: 'failed', pipeline: pipeline, stage: 'test') }
 
-        before { visit namespace_project_pipelines_path(project.namespace, project) }
+        before do
+          pipeline.reload_status!
+          visit namespace_project_pipelines_path(project.namespace, project)
+        end
 
         it 'is not retryable' do
           expect(page).not_to have_link('Retry')
@@ -211,7 +223,7 @@
 
     context 'for invalid commit' do
       before do
-        fill_in('Create for', with: 'invalid reference')
+        fill_in('Create for', with: 'invalid-reference')
         click_on 'Create pipeline'
       end
 
diff --git a/spec/lib/ci/charts_spec.rb b/spec/lib/ci/charts_spec.rb
index 034ea098193adf..2cd6b00dad6b2b 100644
--- a/spec/lib/ci/charts_spec.rb
+++ b/spec/lib/ci/charts_spec.rb
@@ -5,6 +5,7 @@
     before do
       @pipeline = FactoryGirl.create(:ci_pipeline)
       FactoryGirl.create(:ci_build, pipeline: @pipeline)
+      @pipeline.reload_status!
     end
 
     it 'returns build times in minutes' do
diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
index 85374b8761dd93..be51d942af7c14 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -19,7 +19,7 @@ module Ci
         expect(config_processor.builds_for_stage_and_ref(type, "master").first).to eq({
           stage: "test",
           stage_idx: 1,
-          name: :rspec,
+          name: "rspec",
           commands: "pwd\nrspec",
           tag_list: [],
           options: {},
@@ -433,7 +433,7 @@ module Ci
         expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
           stage: "test",
           stage_idx: 1,
-          name: :rspec,
+          name: "rspec",
           commands: "pwd\nrspec",
           tag_list: [],
           options: {
@@ -461,7 +461,7 @@ module Ci
         expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
           stage: "test",
           stage_idx: 1,
-          name: :rspec,
+          name: "rspec",
           commands: "pwd\nrspec",
           tag_list: [],
           options: {
@@ -700,7 +700,7 @@ module Ci
         expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
           stage: "test",
           stage_idx: 1,
-          name: :rspec,
+          name: "rspec",
           commands: "pwd\nrspec",
           tag_list: [],
           options: {
@@ -837,7 +837,7 @@ module Ci
           expect(subject.first).to eq({
             stage: "test",
             stage_idx: 1,
-            name: :normal_job,
+            name: "normal_job",
             commands: "test",
             tag_list: [],
             options: {},
@@ -882,7 +882,7 @@ module Ci
           expect(subject.first).to eq({
             stage: "build",
             stage_idx: 0,
-            name: :job1,
+            name: "job1",
             commands: "execute-script-for-job",
             tag_list: [],
             options: {},
@@ -894,7 +894,7 @@ module Ci
           expect(subject.second).to eq({
             stage: "build",
             stage_idx: 0,
-            name: :job2,
+            name: "job2",
             commands: "execute-script-for-job",
             tag_list: [],
             options: {},
diff --git a/spec/lib/gitlab/badge/build_spec.rb b/spec/lib/gitlab/badge/build_spec.rb
index ef9d9e7fef431d..bb8144d5122c86 100644
--- a/spec/lib/gitlab/badge/build_spec.rb
+++ b/spec/lib/gitlab/badge/build_spec.rb
@@ -96,9 +96,10 @@
   end
 
   def create_build(project, sha, branch)
-    pipeline = create(:ci_pipeline, project: project,
-                                    sha: sha,
-                                    ref: branch)
+    pipeline = create(:ci_empty_pipeline,
+                      project: project,
+                      sha: sha,
+                      ref: branch)
 
     create(:ci_build, pipeline: pipeline, stage: 'notify')
   end
diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb
index 9ecc9aac84bb7f..60a221eba50670 100644
--- a/spec/models/build_spec.rb
+++ b/spec/models/build_spec.rb
@@ -764,6 +764,53 @@ def create_mr(build, pipeline, factory: :merge_request, created_at: Time.now)
     end
   end
 
+  describe '#when' do
+    subject { build.when }
+
+    context 'if is undefined' do
+      before do
+        build.when = nil
+      end
+
+      context 'use from gitlab-ci.yml' do
+        before do
+          stub_ci_pipeline_yaml_file(config)
+        end
+
+        context 'if config is not found' do
+          let(:config) { nil }
+
+          it { is_expected.to eq('on_success') }
+        end
+
+        context 'if config does not have a questioned job' do
+          let(:config) do
+            YAML.dump({
+                        test_other: {
+                          script: 'Hello World'
+                        }
+                      })
+          end
+
+          it { is_expected.to eq('on_success') }
+        end
+
+        context 'if config has when' do
+          let(:config) do
+            YAML.dump({
+                        test: {
+                          script: 'Hello World',
+                          when: 'always'
+                        }
+                      })
+          end
+
+          it { is_expected.to eq('always') }
+        end
+      end
+    end
+  end
+
   describe '#retryable?' do
     context 'when build is running' do
       before do
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index ccee591cf7a50e..fdb579ab45c871 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -38,9 +38,6 @@
     it { expect(pipeline.sha).to start_with(subject) }
   end
 
-  describe '#create_next_builds' do
-  end
-
   describe '#retried' do
     subject { pipeline.retried }
 
@@ -54,304 +51,20 @@
     end
   end
 
-  describe '#create_builds' do
-    let!(:pipeline) { FactoryGirl.create :ci_pipeline, project: project, ref: 'master', tag: false }
-
-    def create_builds(trigger_request = nil)
-      pipeline.create_builds(nil, trigger_request)
-    end
-
-    def create_next_builds
-      pipeline.create_next_builds(pipeline.builds.order(:id).last)
-    end
-
-    it 'creates builds' do
-      expect(create_builds).to be_truthy
-      pipeline.builds.update_all(status: "success")
-      expect(pipeline.builds.count(:all)).to eq(2)
-
-      expect(create_next_builds).to be_truthy
-      pipeline.builds.update_all(status: "success")
-      expect(pipeline.builds.count(:all)).to eq(4)
-
-      expect(create_next_builds).to be_truthy
-      pipeline.builds.update_all(status: "success")
-      expect(pipeline.builds.count(:all)).to eq(5)
-
-      expect(create_next_builds).to be_falsey
-    end
-
-    context 'custom stage with first job allowed to fail' do
-      let(:yaml) do
-        {
-          stages: ['clean', 'test'],
-          clean_job: {
-            stage: 'clean',
-            allow_failure: true,
-            script: 'BUILD',
-          },
-          test_job: {
-            stage: 'test',
-            script: 'TEST',
-          },
-        }
-      end
-
-      before do
-        stub_ci_pipeline_yaml_file(YAML.dump(yaml))
-        create_builds
-      end
-
-      it 'properly schedules builds' do
-        expect(pipeline.builds.pluck(:status)).to contain_exactly('pending')
-        pipeline.builds.running_or_pending.each(&:drop)
-        expect(pipeline.builds.pluck(:status)).to contain_exactly('pending', 'failed')
-      end
-    end
-
-    context 'properly creates builds when "when" is defined' do
-      let(:yaml) do
-        {
-          stages: ["build", "test", "test_failure", "deploy", "cleanup"],
-          build: {
-            stage: "build",
-            script: "BUILD",
-          },
-          test: {
-            stage: "test",
-            script: "TEST",
-          },
-          test_failure: {
-            stage: "test_failure",
-            script: "ON test failure",
-            when: "on_failure",
-          },
-          deploy: {
-            stage: "deploy",
-            script: "PUBLISH",
-          },
-          cleanup: {
-            stage: "cleanup",
-            script: "TIDY UP",
-            when: "always",
-          }
-        }
-      end
-
-      before do
-        stub_ci_pipeline_yaml_file(YAML.dump(yaml))
-      end
-
-      context 'when builds are successful' do
-        it 'properly creates builds' do
-          expect(create_builds).to be_truthy
-          expect(pipeline.builds.pluck(:name)).to contain_exactly('build')
-          expect(pipeline.builds.pluck(:status)).to contain_exactly('pending')
-          pipeline.builds.running_or_pending.each(&:success)
-
-          expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test')
-          expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'pending')
-          pipeline.builds.running_or_pending.each(&:success)
-
-          expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy')
-          expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'pending')
-          pipeline.builds.running_or_pending.each(&:success)
-
-          expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup')
-          expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'pending')
-          pipeline.builds.running_or_pending.each(&:success)
-
-          expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'success')
-          pipeline.reload
-          expect(pipeline.status).to eq('success')
-        end
-      end
-
-      context 'when test job fails' do
-        it 'properly creates builds' do
-          expect(create_builds).to be_truthy
-          expect(pipeline.builds.pluck(:name)).to contain_exactly('build')
-          expect(pipeline.builds.pluck(:status)).to contain_exactly('pending')
-          pipeline.builds.running_or_pending.each(&:success)
-
-          expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test')
-          expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'pending')
-          pipeline.builds.running_or_pending.each(&:drop)
-
-          expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure')
-          expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending')
-          pipeline.builds.running_or_pending.each(&:success)
-
-          expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
-          expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'pending')
-          pipeline.builds.running_or_pending.each(&:success)
-
-          expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'success')
-          pipeline.reload
-          expect(pipeline.status).to eq('failed')
-        end
-      end
-
-      context 'when test and test_failure jobs fail' do
-        it 'properly creates builds' do
-          expect(create_builds).to be_truthy
-          expect(pipeline.builds.pluck(:name)).to contain_exactly('build')
-          expect(pipeline.builds.pluck(:status)).to contain_exactly('pending')
-          pipeline.builds.running_or_pending.each(&:success)
-
-          expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test')
-          expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'pending')
-          pipeline.builds.running_or_pending.each(&:drop)
-
-          expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure')
-          expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending')
-          pipeline.builds.running_or_pending.each(&:drop)
-
-          expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
-          expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'pending')
-          pipeline.builds.running_or_pending.each(&:success)
-
-          expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
-          expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'success')
-          pipeline.reload
-          expect(pipeline.status).to eq('failed')
-        end
-      end
-
-      context 'when deploy job fails' do
-        it 'properly creates builds' do
-          expect(create_builds).to be_truthy
-          expect(pipeline.builds.pluck(:name)).to contain_exactly('build')
-          expect(pipeline.builds.pluck(:status)).to contain_exactly('pending')
-          pipeline.builds.running_or_pending.each(&:success)
-
-          expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test')
-          expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'pending')
-          pipeline.builds.running_or_pending.each(&:success)
-
-          expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy')
-          expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'pending')
-          pipeline.builds.running_or_pending.each(&:drop)
-
-          expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup')
-          expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'pending')
-          pipeline.builds.running_or_pending.each(&:success)
-
-          expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'success')
-          pipeline.reload
-          expect(pipeline.status).to eq('failed')
-        end
-      end
-
-      context 'when build is canceled in the second stage' do
-        it 'does not schedule builds after build has been canceled' do
-          expect(create_builds).to be_truthy
-          expect(pipeline.builds.pluck(:name)).to contain_exactly('build')
-          expect(pipeline.builds.pluck(:status)).to contain_exactly('pending')
-          pipeline.builds.running_or_pending.each(&:success)
-
-          expect(pipeline.builds.running_or_pending).not_to be_empty
-
-          expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test')
-          expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'pending')
-          pipeline.builds.running_or_pending.each(&:cancel)
-
-          expect(pipeline.builds.running_or_pending).to be_empty
-          expect(pipeline.reload.status).to eq('canceled')
-        end
-      end
-
-      context 'when listing manual actions' do
-        let(:yaml) do
-          {
-            stages: ["build", "test", "staging", "production", "cleanup"],
-            build: {
-              stage: "build",
-              script: "BUILD",
-            },
-            test: {
-              stage: "test",
-              script: "TEST",
-            },
-            staging: {
-              stage: "staging",
-              script: "PUBLISH",
-            },
-            production: {
-              stage: "production",
-              script: "PUBLISH",
-              when: "manual",
-            },
-            cleanup: {
-              stage: "cleanup",
-              script: "TIDY UP",
-              when: "always",
-            },
-            clear_cache: {
-              stage: "cleanup",
-              script: "CLEAR CACHE",
-              when: "manual",
-            }
-          }
-        end
-
-        it 'returns only for skipped builds' do
-          # currently all builds are created
-          expect(create_builds).to be_truthy
-          expect(manual_actions).to be_empty
-
-          # succeed stage build
-          pipeline.builds.running_or_pending.each(&:success)
-          expect(manual_actions).to be_empty
-
-          # succeed stage test
-          pipeline.builds.running_or_pending.each(&:success)
-          expect(manual_actions).to be_empty
-
-          # succeed stage staging and skip stage production
-          pipeline.builds.running_or_pending.each(&:success)
-          expect(manual_actions).to be_many # production and clear cache
-
-          # succeed stage cleanup
-          pipeline.builds.running_or_pending.each(&:success)
-
-          # after processing a pipeline we should have 6 builds, 5 succeeded
-          expect(pipeline.builds.count).to eq(6)
-          expect(pipeline.builds.success.count).to eq(4)
-        end
-
-        def manual_actions
-          pipeline.manual_actions
-        end
-      end
-    end
-
-    context 'when no builds created' do
-      let(:pipeline) { build(:ci_pipeline) }
-
-      before do
-        stub_ci_pipeline_yaml_file(YAML.dump(before_script: ['ls']))
-      end
-
-      it 'returns false' do
-        expect(pipeline.create_builds(nil)).to be_falsey
-        expect(pipeline).not_to be_persisted
-      end
-    end
-  end
-
   describe "#finished_at" do
     let(:pipeline) { FactoryGirl.create :ci_pipeline }
 
     it "returns finished_at of latest build" do
       build = FactoryGirl.create :ci_build, pipeline: pipeline, finished_at: Time.now - 60
       FactoryGirl.create :ci_build, pipeline: pipeline, finished_at: Time.now - 120
+      pipeline.reload_status!
 
       expect(pipeline.finished_at.to_i).to eq(build.finished_at.to_i)
     end
 
     it "returns nil if there is no finished build" do
       FactoryGirl.create :ci_not_started_build, pipeline: pipeline
+      pipeline.reload_status!
 
       expect(pipeline.finished_at).to be_nil
     end
@@ -359,7 +72,7 @@ def manual_actions
 
   describe "coverage" do
     let(:project) { FactoryGirl.create :empty_project, build_coverage_regex: "/.*/" }
-    let(:pipeline) { FactoryGirl.create :ci_pipeline, project: project }
+    let(:pipeline) { FactoryGirl.create :ci_empty_pipeline, project: project }
 
     it "calculates average when there are two builds with coverage" do
       FactoryGirl.create :ci_build, name: "rspec", coverage: 30, pipeline: pipeline
@@ -426,31 +139,30 @@ def manual_actions
     end
   end
 
-  describe '#update_state' do
-    it 'executes update_state after touching object' do
-      expect(pipeline).to receive(:update_state).and_return(true)
-      pipeline.touch
-    end
+  describe '#reload_status!' do
+    let(:pipeline) { create :ci_empty_pipeline, project: project }
 
     context 'dependent objects' do
-      let(:commit_status) { build :commit_status, pipeline: pipeline }
+      let(:commit_status) { create :commit_status, :pending, pipeline: pipeline }
+
+      it 'executes reload_status! after succeeding dependent object' do
+        expect(pipeline).to receive(:reload_status!).and_return(true)
 
-      it 'executes update_state after saving dependent object' do
-        expect(pipeline).to receive(:update_state).and_return(true)
-        commit_status.save
+        commit_status.success
       end
     end
 
-    context 'update state' do
+    context 'updates' do
       let(:current) { Time.now.change(usec: 0) }
-      let(:build) { FactoryGirl.create :ci_build, :success, pipeline: pipeline, started_at: current - 120, finished_at: current - 60 }
+      let(:build) { FactoryGirl.create :ci_build, pipeline: pipeline, started_at: current - 120, finished_at: current - 60 }
 
       before do
         build
+        pipeline.reload_status!
       end
 
       [:status, :started_at, :finished_at, :duration].each do |param|
-        it "update #{param}" do
+        it "#{param}" do
           expect(pipeline.send(param)).to eq(build.send(param))
         end
       end
diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb
index bf438b26690948..1b383219eb9c85 100644
--- a/spec/models/project_services/hipchat_service_spec.rb
+++ b/spec/models/project_services/hipchat_service_spec.rb
@@ -291,7 +291,8 @@
     end
 
     context 'build events' do
-      let(:build) { create(:ci_build) }
+      let(:pipeline) { create(:ci_empty_pipeline) }
+      let(:build) { create(:ci_build, pipeline: pipeline) }
       let(:data) { Gitlab::BuildDataBuilder.build(build) }
 
       context 'for failed' do
diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb
index 966d302dfd3acf..a4cdd8f3140a38 100644
--- a/spec/requests/api/builds_spec.rb
+++ b/spec/requests/api/builds_spec.rb
@@ -238,6 +238,10 @@ def path_for_ref(ref = pipeline.ref, job = build.name)
         it { expect(response.headers).to include(download_headers) }
       end
 
+      before do
+        pipeline.reload_status!
+      end
+
       context 'with regular branch' do
         before do
           pipeline.update(ref: 'master',
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 4379fcb3c1ee1c..7ca75d77673334 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -89,16 +89,29 @@
 
       it "returns nil for commit without CI" do
         get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
+
         expect(response).to have_http_status(200)
         expect(json_response['status']).to be_nil
       end
 
       it "returns status for CI" do
         pipeline = project.ensure_pipeline(project.repository.commit.sha, 'master')
+        pipeline.update(status: 'success')
+
         get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
+
         expect(response).to have_http_status(200)
         expect(json_response['status']).to eq(pipeline.status)
       end
+
+      it "returns status for CI when pipeline is created" do
+        project.ensure_pipeline(project.repository.commit.sha, 'master')
+
+        get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
+
+        expect(response).to have_http_status(200)
+        expect(json_response['status']).to be_nil
+      end
     end
 
     context "unauthorized user" do
diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb
index 5702682fc7da2f..82bba1ce8a40fd 100644
--- a/spec/requests/api/triggers_spec.rb
+++ b/spec/requests/api/triggers_spec.rb
@@ -50,7 +50,8 @@
         post api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'master')
         expect(response).to have_http_status(201)
         pipeline.builds.reload
-        expect(pipeline.builds.size).to eq(2)
+        expect(pipeline.builds.pending.size).to eq(2)
+        expect(pipeline.builds.size).to eq(5)
       end
 
       it 'returns bad request with no builds created if there\'s no commit for that ref' do
diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb
index 05b309096cb7d8..ca7932dc5da3d1 100644
--- a/spec/requests/ci/api/builds_spec.rb
+++ b/spec/requests/ci/api/builds_spec.rb
@@ -6,112 +6,102 @@
   let(:runner) { FactoryGirl.create(:ci_runner, tag_list: ["mysql", "ruby"]) }
   let(:project) { FactoryGirl.create(:empty_project) }
 
-  before do
-    stub_ci_pipeline_to_return_yaml_file
-  end
-
   describe "Builds API for runners" do
-    let(:shared_runner) { FactoryGirl.create(:ci_runner, token: "SharedRunner") }
-    let(:shared_project) { FactoryGirl.create(:empty_project, name: "SharedProject") }
+    let(:pipeline) { create(:ci_pipeline_without_jobs, project: project, ref: 'master') }
 
     before do
-      FactoryGirl.create :ci_runner_project, project: project, runner: runner
+      project.runners << runner
     end
 
     describe "POST /builds/register" do
-      it "starts a build" do
-        pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master')
-        pipeline.create_builds(nil)
-        build = pipeline.builds.first
+      let!(:build) { create(:ci_build, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
 
-        post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
+      it "starts a build" do
+        register_builds info: { platform: :darwin }
 
         expect(response).to have_http_status(201)
         expect(json_response['sha']).to eq(build.sha)
         expect(runner.reload.platform).to eq("darwin")
+        expect(json_response["options"]).to eq({ "image" => "ruby:2.1", "services" => ["postgres"] })
+        expect(json_response["variables"]).to include(
+          { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true },
+          { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true },
+          { "key" => "DB_NAME", "value" => "postgres", "public" => true }
+        )
       end
 
-      it "returns 404 error if no pending build found" do
-        post ci_api("/builds/register"), token: runner.token
-
-        expect(response).to have_http_status(404)
-      end
-
-      it "returns 404 error if no builds for specific runner" do
-        pipeline = FactoryGirl.create(:ci_pipeline, project: shared_project)
-        FactoryGirl.create(:ci_build, pipeline: pipeline, status: 'pending')
+      context 'when builds are finished' do
+        before do
+          build.success
+        end
 
-        post ci_api("/builds/register"), token: runner.token
+        it "returns 404 error if no builds for specific runner" do
+          register_builds
 
-        expect(response).to have_http_status(404)
+          expect(response).to have_http_status(404)
+        end
       end
 
-      it "returns 404 error if no builds for shared runner" do
-        pipeline = FactoryGirl.create(:ci_pipeline, project: project)
-        FactoryGirl.create(:ci_build, pipeline: pipeline, status: 'pending')
+      context 'for other project with builds' do
+        before do
+          build.success
+          create(:ci_build, :pending)
+        end
 
-        post ci_api("/builds/register"), token: shared_runner.token
+        it "returns 404 error if no builds for shared runner" do
+          register_builds
 
-        expect(response).to have_http_status(404)
+          expect(response).to have_http_status(404)
+        end
       end
 
-      it "returns options" do
-        pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master')
-        pipeline.create_builds(nil)
+      context 'for shared runner' do
+        let(:shared_runner) { create(:ci_runner, token: "SharedRunner") }
 
-        post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
+        it "should return 404 error if no builds for shared runner" do
+          register_builds shared_runner.token
 
-        expect(response).to have_http_status(201)
-        expect(json_response["options"]).to eq({ "image" => "ruby:2.1", "services" => ["postgres"] })
+          expect(response).to have_http_status(404)
+        end
       end
 
-      it "returns variables" do
-        pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master')
-        pipeline.create_builds(nil)
-        project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value")
-
-        post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
+      context 'for triggered build' do
+        before do
+          trigger = create(:ci_trigger, project: project)
+          create(:ci_trigger_request_with_variables, pipeline: pipeline, builds: [build], trigger: trigger)
+          project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value")
+        end
 
-        expect(response).to have_http_status(201)
-        expect(json_response["variables"]).to include(
-          { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true },
-          { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true },
-          { "key" => "DB_NAME", "value" => "postgres", "public" => true },
-          { "key" => "SECRET_KEY", "value" => "secret_value", "public" => false }
-        )
+        it "returns variables for triggers" do
+          register_builds info: { platform: :darwin }
+
+          expect(response).to have_http_status(201)
+          expect(json_response["variables"]).to include(
+            { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true },
+            { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true },
+            { "key" => "CI_BUILD_TRIGGERED", "value" => "true", "public" => true },
+            { "key" => "DB_NAME", "value" => "postgres", "public" => true },
+            { "key" => "SECRET_KEY", "value" => "secret_value", "public" => false },
+            { "key" => "TRIGGER_KEY_1", "value" => "TRIGGER_VALUE_1", "public" => false },
+          )
+        end
       end
 
-      it "returns variables for triggers" do
-        trigger = FactoryGirl.create(:ci_trigger, project: project)
-        pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master')
-
-        trigger_request = FactoryGirl.create(:ci_trigger_request_with_variables, pipeline: pipeline, trigger: trigger)
-        pipeline.create_builds(nil, trigger_request)
-        project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value")
-
-        post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
-
-        expect(response).to have_http_status(201)
-        expect(json_response["variables"]).to include(
-          { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true },
-          { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true },
-          { "key" => "CI_BUILD_TRIGGERED", "value" => "true", "public" => true },
-          { "key" => "DB_NAME", "value" => "postgres", "public" => true },
-          { "key" => "SECRET_KEY", "value" => "secret_value", "public" => false },
-          { "key" => "TRIGGER_KEY_1", "value" => "TRIGGER_VALUE_1", "public" => false }
-        )
-      end
+      context 'with multiple builds' do
+        before do
+          build.success
+        end
 
-      it "returns dependent builds" do
-        pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master')
-        pipeline.create_builds(nil, nil)
-        pipeline.builds.where(stage: 'test').each(&:success)
+        let!(:test_build) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) }
 
-        post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
+        it "returns dependent builds" do
+          register_builds info: { platform: :darwin }
 
-        expect(response).to have_http_status(201)
-        expect(json_response["depends_on_builds"].count).to eq(2)
-        expect(json_response["depends_on_builds"][0]["name"]).to eq("rspec")
+          expect(response).to have_http_status(201)
+          expect(json_response["id"]).to eq(test_build.id)
+          expect(json_response["depends_on_builds"].count).to eq(1)
+          expect(json_response["depends_on_builds"][0]).to include('id' => build.id, 'name' => 'spinach')
+        end
       end
 
       %w(name version revision platform architecture).each do |param|
@@ -121,8 +111,9 @@
           subject { runner.read_attribute(param.to_sym) }
 
           it do
-            post ci_api("/builds/register"), token: runner.token, info: { param => value }
-            expect(response).to have_http_status(404)
+            register_builds info: { param => value }
+
+            expect(response).to have_http_status(201)
             runner.reload
             is_expected.to eq(value)
           end
@@ -131,8 +122,7 @@
 
       context 'when build has no tags' do
         before do
-          pipeline = create(:ci_pipeline, project: project)
-          create(:ci_build, pipeline: pipeline, tags: [])
+          build.update(tags: [])
         end
 
         context 'when runner is allowed to pick untagged builds' do
@@ -154,17 +144,15 @@
             expect(response).to have_http_status 404
           end
         end
+      end
 
-        def register_builds
-          post ci_api("/builds/register"), token: runner.token,
-                                           info: { platform: :darwin }
-        end
+      def register_builds(token = runner.token, **params)
+        post ci_api("/builds/register"), params.merge(token: token)
       end
     end
 
     describe "PUT /builds/:id" do
-      let(:pipeline) {create(:ci_pipeline, project: project)}
-      let(:build) { create(:ci_build, :trace, pipeline: pipeline, runner_id: runner.id) }
+      let(:build) { create(:ci_build, :pending, :trace, pipeline: pipeline, runner_id: runner.id) }
 
       before do
         build.run!
@@ -189,7 +177,7 @@ def register_builds
     end
 
     describe 'PATCH /builds/:id/trace.txt' do
-      let(:build) { create(:ci_build, :trace, runner_id: runner.id) }
+      let(:build) { create(:ci_build, :pending, :trace, runner_id: runner.id) }
       let(:headers) { { Ci::API::Helpers::BUILD_TOKEN_HEADER => build.token, 'Content-Type' => 'text/plain' } }
       let(:headers_with_range) { headers.merge({ 'Content-Range' => '11-20' }) }
 
@@ -237,8 +225,7 @@ def register_builds
     context "Artifacts" do
       let(:file_upload) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') }
       let(:file_upload2) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/gif') }
-      let(:pipeline) { create(:ci_pipeline, project: project) }
-      let(:build) { create(:ci_build, pipeline: pipeline, runner_id: runner.id) }
+      let(:build) { create(:ci_build, :pending, pipeline: pipeline, runner_id: runner.id) }
       let(:authorize_url) { ci_api("/builds/#{build.id}/artifacts/authorize") }
       let(:post_url) { ci_api("/builds/#{build.id}/artifacts") }
       let(:delete_url) { ci_api("/builds/#{build.id}/artifacts") }
diff --git a/spec/requests/ci/api/triggers_spec.rb b/spec/requests/ci/api/triggers_spec.rb
index 3312bd11669847..0a0f979f57d659 100644
--- a/spec/requests/ci/api/triggers_spec.rb
+++ b/spec/requests/ci/api/triggers_spec.rb
@@ -42,7 +42,8 @@
         post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options
         expect(response).to have_http_status(201)
         pipeline.builds.reload
-        expect(pipeline.builds.size).to eq(2)
+        expect(pipeline.builds.pending.size).to eq(2)
+        expect(pipeline.builds.size).to eq(5)
       end
 
       it 'returns bad request with no builds created if there\'s no commit for that ref' do
diff --git a/spec/services/ci/create_builds_service_spec.rb b/spec/services/ci/create_builds_service_spec.rb
deleted file mode 100644
index 8b0becd83d3805..00000000000000
--- a/spec/services/ci/create_builds_service_spec.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-require 'spec_helper'
-
-describe Ci::CreateBuildsService, services: true do
-  let(:pipeline) { create(:ci_pipeline, ref: 'master') }
-  let(:user) { create(:user) }
-
-  describe '#execute' do
-    # Using stubbed .gitlab-ci.yml created in commit factory
-    #
-
-    subject do
-      described_class.new(pipeline).execute('test', user, status, nil)
-    end
-
-    context 'next builds available' do
-      let(:status) { 'success' }
-
-      it { is_expected.to be_an_instance_of Array }
-      it { is_expected.to all(be_an_instance_of Ci::Build) }
-
-      it 'does not persist created builds' do
-        expect(subject.first).not_to be_persisted
-      end
-    end
-
-    context 'builds skipped' do
-      let(:status) { 'skipped' }
-
-      it { is_expected.to be_empty }
-    end
-  end
-end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
new file mode 100644
index 00000000000000..4aadd009f3ecf0
--- /dev/null
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -0,0 +1,214 @@
+require 'spec_helper'
+
+describe Ci::CreatePipelineService, services: true do
+  let(:project) { FactoryGirl.create(:project) }
+  let(:user) { create(:admin) }
+
+  before do
+    stub_ci_pipeline_to_return_yaml_file
+  end
+
+  describe '#execute' do
+    def execute(params)
+      described_class.new(project, user, params).execute
+    end
+
+    context 'valid params' do
+      let(:pipeline) do
+        execute(ref: 'refs/heads/master',
+                before: '00000000',
+                after: project.commit.id,
+                commits: [{ message: "Message" }])
+      end
+
+      it { expect(pipeline).to be_kind_of(Ci::Pipeline) }
+      it { expect(pipeline).to be_valid }
+      it { expect(pipeline).to be_persisted }
+      it { expect(pipeline).to eq(project.pipelines.last) }
+      it { expect(pipeline).to have_attributes(user: user) }
+      it { expect(pipeline.builds.first).to be_kind_of(Ci::Build) }
+    end
+
+    context "skip tag if there is no build for it" do
+      it "creates commit if there is appropriate job" do
+        result = execute(ref: 'refs/heads/master',
+                         before: '00000000',
+                         after: project.commit.id,
+                         commits: [{ message: "Message" }])
+        expect(result).to be_persisted
+      end
+
+      it "creates commit if there is no appropriate job but deploy job has right ref setting" do
+        config = YAML.dump({ deploy: { script: "ls", only: ["master"] } })
+        stub_ci_pipeline_yaml_file(config)
+        result = execute(ref: 'refs/heads/master',
+                         before: '00000000',
+                         after: project.commit.id,
+                         commits: [{ message: "Message" }])
+
+        expect(result).to be_persisted
+      end
+    end
+
+    it 'skips creating pipeline for refs without .gitlab-ci.yml' do
+      stub_ci_pipeline_yaml_file(nil)
+      result = execute(ref: 'refs/heads/master',
+                       before: '00000000',
+                       after: project.commit.id,
+                       commits: [{ message: 'Message' }])
+
+      expect(result).not_to be_persisted
+      expect(Ci::Pipeline.count).to eq(0)
+    end
+
+    it 'fails commits if yaml is invalid' do
+      message = 'message'
+      allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message }
+      stub_ci_pipeline_yaml_file('invalid: file: file')
+      commits = [{ message: message }]
+      pipeline = execute(ref: 'refs/heads/master',
+                         before: '00000000',
+                         after: project.commit.id,
+                         commits: commits)
+
+      expect(pipeline).to be_persisted
+      expect(pipeline.builds.any?).to be false
+      expect(pipeline.status).to eq('failed')
+      expect(pipeline.yaml_errors).not_to be_nil
+    end
+
+    context 'when commit contains a [ci skip] directive' do
+      let(:message) { "some message[ci skip]" }
+      let(:messageFlip) { "some message[skip ci]" }
+      let(:capMessage) { "some message[CI SKIP]" }
+      let(:capMessageFlip) { "some message[SKIP CI]" }
+
+      before do
+        allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message }
+      end
+
+      it "skips builds creation if there is [ci skip] tag in commit message" do
+        commits = [{ message: message }]
+        pipeline = execute(ref: 'refs/heads/master',
+                           before: '00000000',
+                           after: project.commit.id,
+                           commits: commits)
+
+        expect(pipeline).to be_persisted
+        expect(pipeline.builds.any?).to be false
+        expect(pipeline.status).to eq("skipped")
+      end
+
+      it "skips builds creation if there is [skip ci] tag in commit message" do
+        commits = [{ message: messageFlip }]
+        pipeline = execute(ref: 'refs/heads/master',
+                           before: '00000000',
+                           after: project.commit.id,
+                           commits: commits)
+
+        expect(pipeline).to be_persisted
+        expect(pipeline.builds.any?).to be false
+        expect(pipeline.status).to eq("skipped")
+      end
+
+      it "skips builds creation if there is [CI SKIP] tag in commit message" do
+        commits = [{ message: capMessage }]
+        pipeline = execute(ref: 'refs/heads/master',
+                           before: '00000000',
+                           after: project.commit.id,
+                           commits: commits)
+
+        expect(pipeline).to be_persisted
+        expect(pipeline.builds.any?).to be false
+        expect(pipeline.status).to eq("skipped")
+      end
+
+      it "skips builds creation if there is [SKIP CI] tag in commit message" do
+        commits = [{ message: capMessageFlip }]
+        pipeline = execute(ref: 'refs/heads/master',
+                           before: '00000000',
+                           after: project.commit.id,
+                           commits: commits)
+
+        expect(pipeline).to be_persisted
+        expect(pipeline.builds.any?).to be false
+        expect(pipeline.status).to eq("skipped")
+      end
+
+      it "does not skips builds creation if there is no [ci skip] or [skip ci] tag in commit message" do
+        allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { "some message" }
+
+        commits = [{ message: "some message" }]
+        pipeline = execute(ref: 'refs/heads/master',
+                           before: '00000000',
+                           after: project.commit.id,
+                           commits: commits)
+
+        expect(pipeline).to be_persisted
+        expect(pipeline.builds.first.name).to eq("rspec")
+      end
+
+      it "fails builds creation if there is [ci skip] tag in commit message and yaml is invalid" do
+        stub_ci_pipeline_yaml_file('invalid: file: fiile')
+        commits = [{ message: message }]
+        pipeline = execute(ref: 'refs/heads/master',
+                           before: '00000000',
+                           after: project.commit.id,
+                           commits: commits)
+
+        expect(pipeline).to be_persisted
+        expect(pipeline.builds.any?).to be false
+        expect(pipeline.status).to eq("failed")
+        expect(pipeline.yaml_errors).not_to be_nil
+      end
+    end
+
+    it "creates commit with failed status if yaml is invalid" do
+      stub_ci_pipeline_yaml_file('invalid: file')
+      commits = [{ message: "some message" }]
+      pipeline = execute(ref: 'refs/heads/master',
+                         before: '00000000',
+                         after: project.commit.id,
+                         commits: commits)
+
+      expect(pipeline).to be_persisted
+      expect(pipeline.status).to eq("failed")
+      expect(pipeline.builds.any?).to be false
+    end
+
+    context 'when there are no jobs for this pipeline' do
+      before do
+        config = YAML.dump({ test: { script: 'ls', only: ['feature'] } })
+        stub_ci_pipeline_yaml_file(config)
+      end
+
+      it 'does not create a new pipeline' do
+        result = execute(ref: 'refs/heads/master',
+                         before: '00000000',
+                         after: project.commit.id,
+                         commits: [{ message: 'some msg' }])
+
+        expect(result).not_to be_persisted
+        expect(Ci::Build.all).to be_empty
+        expect(Ci::Pipeline.count).to eq(0)
+      end
+    end
+
+    context 'with manual actions' do
+      before do
+        config = YAML.dump({ deploy: { script: 'ls', when: 'manual' } })
+        stub_ci_pipeline_yaml_file(config)
+      end
+
+      it 'does not create a new pipeline' do
+        result = execute(ref: 'refs/heads/master',
+                         before: '00000000',
+                         after: project.commit.id,
+                         commits: [{ message: 'some msg' }])
+
+        expect(result).to be_persisted
+        expect(result.manual_actions).not_to be_empty
+      end
+    end
+  end
+end
diff --git a/spec/services/ci/create_trigger_request_service_spec.rb b/spec/services/ci/create_trigger_request_service_spec.rb
index b72e0bd3dbeb47..d8c443d29d5646 100644
--- a/spec/services/ci/create_trigger_request_service_spec.rb
+++ b/spec/services/ci/create_trigger_request_service_spec.rb
@@ -1,7 +1,7 @@
 require 'spec_helper'
 
 describe Ci::CreateTriggerRequestService, services: true do
-  let(:service) { Ci::CreateTriggerRequestService.new }
+  let(:service) { described_class.new }
   let(:project) { create(:project) }
   let(:trigger) { create(:ci_trigger, project: project) }
 
@@ -27,8 +27,7 @@
       subject { service.execute(project, trigger, 'master') }
 
       before do
-        stub_ci_pipeline_yaml_file('{}')
-        FactoryGirl.create :ci_pipeline, project: project
+        stub_ci_pipeline_yaml_file('script: { only: [develop], script: hello World }')
       end
 
       it { expect(subject).to be_nil }
diff --git a/spec/services/ci/image_for_build_service_spec.rb b/spec/services/ci/image_for_build_service_spec.rb
index 3a3e3efe709ada..259062406c7370 100644
--- a/spec/services/ci/image_for_build_service_spec.rb
+++ b/spec/services/ci/image_for_build_service_spec.rb
@@ -5,8 +5,8 @@ module Ci
     let(:service) { ImageForBuildService.new }
     let(:project) { FactoryGirl.create(:empty_project) }
     let(:commit_sha) { '01234567890123456789' }
-    let(:commit) { project.ensure_pipeline(commit_sha, 'master') }
-    let(:build) { FactoryGirl.create(:ci_build, pipeline: commit) }
+    let(:pipeline) { project.ensure_pipeline(commit_sha, 'master') }
+    let(:build) { FactoryGirl.create(:ci_build, pipeline: pipeline) }
 
     describe '#execute' do
       before { build }
@@ -14,6 +14,7 @@ module Ci
       context 'branch name' do
         before { allow(project).to receive(:commit).and_return(OpenStruct.new(sha: commit_sha)) }
         before { build.run! }
+        before { pipeline.reload_status! }
         let(:image) { service.execute(project, ref: 'master') }
 
         it { expect(image).to be_kind_of(OpenStruct) }
@@ -31,6 +32,7 @@ module Ci
 
       context 'commit sha' do
         before { build.run! }
+        before { pipeline.reload_status! }
         let(:image) { service.execute(project, sha: build.sha) }
 
         it { expect(image).to be_kind_of(OpenStruct) }
diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb
new file mode 100644
index 00000000000000..ad8c2485888d0c
--- /dev/null
+++ b/spec/services/ci/process_pipeline_service_spec.rb
@@ -0,0 +1,288 @@
+require 'spec_helper'
+
+describe Ci::ProcessPipelineService, services: true do
+  let(:pipeline) { create(:ci_pipeline, ref: 'master') }
+  let(:user) { create(:user) }
+  let(:all_builds) { pipeline.builds }
+  let(:builds) { all_builds.where.not(status: [:created, :skipped]) }
+  let(:config) { nil }
+
+  before do
+    allow(pipeline).to receive(:ci_yaml_file).and_return(config)
+  end
+
+  describe '#execute' do
+    def create_builds
+      described_class.new(pipeline.project, user).execute(pipeline)
+    end
+
+    def succeed_pending
+      builds.pending.update_all(status: 'success')
+    end
+
+    context 'start queuing next builds' do
+      before do
+        create(:ci_build, :created, pipeline: pipeline, name: 'linux', stage_idx: 0)
+        create(:ci_build, :created, pipeline: pipeline, name: 'mac', stage_idx: 0)
+        create(:ci_build, :created, pipeline: pipeline, name: 'rspec', stage_idx: 1)
+        create(:ci_build, :created, pipeline: pipeline, name: 'rubocop', stage_idx: 1)
+        create(:ci_build, :created, pipeline: pipeline, name: 'deploy', stage_idx: 2)
+      end
+
+      it 'processes a pipeline' do
+        expect(create_builds).to be_truthy
+        succeed_pending
+        expect(builds.success.count).to eq(2)
+
+        expect(create_builds).to be_truthy
+        succeed_pending
+        expect(builds.success.count).to eq(4)
+
+        expect(create_builds).to be_truthy
+        succeed_pending
+        expect(builds.success.count).to eq(5)
+
+        expect(create_builds).to be_falsey
+      end
+
+      it 'does not process pipeline if existing stage is running' do
+        expect(create_builds).to be_truthy
+        expect(builds.pending.count).to eq(2)
+        
+        expect(create_builds).to be_falsey
+        expect(builds.pending.count).to eq(2)
+      end
+    end
+
+    context 'custom stage with first job allowed to fail' do
+      before do
+        create(:ci_build, :created, pipeline: pipeline, name: 'clean_job', stage_idx: 0, allow_failure: true)
+        create(:ci_build, :created, pipeline: pipeline, name: 'test_job', stage_idx: 1, allow_failure: true)
+      end
+
+      it 'automatically triggers a next stage when build finishes' do
+        expect(create_builds).to be_truthy
+        expect(builds.pluck(:status)).to contain_exactly('pending')
+
+        pipeline.builds.running_or_pending.each(&:drop)
+        expect(builds.pluck(:status)).to contain_exactly('failed', 'pending')
+      end
+    end
+
+    context 'properly creates builds when "when" is defined' do
+      before do
+        create(:ci_build, :created, pipeline: pipeline, name: 'build', stage_idx: 0)
+        create(:ci_build, :created, pipeline: pipeline, name: 'test', stage_idx: 1)
+        create(:ci_build, :created, pipeline: pipeline, name: 'test_failure', stage_idx: 2, when: 'on_failure')
+        create(:ci_build, :created, pipeline: pipeline, name: 'deploy', stage_idx: 3)
+        create(:ci_build, :created, pipeline: pipeline, name: 'production', stage_idx: 3, when: 'manual')
+        create(:ci_build, :created, pipeline: pipeline, name: 'cleanup', stage_idx: 4, when: 'always')
+        create(:ci_build, :created, pipeline: pipeline, name: 'clear cache', stage_idx: 4, when: 'manual')
+      end
+
+      context 'when builds are successful' do
+        it 'properly creates builds' do
+          expect(create_builds).to be_truthy
+          expect(builds.pluck(:name)).to contain_exactly('build')
+          expect(builds.pluck(:status)).to contain_exactly('pending')
+          pipeline.builds.running_or_pending.each(&:success)
+
+          expect(builds.pluck(:name)).to contain_exactly('build', 'test')
+          expect(builds.pluck(:status)).to contain_exactly('success', 'pending')
+          pipeline.builds.running_or_pending.each(&:success)
+
+          expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy')
+          expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'pending')
+          pipeline.builds.running_or_pending.each(&:success)
+
+          expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup')
+          expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'pending')
+          pipeline.builds.running_or_pending.each(&:success)
+
+          expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'success')
+          pipeline.reload
+          expect(pipeline.status).to eq('success')
+        end
+      end
+
+      context 'when test job fails' do
+        it 'properly creates builds' do
+          expect(create_builds).to be_truthy
+          expect(builds.pluck(:name)).to contain_exactly('build')
+          expect(builds.pluck(:status)).to contain_exactly('pending')
+          pipeline.builds.running_or_pending.each(&:success)
+
+          expect(builds.pluck(:name)).to contain_exactly('build', 'test')
+          expect(builds.pluck(:status)).to contain_exactly('success', 'pending')
+          pipeline.builds.running_or_pending.each(&:drop)
+
+          expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure')
+          expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending')
+          pipeline.builds.running_or_pending.each(&:success)
+
+          expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
+          expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'pending')
+          pipeline.builds.running_or_pending.each(&:success)
+
+          expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'success')
+          pipeline.reload
+          expect(pipeline.status).to eq('failed')
+        end
+      end
+
+      context 'when test and test_failure jobs fail' do
+        it 'properly creates builds' do
+          expect(create_builds).to be_truthy
+          expect(builds.pluck(:name)).to contain_exactly('build')
+          expect(builds.pluck(:status)).to contain_exactly('pending')
+          pipeline.builds.running_or_pending.each(&:success)
+
+          expect(builds.pluck(:name)).to contain_exactly('build', 'test')
+          expect(builds.pluck(:status)).to contain_exactly('success', 'pending')
+          pipeline.builds.running_or_pending.each(&:drop)
+
+          expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure')
+          expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending')
+          pipeline.builds.running_or_pending.each(&:drop)
+
+          expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
+          expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'pending')
+          pipeline.builds.running_or_pending.each(&:success)
+
+          expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
+          expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'success')
+          pipeline.reload
+          expect(pipeline.status).to eq('failed')
+        end
+      end
+
+      context 'when deploy job fails' do
+        it 'properly creates builds' do
+          expect(create_builds).to be_truthy
+          expect(builds.pluck(:name)).to contain_exactly('build')
+          expect(builds.pluck(:status)).to contain_exactly('pending')
+          pipeline.builds.running_or_pending.each(&:success)
+
+          expect(builds.pluck(:name)).to contain_exactly('build', 'test')
+          expect(builds.pluck(:status)).to contain_exactly('success', 'pending')
+          pipeline.builds.running_or_pending.each(&:success)
+
+          expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy')
+          expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'pending')
+          pipeline.builds.running_or_pending.each(&:drop)
+
+          expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup')
+          expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'pending')
+          pipeline.builds.running_or_pending.each(&:success)
+
+          expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'success')
+          pipeline.reload
+          expect(pipeline.status).to eq('failed')
+        end
+      end
+
+      context 'when build is canceled in the second stage' do
+        it 'does not schedule builds after build has been canceled' do
+          expect(create_builds).to be_truthy
+          expect(builds.pluck(:name)).to contain_exactly('build')
+          expect(builds.pluck(:status)).to contain_exactly('pending')
+          pipeline.builds.running_or_pending.each(&:success)
+
+          expect(builds.running_or_pending).not_to be_empty
+
+          expect(builds.pluck(:name)).to contain_exactly('build', 'test')
+          expect(builds.pluck(:status)).to contain_exactly('success', 'pending')
+          pipeline.builds.running_or_pending.each(&:cancel)
+
+          expect(builds.running_or_pending).to be_empty
+          expect(pipeline.reload.status).to eq('canceled')
+        end
+      end
+
+      context 'when listing manual actions' do
+        it 'returns only for skipped builds' do
+          # currently all builds are created
+          expect(create_builds).to be_truthy
+          expect(manual_actions).to be_empty
+
+          # succeed stage build
+          pipeline.builds.running_or_pending.each(&:success)
+          expect(manual_actions).to be_empty
+
+          # succeed stage test
+          pipeline.builds.running_or_pending.each(&:success)
+          expect(manual_actions).to be_one # production
+
+          # succeed stage deploy
+          pipeline.builds.running_or_pending.each(&:success)
+          expect(manual_actions).to be_many # production and clear cache
+        end
+
+        def manual_actions
+          pipeline.manual_actions
+        end
+      end
+    end
+
+    context 'creates a builds from .gitlab-ci.yml' do
+      let(:config) do
+        YAML.dump({
+          rspec: {
+            stage: 'test',
+            script: 'rspec'
+          },
+          rubocop: {
+            stage: 'test',
+            script: 'rubocop'
+          },
+          deploy: {
+            stage: 'deploy',
+            script: 'deploy'
+          }
+        })
+      end
+
+      # Using stubbed .gitlab-ci.yml created in commit factory
+      #
+
+      before do
+        stub_ci_pipeline_yaml_file(config)
+        create(:ci_build, :created, pipeline: pipeline, name: 'linux', stage: 'build', stage_idx: 0)
+        create(:ci_build, :created, pipeline: pipeline, name: 'mac', stage: 'build', stage_idx: 0)
+      end
+
+      it 'when processing a pipeline' do
+        # Currently we have two builds with state created
+        expect(builds.count).to eq(0)
+        expect(all_builds.count).to eq(2)
+
+        # Create builds will mark the created as pending
+        expect(create_builds).to be_truthy
+        expect(builds.count).to eq(2)
+        expect(all_builds.count).to eq(2)
+
+        # When we builds succeed we will create a rest of pipeline from .gitlab-ci.yml
+        # We will have 2 succeeded, 2 pending (from stage test), total 5 (one more build from deploy)
+        succeed_pending
+        expect(create_builds).to be_truthy
+        expect(builds.success.count).to eq(2)
+        expect(builds.pending.count).to eq(2)
+        expect(all_builds.count).to eq(5)
+
+        # When we succeed the 2 pending from stage test,
+        # We will queue a deploy stage, no new builds will be created
+        succeed_pending
+        expect(create_builds).to be_truthy
+        expect(builds.pending.count).to eq(1)
+        expect(builds.success.count).to eq(4)
+        expect(all_builds.count).to eq(5)
+
+        # When we succeed last pending build, we will have a total of 5 succeeded builds, no new builds will be created
+        succeed_pending
+        expect(create_builds).to be_falsey
+        expect(builds.success.count).to eq(5)
+        expect(all_builds.count).to eq(5)
+      end
+    end
+  end
+end
diff --git a/spec/services/create_commit_builds_service_spec.rb b/spec/services/create_commit_builds_service_spec.rb
deleted file mode 100644
index d4c5e584421627..00000000000000
--- a/spec/services/create_commit_builds_service_spec.rb
+++ /dev/null
@@ -1,241 +0,0 @@
-require 'spec_helper'
-
-describe CreateCommitBuildsService, services: true do
-  let(:service) { CreateCommitBuildsService.new }
-  let(:project) { FactoryGirl.create(:empty_project) }
-  let(:user) { create(:user) }
-
-  before do
-    stub_ci_pipeline_to_return_yaml_file
-  end
-
-  describe '#execute' do
-    context 'valid params' do
-      let(:pipeline) do
-        service.execute(project, user,
-                        ref: 'refs/heads/master',
-                        before: '00000000',
-                        after: '31das312',
-                        commits: [{ message: "Message" }]
-                       )
-      end
-
-      it { expect(pipeline).to be_kind_of(Ci::Pipeline) }
-      it { expect(pipeline).to be_valid }
-      it { expect(pipeline).to be_persisted }
-      it { expect(pipeline).to eq(project.pipelines.last) }
-      it { expect(pipeline).to have_attributes(user: user) }
-      it { expect(pipeline.builds.first).to be_kind_of(Ci::Build) }
-    end
-
-    context "skip tag if there is no build for it" do
-      it "creates commit if there is appropriate job" do
-        result = service.execute(project, user,
-                                 ref: 'refs/tags/0_1',
-                                 before: '00000000',
-                                 after: '31das312',
-                                 commits: [{ message: "Message" }]
-                                )
-        expect(result).to be_persisted
-      end
-
-      it "creates commit if there is no appropriate job but deploy job has right ref setting" do
-        config = YAML.dump({ deploy: { script: "ls", only: ["0_1"] } })
-        stub_ci_pipeline_yaml_file(config)
-
-        result = service.execute(project, user,
-                                 ref: 'refs/heads/0_1',
-                                 before: '00000000',
-                                 after: '31das312',
-                                 commits: [{ message: "Message" }]
-                                )
-        expect(result).to be_persisted
-      end
-    end
-
-    it 'skips creating pipeline for refs without .gitlab-ci.yml' do
-      stub_ci_pipeline_yaml_file(nil)
-      result = service.execute(project, user,
-                               ref: 'refs/heads/0_1',
-                               before: '00000000',
-                               after: '31das312',
-                               commits: [{ message: 'Message' }]
-                              )
-      expect(result).to be_falsey
-      expect(Ci::Pipeline.count).to eq(0)
-    end
-
-    it 'fails commits if yaml is invalid' do
-      message = 'message'
-      allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message }
-      stub_ci_pipeline_yaml_file('invalid: file: file')
-      commits = [{ message: message }]
-      pipeline = service.execute(project, user,
-                                 ref: 'refs/tags/0_1',
-                                 before: '00000000',
-                                 after: '31das312',
-                                 commits: commits
-                                )
-      expect(pipeline).to be_persisted
-      expect(pipeline.builds.any?).to be false
-      expect(pipeline.status).to eq('failed')
-      expect(pipeline.yaml_errors).not_to be_nil
-    end
-
-    context 'when commit contains a [ci skip] directive' do
-      let(:message) { "some message[ci skip]" }
-      let(:messageFlip) { "some message[skip ci]" }
-      let(:capMessage) { "some message[CI SKIP]" }
-      let(:capMessageFlip) { "some message[SKIP CI]" }
-
-      before do
-        allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message }
-      end
-
-      it "skips builds creation if there is [ci skip] tag in commit message" do
-        commits = [{ message: message }]
-        pipeline = service.execute(project, user,
-                                   ref: 'refs/tags/0_1',
-                                   before: '00000000',
-                                   after: '31das312',
-                                   commits: commits
-                                  )
-
-        expect(pipeline).to be_persisted
-        expect(pipeline.builds.any?).to be false
-        expect(pipeline.status).to eq("skipped")
-      end
-
-      it "skips builds creation if there is [skip ci] tag in commit message" do
-        commits = [{ message: messageFlip }]
-        pipeline = service.execute(project, user,
-                                   ref: 'refs/tags/0_1',
-                                   before: '00000000',
-                                   after: '31das312',
-                                   commits: commits
-                                  )
-
-        expect(pipeline).to be_persisted
-        expect(pipeline.builds.any?).to be false
-        expect(pipeline.status).to eq("skipped")
-      end
-
-      it "skips builds creation if there is [CI SKIP] tag in commit message" do
-        commits = [{ message: capMessage }]
-        pipeline = service.execute(project, user,
-                                   ref: 'refs/tags/0_1',
-                                   before: '00000000',
-                                   after: '31das312',
-                                   commits: commits
-                                  )
-
-        expect(pipeline).to be_persisted
-        expect(pipeline.builds.any?).to be false
-        expect(pipeline.status).to eq("skipped")
-      end
-
-      it "skips builds creation if there is [SKIP CI] tag in commit message" do
-        commits = [{ message: capMessageFlip }]
-        pipeline = service.execute(project, user,
-                                   ref: 'refs/tags/0_1',
-                                   before: '00000000',
-                                   after: '31das312',
-                                   commits: commits
-                                  )
-
-        expect(pipeline).to be_persisted
-        expect(pipeline.builds.any?).to be false
-        expect(pipeline.status).to eq("skipped")
-      end
-
-      it "does not skips builds creation if there is no [ci skip] or [skip ci] tag in commit message" do
-        allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { "some message" }
-
-        commits = [{ message: "some message" }]
-        pipeline = service.execute(project, user,
-                                   ref: 'refs/tags/0_1',
-                                   before: '00000000',
-                                   after: '31das312',
-                                   commits: commits
-                                  )
-
-        expect(pipeline).to be_persisted
-        expect(pipeline.builds.first.name).to eq("staging")
-      end
-
-      it "skips builds creation if there is [ci skip] tag in commit message and yaml is invalid" do
-        stub_ci_pipeline_yaml_file('invalid: file: fiile')
-        commits = [{ message: message }]
-        pipeline = service.execute(project, user,
-                                   ref: 'refs/tags/0_1',
-                                   before: '00000000',
-                                   after: '31das312',
-                                   commits: commits
-                                  )
-        expect(pipeline).to be_persisted
-        expect(pipeline.builds.any?).to be false
-        expect(pipeline.status).to eq("skipped")
-        expect(pipeline.yaml_errors).to be_nil
-      end
-    end
-
-    it "skips build creation if there are already builds" do
-      allow_any_instance_of(Ci::Pipeline).to receive(:ci_yaml_file) { gitlab_ci_yaml }
-
-      commits = [{ message: "message" }]
-      pipeline = service.execute(project, user,
-                                 ref: 'refs/heads/master',
-                                 before: '00000000',
-                                 after: '31das312',
-                                 commits: commits
-                                )
-      expect(pipeline).to be_persisted
-      expect(pipeline.builds.count(:all)).to eq(2)
-
-      pipeline = service.execute(project, user,
-                                 ref: 'refs/heads/master',
-                                 before: '00000000',
-                                 after: '31das312',
-                                 commits: commits
-                                )
-      expect(pipeline).to be_persisted
-      expect(pipeline.builds.count(:all)).to eq(2)
-    end
-
-    it "creates commit with failed status if yaml is invalid" do
-      stub_ci_pipeline_yaml_file('invalid: file')
-
-      commits = [{ message: "some message" }]
-
-      pipeline = service.execute(project, user,
-                                 ref: 'refs/tags/0_1',
-                                 before: '00000000',
-                                 after: '31das312',
-                                 commits: commits
-                                )
-
-      expect(pipeline).to be_persisted
-      expect(pipeline.status).to eq("failed")
-      expect(pipeline.builds.any?).to be false
-    end
-
-    context 'when there are no jobs for this pipeline' do
-      before do
-        config = YAML.dump({ test: { script: 'ls', only: ['feature'] } })
-        stub_ci_pipeline_yaml_file(config)
-      end
-
-      it 'does not create a new pipeline' do
-        result = service.execute(project, user,
-                                 ref: 'refs/heads/master',
-                                 before: '00000000',
-                                 after: '31das312',
-                                 commits: [{ message: 'some msg' }])
-
-        expect(result).to be_falsey
-        expect(Ci::Build.all).to be_empty
-        expect(Ci::Pipeline.count).to eq(0)
-      end
-    end
-  end
-end
diff --git a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb
index 4da8146e3d6a41..520e906b21f357 100644
--- a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb
+++ b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb
@@ -110,19 +110,15 @@
 
     context 'properly handles multiple stages' do
       let(:ref) { mr_merge_if_green_enabled.source_branch }
-      let(:build) { create(:ci_build, pipeline: pipeline, ref: ref, name: 'build', stage: 'build') }
-      let(:test) { create(:ci_build, pipeline: pipeline, ref: ref, name: 'test', stage: 'test') }
+      let!(:build) { create(:ci_build, :created, pipeline: pipeline, ref: ref, name: 'build', stage: 'build') }
+      let!(:test) { create(:ci_build, :created, pipeline: pipeline, ref: ref, name: 'test', stage: 'test') }
+      let(:pipeline) { create(:ci_empty_pipeline, ref: mr_merge_if_green_enabled.source_branch, project: project) }
 
       before do
         # This behavior of MergeRequest: we instantiate a new object
         allow_any_instance_of(MergeRequest).to receive(:pipeline).and_wrap_original do
           Ci::Pipeline.find(pipeline.id)
         end
-
-        # We create test after the build
-        allow(pipeline).to receive(:create_next_builds).and_wrap_original do
-          test
-        end
       end
 
       it "doesn't merge if some stages failed" do
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index 7f803a06902b21..1d2cf7acddd2ca 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -53,7 +53,13 @@
       subject { PostReceive.new.perform(pwd(project), key_id, base64_changes) }
 
       context "creates a Ci::Pipeline for every change" do
-        before { stub_ci_pipeline_to_return_yaml_file }
+        before do
+          allow_any_instance_of(Ci::CreatePipelineService).to receive(:commit) do
+            OpenStruct.new(id: '123456')
+          end
+          allow_any_instance_of(Ci::CreatePipelineService).to receive(:branch?).and_return(true)
+          stub_ci_pipeline_to_return_yaml_file
+        end
 
         it { expect{ subject }.to change{ Ci::Pipeline.count }.by(2) }
       end
-- 
GitLab


From ecb3f1eb6c87ab40108a5d71a3287a205ab6fefb Mon Sep 17 00:00:00 2001
From: Christopher Bartz 
Date: Wed, 13 Apr 2016 14:17:42 +0200
Subject: [PATCH 130/153] Rename `markdown_preview` routes to
 `preview_markdown`

---
 CHANGELOG                                                 | 1 +
 app/assets/javascripts/dropzone_input.js                  | 2 +-
 .../{markdown_preview.js => preview_markdown.js}          | 4 ++--
 app/controllers/projects/wikis_controller.rb              | 2 +-
 app/controllers/projects_controller.rb                    | 2 +-
 app/views/layouts/project.html.haml                       | 6 +++---
 config/routes.rb                                          | 4 ++--
 spec/routing/project_routing_spec.rb                      | 8 ++++----
 8 files changed, 15 insertions(+), 14 deletions(-)
 rename app/assets/javascripts/{markdown_preview.js => preview_markdown.js} (98%)

diff --git a/CHANGELOG b/CHANGELOG
index b94d3b799142b3..041da1503d7a10 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -5,6 +5,7 @@ v 8.11.0 (unreleased)
   - Fix don't pass a local variable called `i` to a partial. !20510 (herminiotorres)
   - Fix rename `add_users_into_project` and `projects_ids`. !20512 (herminiotorres)
   - Fix the title of the toggle dropdown button. !5515 (herminiotorres)
+  - Rename `markdown_preview` routes to `preview_markdown`. (Christopher Bartz)
   - Improve diff performance by eliminating redundant checks for text blobs
   - Ensure that branch names containing escapable characters (e.g. %20) aren't unescaped indiscriminately. !5770 (ewiltshi)
   - Convert switch icon into icon font (ClemMakesApps)
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index 288cce04f87828..4a6fea929c758b 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -1,5 +1,5 @@
 
-/*= require markdown_preview */
+/*= require preview_markdown */
 
 (function() {
   this.DropzoneInput = (function() {
diff --git a/app/assets/javascripts/markdown_preview.js b/app/assets/javascripts/preview_markdown.js
similarity index 98%
rename from app/assets/javascripts/markdown_preview.js
rename to app/assets/javascripts/preview_markdown.js
index 18fc7bae09a6e5..5fd7579964024c 100644
--- a/app/assets/javascripts/markdown_preview.js
+++ b/app/assets/javascripts/preview_markdown.js
@@ -28,7 +28,7 @@
     };
 
     MarkdownPreview.prototype.renderMarkdown = function(text, success) {
-      if (!window.markdown_preview_path) {
+      if (!window.preview_markdown_path) {
         return;
       }
       if (text === this.ajaxCache.text) {
@@ -36,7 +36,7 @@
       }
       return $.ajax({
         type: 'POST',
-        url: window.markdown_preview_path,
+        url: window.preview_markdown_path,
         data: {
           text: text
         },
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index 607fe9c7fed02d..177ccf5eec9631 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -91,7 +91,7 @@ def destroy
     )
   end
 
-  def markdown_preview
+  def preview_markdown
     text = params[:text]
 
     ext = Gitlab::ReferenceExtractor.new(@project, current_user)
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 207f9d6a77fb09..47efbd4a93902e 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -238,7 +238,7 @@ def toggle_star
     }
   end
 
-  def markdown_preview
+  def preview_markdown
     text = params[:text]
 
     ext = Gitlab::ReferenceExtractor.new(@project, current_user)
diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml
index ee9c0366f2bdea..9fe94291db749d 100644
--- a/app/views/layouts/project.html.haml
+++ b/app/views/layouts/project.html.haml
@@ -6,13 +6,13 @@
 - content_for :scripts_body_top do
   - project = @target_project || @project
   - if @project_wiki && @page
-    - markdown_preview_path = namespace_project_wiki_markdown_preview_path(project.namespace, project, @page.slug)
+    - preview_markdown_path = namespace_project_wiki_preview_markdown_path(project.namespace, project, @page.slug)
   - else
-    - markdown_preview_path = markdown_preview_namespace_project_path(project.namespace, project)
+    - preview_markdown_path = preview_markdown_namespace_project_path(project.namespace, project)
   - if current_user
     :javascript
       window.project_uploads_path = "#{namespace_project_uploads_path project.namespace,project}";
-      window.markdown_preview_path = "#{markdown_preview_path}";
+      window.preview_markdown_path = "#{preview_markdown_path}";
 
 - content_for :scripts_body do
   = render "layouts/init_auto_complete" if current_user
diff --git a/config/routes.rb b/config/routes.rb
index 2f5f32d9e30a46..cc7b0fb5d93a69 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -471,7 +471,7 @@
         post :unarchive
         post :housekeeping
         post :toggle_star
-        post :markdown_preview
+        post :preview_markdown
         post :export
         post :remove_export
         post :generate_new_export
@@ -660,7 +660,7 @@
           get '/wikis/*id', to: 'wikis#show', as: 'wiki', constraints: WIKI_SLUG_ID
           delete '/wikis/*id', to: 'wikis#destroy', constraints: WIKI_SLUG_ID
           put '/wikis/*id', to: 'wikis#update', constraints: WIKI_SLUG_ID
-          post '/wikis/*id/markdown_preview', to: 'wikis#markdown_preview', constraints: WIKI_SLUG_ID, as: 'wiki_markdown_preview'
+          post '/wikis/*id/preview_markdown', to: 'wikis#preview_markdown', constraints: WIKI_SLUG_ID, as: 'wiki_preview_markdown'
         end
 
         resource :repository, only: [:create] do
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index b941e78f983539..77842057a10449 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -60,7 +60,7 @@
 #                  project GET    /:id(.:format)          projects#show
 #                          PUT    /:id(.:format)          projects#update
 #                          DELETE /:id(.:format)          projects#destroy
-# markdown_preview_project POST   /:id/markdown_preview(.:format) projects#markdown_preview
+# preview_markdown_project POST   /:id/preview_markdown(.:format) projects#preview_markdown
 describe ProjectsController, 'routing' do
   it 'to #create' do
     expect(post('/projects')).to route_to('projects#create')
@@ -91,9 +91,9 @@
     expect(delete('/gitlab/gitlabhq')).to route_to('projects#destroy', namespace_id: 'gitlab', id: 'gitlabhq')
   end
 
-  it 'to #markdown_preview' do
-    expect(post('/gitlab/gitlabhq/markdown_preview')).to(
-      route_to('projects#markdown_preview', namespace_id: 'gitlab', id: 'gitlabhq')
+  it 'to #preview_markdown' do
+    expect(post('/gitlab/gitlabhq/preview_markdown')).to(
+      route_to('projects#preview_markdown', namespace_id: 'gitlab', id: 'gitlabhq')
     )
   end
 end
-- 
GitLab


From c2a1011f529c21b8b571edc0daaf1f4875509e48 Mon Sep 17 00:00:00 2001
From: Stan Hu 
Date: Thu, 11 Aug 2016 08:45:14 -0700
Subject: [PATCH 131/153] Remove unused SpamReport model; this was renamed to
 SpamLog

---
 app/models/spam_report.rb | 5 -----
 1 file changed, 5 deletions(-)
 delete mode 100644 app/models/spam_report.rb

diff --git a/app/models/spam_report.rb b/app/models/spam_report.rb
deleted file mode 100644
index cdc7321b08e7cd..00000000000000
--- a/app/models/spam_report.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-class SpamReport < ActiveRecord::Base
-  belongs_to :user
-
-  validates :user, presence: true
-end
-- 
GitLab


From 32b579e8426094af4b7731d3e91597a377a7ba7e Mon Sep 17 00:00:00 2001
From: Douwe Maan 
Date: Thu, 11 Aug 2016 10:00:31 -0500
Subject: [PATCH 132/153] Show member roles to all users on members page

---
 CHANGELOG                                  |  1 +
 app/helpers/members_helper.rb              |  6 ---
 app/views/shared/members/_member.html.haml |  2 +-
 features/explore/groups.feature            | 25 -----------
 features/steps/explore/groups.rb           |  4 --
 spec/helpers/members_helper_spec.rb        | 48 ----------------------
 6 files changed, 2 insertions(+), 84 deletions(-)

diff --git a/CHANGELOG b/CHANGELOG
index d248639296f48a..4597c83ecdf268 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -22,6 +22,7 @@ v 8.11.0 (unreleased)
   - Cache highlighted diff lines for merge requests
   - Pre-create all builds for a Pipeline when the new Pipeline is created !5295
   - Fix of 'Commits being passed to custom hooks are already reachable when using the UI'
+  - Show member roles to all users on members page
   - Fix awardable button mutuality loading spinners (ClemMakesApps)
   - Add support for using RequestStore within Sidekiq tasks via SIDEKIQ_REQUEST_STORE env variable
   - Optimize maximum user access level lookup in loading of notes
diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb
index ec106418f2dc4f..877c77050bed09 100644
--- a/app/helpers/members_helper.rb
+++ b/app/helpers/members_helper.rb
@@ -6,12 +6,6 @@ def action_member_permission(action, member)
     "#{action}_#{member.type.underscore}".to_sym
   end
 
-  def default_show_roles(member)
-    can?(current_user, action_member_permission(:update, member), member) ||
-      can?(current_user, action_member_permission(:destroy, member), member) ||
-      can?(current_user, action_member_permission(:admin, member), member.source)
-  end
-
   def remove_member_message(member, user: nil)
     user = current_user if defined?(current_user)
 
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index 5ae485f36baa08..fc6e206d0821c0 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -1,4 +1,4 @@
-- show_roles = local_assigns.fetch(:show_roles, default_show_roles(member))
+- show_roles = local_assigns.fetch(:show_roles, true)
 - show_controls = local_assigns.fetch(:show_controls, true)
 - user = member.user
 
diff --git a/features/explore/groups.feature b/features/explore/groups.feature
index 5fc9b1356010c2..9eacbe0b25e8bb 100644
--- a/features/explore/groups.feature
+++ b/features/explore/groups.feature
@@ -24,14 +24,6 @@ Feature: Explore Groups
     Then I should see project "Internal" items
     And I should not see project "Enterprise" items
 
-  Scenario: I should see group's members as user
-    Given group "TestGroup" has internal project "Internal"
-    And "John Doe" is owner of group "TestGroup"
-    When I sign in as a user
-    And I visit group "TestGroup" members page
-    Then I should see group member "John Doe"
-    And I should not see member roles
-
   Scenario: I should see group with private, internal and public projects as visitor
     Given group "TestGroup" has internal project "Internal"
     Given group "TestGroup" has public project "Community"
@@ -56,14 +48,6 @@ Feature: Explore Groups
     And I should not see project "Internal" items
     And I should not see project "Enterprise" items
 
-  Scenario: I should see group's members as visitor
-    Given group "TestGroup" has internal project "Internal"
-    Given group "TestGroup" has public project "Community"
-    And "John Doe" is owner of group "TestGroup"
-    When I visit group "TestGroup" members page
-    Then I should see group member "John Doe"
-    And I should not see member roles
-
   Scenario: I should see group with private, internal and public projects as user
     Given group "TestGroup" has internal project "Internal"
     Given group "TestGroup" has public project "Community"
@@ -91,15 +75,6 @@ Feature: Explore Groups
     And I should see project "Internal" items
     And I should not see project "Enterprise" items
 
-  Scenario: I should see group's members as user
-    Given group "TestGroup" has internal project "Internal"
-    Given group "TestGroup" has public project "Community"
-    And "John Doe" is owner of group "TestGroup"
-    When I sign in as a user
-    And I visit group "TestGroup" members page
-    Then I should see group member "John Doe"
-    And I should not see member roles
-
   Scenario: I should see group with public project in public groups area
     Given group "TestGroup" has public project "Community"
     When I visit the public groups area
diff --git a/features/steps/explore/groups.rb b/features/steps/explore/groups.rb
index 87f32e70d59257..409bf0cb4167cb 100644
--- a/features/steps/explore/groups.rb
+++ b/features/steps/explore/groups.rb
@@ -62,10 +62,6 @@ class Spinach::Features::ExploreGroups < Spinach::FeatureSteps
     expect(page).to have_content "John Doe"
   end
 
-  step 'I should not see member roles' do
-    expect(body).not_to match(%r{owner|developer|reporter|guest}i)
-  end
-
   protected
 
   def group_has_project(groupname, projectname, visibility_level)
diff --git a/spec/helpers/members_helper_spec.rb b/spec/helpers/members_helper_spec.rb
index f75fdb739f6891..7998209b7b00e7 100644
--- a/spec/helpers/members_helper_spec.rb
+++ b/spec/helpers/members_helper_spec.rb
@@ -9,54 +9,6 @@
     it { expect(action_member_permission(:admin, group_member)).to eq :admin_group_member }
   end
 
-  describe '#default_show_roles' do
-    let(:user) { double }
-    let(:member) { build(:project_member) }
-
-    before do
-      allow(helper).to receive(:current_user).and_return(user)
-      allow(helper).to receive(:can?).with(user, :update_project_member, member).and_return(false)
-      allow(helper).to receive(:can?).with(user, :destroy_project_member, member).and_return(false)
-      allow(helper).to receive(:can?).with(user, :admin_project_member, member.source).and_return(false)
-    end
-
-    context 'when the current cannot update, destroy or admin the passed member' do
-      it 'returns false' do
-        expect(helper.default_show_roles(member)).to be_falsy
-      end
-    end
-
-    context 'when the current can update the passed member' do
-      before do
-        allow(helper).to receive(:can?).with(user, :update_project_member, member).and_return(true)
-      end
-
-      it 'returns true' do
-        expect(helper.default_show_roles(member)).to be_truthy
-      end
-    end
-
-    context 'when the current can destroy the passed member' do
-      before do
-        allow(helper).to receive(:can?).with(user, :destroy_project_member, member).and_return(true)
-      end
-
-      it 'returns true' do
-        expect(helper.default_show_roles(member)).to be_truthy
-      end
-    end
-
-    context 'when the current can admin the passed member source' do
-      before do
-        allow(helper).to receive(:can?).with(user, :admin_project_member, member.source).and_return(true)
-      end
-
-      it 'returns true' do
-        expect(helper.default_show_roles(member)).to be_truthy
-      end
-    end
-  end
-
   describe '#remove_member_message' do
     let(:requester) { build(:user) }
     let(:project) { create(:project) }
-- 
GitLab


From 6109daf480327581b6e2dcdfffe90464be6c7796 Mon Sep 17 00:00:00 2001
From: Scott Le 
Date: Thu, 28 Jul 2016 11:04:57 +0700
Subject: [PATCH 133/153] api for generating new merge request

DRY code + fix rubocop

Add more test cases

Append to changelog

DRY changes list

find_url service for merge_requests

use GET for getting merge request links

remove files

rename to get_url_service

reduce loop

add test case for cross project

refactor tiny thing

update changelog
---
 CHANGELOG                                     |   1 +
 app/models/merge_request.rb                   |   1 +
 .../merge_requests/get_urls_service.rb        |  52 +++++++++
 lib/api/internal.rb                           |   4 +
 lib/gitlab/changes_list.rb                    |  25 +++++
 lib/gitlab/checks/change_access.rb            |  24 +----
 lib/gitlab/git.rb                             |  18 ++++
 lib/gitlab/git_access.rb                      |   4 +-
 spec/lib/gitlab/changes_list_spec.rb          |  30 ++++++
 spec/requests/api/internal_spec.rb            |  18 ++++
 .../merge_requests/get_urls_service_spec.rb   | 100 ++++++++++++++++++
 11 files changed, 254 insertions(+), 23 deletions(-)
 create mode 100644 app/services/merge_requests/get_urls_service.rb
 create mode 100644 lib/gitlab/changes_list.rb
 create mode 100644 spec/lib/gitlab/changes_list_spec.rb
 create mode 100644 spec/services/merge_requests/get_urls_service_spec.rb

diff --git a/CHANGELOG b/CHANGELOG
index 282e4b9b4498b5..2f527bde05f220 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -102,6 +102,7 @@ v 8.11.0 (unreleased)
   - Fix importing GitLab projects with an invalid MR source project
   - Sort folders with submodules in Files view !5521
   - Each `File::exists?` replaced to `File::exist?` because of deprecate since ruby version 2.2.0
+  - Print urls to create (or view) merge requests after git push !5542 (Scott Le)
 
 v 8.10.5
   - Add a data migration to fix some missing timestamps in the members table. !5670
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index b1fb3ce5d6973c..f6d0d0c98f594e 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -104,6 +104,7 @@ class MergeRequest < ActiveRecord::Base
   scope :from_project, ->(project) { where(source_project_id: project.id) }
   scope :merged, -> { with_state(:merged) }
   scope :closed_and_merged, -> { with_states(:closed, :merged) }
+  scope :from_source_branches, ->(branches) { where(source_branch: branches) }
 
   scope :join_project, -> { joins(:target_project) }
   scope :references_project, -> { references(:target_project) }
diff --git a/app/services/merge_requests/get_urls_service.rb b/app/services/merge_requests/get_urls_service.rb
new file mode 100644
index 00000000000000..501fd135e16624
--- /dev/null
+++ b/app/services/merge_requests/get_urls_service.rb
@@ -0,0 +1,52 @@
+module MergeRequests
+  class GetUrlsService < BaseService
+    attr_reader :project
+
+    def initialize(project)
+      @project = project
+    end
+
+    def execute(changes)
+      branches = get_branches(changes)
+      merge_requests_map = opened_merge_requests_from_source_branches(branches)
+      branches.map do |branch|
+        existing_merge_request = merge_requests_map[branch]
+        if existing_merge_request
+          url_for_existing_merge_request(existing_merge_request)
+        else
+          url_for_new_merge_request(branch)
+        end
+      end
+    end
+
+    private
+
+    def opened_merge_requests_from_source_branches(branches)
+      merge_requests = MergeRequest.from_project(project).opened.from_source_branches(branches)
+      merge_requests.inject({}) do |hash, mr|
+        hash[mr.source_branch] = mr
+        hash
+      end
+    end
+
+    def get_branches(changes)
+      changes_list = Gitlab::ChangesList.new(changes)
+      changes_list.map do |change|
+        next unless Gitlab::Git.branch_ref?(change[:ref])
+        Gitlab::Git.branch_name(change[:ref])
+      end.compact
+    end
+
+    def url_for_new_merge_request(branch_name)
+      merge_request_params = { source_branch: branch_name }
+      url = Gitlab::Routing.url_helpers.new_namespace_project_merge_request_url(project.namespace, project, merge_request: merge_request_params)
+      { branch_name: branch_name, url: url, new_merge_request: true }
+    end
+
+    def url_for_existing_merge_request(merge_request)
+      target_project = merge_request.target_project
+      url = Gitlab::Routing.url_helpers.namespace_project_merge_request_url(target_project.namespace, target_project, merge_request)
+      { branch_name: merge_request.source_branch, url: url, new_merge_request: false }
+    end
+  end
+end
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index 959b700de782ae..d8e9ac406c4c40 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -74,6 +74,10 @@ def project
         response
       end
 
+      get "/merge_request_urls" do
+        ::MergeRequests::GetUrlsService.new(project).execute(params[:changes])
+      end
+
       #
       # Discover user by ssh key
       #
diff --git a/lib/gitlab/changes_list.rb b/lib/gitlab/changes_list.rb
new file mode 100644
index 00000000000000..95308aca95f788
--- /dev/null
+++ b/lib/gitlab/changes_list.rb
@@ -0,0 +1,25 @@
+module Gitlab
+  class ChangesList
+    include Enumerable
+
+    attr_reader :raw_changes
+
+    def initialize(changes)
+      @raw_changes = changes.kind_of?(String) ? changes.lines : changes
+    end
+
+    def each(&block)
+      changes.each(&block)
+    end
+
+    def changes
+      @changes ||= begin
+        @raw_changes.map do |change|
+          next if change.blank?
+          oldrev, newrev, ref = change.strip.split(' ')
+          { oldrev: oldrev, newrev: newrev, ref: ref }
+        end.compact
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/checks/change_access.rb b/lib/gitlab/checks/change_access.rb
index 5551fac4b8bdf6..52f117e963b3ba 100644
--- a/lib/gitlab/checks/change_access.rb
+++ b/lib/gitlab/checks/change_access.rb
@@ -4,8 +4,8 @@ class ChangeAccess
       attr_reader :user_access, :project
 
       def initialize(change, user_access:, project:)
-        @oldrev, @newrev, @ref = change.split(' ')
-        @branch_name = branch_name(@ref)
+        @oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref)
+        @branch_name = Gitlab::Git.branch_name(@ref)
         @user_access = user_access
         @project = project
       end
@@ -47,7 +47,7 @@ def protected_branch_checks
       end
 
       def tag_checks
-        tag_ref = tag_name(@ref)
+        tag_ref = Gitlab::Git.tag_name(@ref)
 
         if tag_ref && protected_tag?(tag_ref) && user_access.cannot_do_action?(:admin_project)
           "You are not allowed to change existing tags on this project."
@@ -73,24 +73,6 @@ def forced_push?
       def matching_merge_request?
         Checks::MatchingMergeRequest.new(@newrev, @branch_name, @project).match?
       end
-
-      def branch_name(ref)
-        ref = @ref.to_s
-        if Gitlab::Git.branch_ref?(ref)
-          Gitlab::Git.ref_name(ref)
-        else
-          nil
-        end
-      end
-
-      def tag_name(ref)
-        ref = @ref.to_s
-        if Gitlab::Git.tag_ref?(ref)
-          Gitlab::Git.ref_name(ref)
-        else
-          nil
-        end
-      end
     end
   end
 end
diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb
index 191bea86ac378d..7584efe4fa864d 100644
--- a/lib/gitlab/git.rb
+++ b/lib/gitlab/git.rb
@@ -9,6 +9,24 @@ def ref_name(ref)
         ref.gsub(/\Arefs\/(tags|heads)\//, '')
       end
 
+      def branch_name(ref)
+        ref = ref.to_s
+        if self.branch_ref?(ref)
+          self.ref_name(ref)
+        else
+          nil
+        end
+      end
+
+      def tag_name(ref)
+        ref = ref.to_s
+        if self.tag_ref?(ref)
+          self.ref_name(ref)
+        else
+          nil
+        end
+      end
+
       def tag_ref?(ref)
         ref.start_with?(TAG_REF_PREFIX)
       end
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index 5bc70f530d2251..1882eb8d0508c3 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -76,10 +76,10 @@ def user_push_access_check(changes)
         return build_status_object(false, "A repository for this project does not exist yet.")
       end
 
-      changes = changes.lines if changes.kind_of?(String)
+      changes_list = Gitlab::ChangesList.new(changes)
 
       # Iterate over all changes to find if user allowed all of them to be applied
-      changes.map(&:strip).reject(&:blank?).each do |change|
+      changes_list.each do |change|
         status = change_access_check(change)
         unless status.allowed?
           # If user does not have access to make at least one change - cancel all push
diff --git a/spec/lib/gitlab/changes_list_spec.rb b/spec/lib/gitlab/changes_list_spec.rb
new file mode 100644
index 00000000000000..69d86144e321fb
--- /dev/null
+++ b/spec/lib/gitlab/changes_list_spec.rb
@@ -0,0 +1,30 @@
+require "spec_helper"
+
+describe Gitlab::ChangesList do
+  let(:valid_changes_string) { "\n000000 570e7b2 refs/heads/my_branch\nd14d6c 6fd24d refs/heads/master" }
+  let(:invalid_changes) { 1 }
+
+  context 'when changes is a valid string' do
+    let(:changes_list) { Gitlab::ChangesList.new(valid_changes_string) }
+
+    it 'splits elements by newline character' do
+      expect(changes_list).to contain_exactly({
+        oldrev: "000000",
+        newrev: "570e7b2",
+        ref: "refs/heads/my_branch"
+      }, {
+        oldrev: "d14d6c",
+        newrev: "6fd24d",
+        ref: "refs/heads/master"
+      })
+    end
+
+    it 'behaves like a list' do
+      expect(changes_list.first).to eq({
+        oldrev: "000000",
+        newrev: "570e7b2",
+        ref: "refs/heads/my_branch"
+      })
+    end
+  end
+end
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index f6f85d6e95ede6..be52f88831f707 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -275,6 +275,24 @@
     end
   end
 
+  describe 'GET /internal/merge_request_urls' do
+    let(:repo_name) { "#{project.namespace.name}/#{project.path}" }
+    let(:changes) { URI.escape("#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/new_branch") }
+
+    before do
+      project.team << [user, :developer]
+      get api("/internal/merge_request_urls?project=#{repo_name}&changes=#{changes}"), secret_token: secret_token
+    end
+
+    it 'returns link to create new merge request' do
+      expect(json_response).to match [{
+        "branch_name" => "new_branch",
+        "url" => "http://localhost/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch",
+        "new_merge_request" => true
+      }]
+    end
+  end
+
   def pull(key, project, protocol = 'ssh')
     post(
       api("/internal/allowed"),
diff --git a/spec/services/merge_requests/get_urls_service_spec.rb b/spec/services/merge_requests/get_urls_service_spec.rb
new file mode 100644
index 00000000000000..ec26770c3ebc61
--- /dev/null
+++ b/spec/services/merge_requests/get_urls_service_spec.rb
@@ -0,0 +1,100 @@
+require "spec_helper"
+
+describe MergeRequests::GetUrlsService do
+  let(:project) { create(:project, :public) }
+  let(:service) { MergeRequests::GetUrlsService.new(project) }
+  let(:source_branch) { "my_branch" }
+  let(:new_merge_request_url) { "http://localhost/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=#{source_branch}" }
+  let(:show_merge_request_url) { "http://localhost/#{project.namespace.name}/#{project.path}/merge_requests/#{merge_request.iid}" }
+  let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" }
+  let(:existing_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" }
+
+  describe "#execute" do
+    shared_examples 'new_merge_request_link' do
+      it 'returns url to create new merge request' do
+        result = service.execute(changes)
+        expect(result).to match([{
+          branch_name: source_branch,
+          url: new_merge_request_url,
+          new_merge_request: true
+        }])
+      end
+    end
+
+    shared_examples 'show_merge_request_url' do
+      it 'returns url to view merge request' do
+        result = service.execute(changes)
+        expect(result).to match([{
+          branch_name: source_branch,
+          url: show_merge_request_url,
+          new_merge_request: false
+        }])
+      end
+    end
+
+    context 'pushing one completely new branch' do
+      let(:changes) { new_branch_changes }
+      it_behaves_like 'new_merge_request_link'
+    end
+
+    context 'pushing to existing branch but no merge request' do
+      let(:changes) { existing_branch_changes }
+      it_behaves_like 'new_merge_request_link'
+    end
+
+    context 'pushing to existing branch and merge request opened' do
+      let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch) }
+      let(:changes) { existing_branch_changes }
+      it_behaves_like 'show_merge_request_url'
+    end
+
+    context 'pushing to existing branch and merge request is reopened' do
+      let!(:merge_request) { create(:merge_request, :reopened, source_project: project, source_branch: source_branch) }
+      let(:changes) { existing_branch_changes }
+      it_behaves_like 'show_merge_request_url'
+    end
+
+    context 'pushing to existing branch from forked project' do
+      let(:user) { create(:user) }
+      let!(:forked_project) { Projects::ForkService.new(project, user).execute }
+      let!(:merge_request) { create(:merge_request, source_project: forked_project, target_project: project, source_branch: source_branch) }
+      let(:changes) { existing_branch_changes }
+      # Source project is now the forked one
+      let(:service) { MergeRequests::GetUrlsService.new(forked_project) }
+      it_behaves_like 'show_merge_request_url'
+    end
+
+    context 'pushing to existing branch and merge request is closed' do
+      let!(:merge_request) { create(:merge_request, :closed, source_project: project, source_branch: source_branch) }
+      let(:changes) { existing_branch_changes }
+      it_behaves_like 'new_merge_request_link'
+    end
+
+    context 'pushing to existing branch and merge request is merged' do
+      let!(:merge_request) { create(:merge_request, :merged, source_project: project, source_branch: source_branch) }
+      let(:changes) { existing_branch_changes }
+      it_behaves_like 'new_merge_request_link'
+    end
+
+    context 'pushing new branch and existing branch (with merge request created) at once' do
+      let!(:merge_request) { create(:merge_request, source_project: project, source_branch: "existing_branch") }
+      let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/new_branch" }
+      let(:existing_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/existing_branch" }
+      let(:changes) { "#{new_branch_changes}\n#{existing_branch_changes}" }
+      let(:new_merge_request_url) { "http://localhost/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch" }
+
+      it 'returns 2 urls for both creating new and showing merge request' do
+        result = service.execute(changes)
+        expect(result).to match([{
+          branch_name: "new_branch",
+          url: new_merge_request_url,
+          new_merge_request: true
+        }, {
+          branch_name: "existing_branch",
+          url: show_merge_request_url,
+          new_merge_request: false
+        }])
+      end
+    end
+  end
+end
-- 
GitLab


From 790dd898692e58a05fdac7192ea50048dcceee79 Mon Sep 17 00:00:00 2001
From: "Z.J. van de Weg" 
Date: Mon, 27 Jun 2016 16:10:50 +0200
Subject: [PATCH 134/153] Default build tests ruby 2.3.1

Ruby 2.1.8 is used only on master for now, as this would give
confidance in case this commit has to be reverted.
---
 .gitlab-ci.yml | 80 +++++++++++++++++++++++++-------------------------
 .ruby-version  |  2 +-
 2 files changed, 41 insertions(+), 41 deletions(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 8450d43fc0fbd8..d89a0875440a92 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,7 +1,7 @@
-image: "ruby:2.1"
+image: "ruby:2.3"
 
 cache:
-  key: "ruby21"
+  key: "ruby-23"
   paths:
   - vendor/apt
   - vendor/ruby
@@ -138,57 +138,57 @@ spinach 7 10: *spinach-knapsack
 spinach 8 10: *spinach-knapsack
 spinach 9 10: *spinach-knapsack
 
-# Execute all testing suites against Ruby 2.3
-.ruby-23: &ruby-23
-  image: "ruby:2.3"
+# Execute all testing suites against Ruby 2.1
+.ruby-21: &ruby-21
+  image: "ruby:2.1"
   <<: *use-db
   only:
     - master
   cache:
-    key: "ruby-23"
+    key: "ruby21"
     paths:
       - vendor/apt
       - vendor/ruby
 
-.rspec-knapsack-ruby23: &rspec-knapsack-ruby23
+.rspec-knapsack-ruby21: &rspec-knapsack-ruby21
   <<: *rspec-knapsack
-  <<: *ruby-23
+  <<: *ruby-21
 
-.spinach-knapsack-ruby23: &spinach-knapsack-ruby23
+.spinach-knapsack-ruby23: &spinach-knapsack-ruby21
   <<: *spinach-knapsack
-  <<: *ruby-23
+  <<: *ruby-21
 
-rspec 0 20 ruby23: *rspec-knapsack-ruby23
-rspec 1 20 ruby23: *rspec-knapsack-ruby23
-rspec 2 20 ruby23: *rspec-knapsack-ruby23
-rspec 3 20 ruby23: *rspec-knapsack-ruby23
-rspec 4 20 ruby23: *rspec-knapsack-ruby23
-rspec 5 20 ruby23: *rspec-knapsack-ruby23
-rspec 6 20 ruby23: *rspec-knapsack-ruby23
-rspec 7 20 ruby23: *rspec-knapsack-ruby23
-rspec 8 20 ruby23: *rspec-knapsack-ruby23
-rspec 9 20 ruby23: *rspec-knapsack-ruby23
-rspec 10 20 ruby23: *rspec-knapsack-ruby23
-rspec 11 20 ruby23: *rspec-knapsack-ruby23
-rspec 12 20 ruby23: *rspec-knapsack-ruby23
-rspec 13 20 ruby23: *rspec-knapsack-ruby23
-rspec 14 20 ruby23: *rspec-knapsack-ruby23
-rspec 15 20 ruby23: *rspec-knapsack-ruby23
-rspec 16 20 ruby23: *rspec-knapsack-ruby23
-rspec 17 20 ruby23: *rspec-knapsack-ruby23
-rspec 18 20 ruby23: *rspec-knapsack-ruby23
-rspec 19 20 ruby23: *rspec-knapsack-ruby23
+rspec 0 20 ruby21: *rspec-knapsack-ruby21
+rspec 1 20 ruby21: *rspec-knapsack-ruby21
+rspec 2 20 ruby21: *rspec-knapsack-ruby21
+rspec 3 20 ruby21: *rspec-knapsack-ruby21
+rspec 4 20 ruby21: *rspec-knapsack-ruby21
+rspec 5 20 ruby21: *rspec-knapsack-ruby21
+rspec 6 20 ruby21: *rspec-knapsack-ruby21
+rspec 7 20 ruby21: *rspec-knapsack-ruby21
+rspec 8 20 ruby21: *rspec-knapsack-ruby21
+rspec 9 20 ruby21: *rspec-knapsack-ruby21
+rspec 10 20 ruby21: *rspec-knapsack-ruby21
+rspec 11 20 ruby21: *rspec-knapsack-ruby21
+rspec 12 20 ruby21: *rspec-knapsack-ruby21
+rspec 13 20 ruby21: *rspec-knapsack-ruby21
+rspec 14 20 ruby21: *rspec-knapsack-ruby21
+rspec 15 20 ruby21: *rspec-knapsack-ruby21
+rspec 16 20 ruby21: *rspec-knapsack-ruby21
+rspec 17 20 ruby21: *rspec-knapsack-ruby21
+rspec 18 20 ruby21: *rspec-knapsack-ruby21
+rspec 19 20 ruby21: *rspec-knapsack-ruby21
 
-spinach 0 10 ruby23: *spinach-knapsack-ruby23
-spinach 1 10 ruby23: *spinach-knapsack-ruby23
-spinach 2 10 ruby23: *spinach-knapsack-ruby23
-spinach 3 10 ruby23: *spinach-knapsack-ruby23
-spinach 4 10 ruby23: *spinach-knapsack-ruby23
-spinach 5 10 ruby23: *spinach-knapsack-ruby23
-spinach 6 10 ruby23: *spinach-knapsack-ruby23
-spinach 7 10 ruby23: *spinach-knapsack-ruby23
-spinach 8 10 ruby23: *spinach-knapsack-ruby23
-spinach 9 10 ruby23: *spinach-knapsack-ruby23
+spinach 0 10 ruby21: *spinach-knapsack-ruby21
+spinach 1 10 ruby21: *spinach-knapsack-ruby21
+spinach 2 10 ruby21: *spinach-knapsack-ruby21
+spinach 3 10 ruby21: *spinach-knapsack-ruby21
+spinach 4 10 ruby21: *spinach-knapsack-ruby21
+spinach 5 10 ruby21: *spinach-knapsack-ruby21
+spinach 6 10 ruby21: *spinach-knapsack-ruby21
+spinach 7 10 ruby21: *spinach-knapsack-ruby21
+spinach 8 10 ruby21: *spinach-knapsack-ruby21
+spinach 9 10 ruby21: *spinach-knapsack-ruby21
 
 # Other generic tests
 
diff --git a/.ruby-version b/.ruby-version
index ebf14b46981c41..2bf1c1ccf363ac 100644
--- a/.ruby-version
+++ b/.ruby-version
@@ -1 +1 @@
-2.1.8
+2.3.1
-- 
GitLab


From 39103b1437b65b9e516649153d16c31226396989 Mon Sep 17 00:00:00 2001
From: Connor Shea 
Date: Mon, 27 Jun 2016 09:34:17 -0600
Subject: [PATCH 135/153] Update docs to reference Ruby 2.3.1

---
 doc/install/installation.md | 13 ++++++-------
 1 file changed, 6 insertions(+), 7 deletions(-)

diff --git a/doc/install/installation.md b/doc/install/installation.md
index c044d0880d3e40..118d1fc3966b9a 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -108,8 +108,7 @@ Then select 'Internet Site' and press enter to confirm the hostname.
 
 ## 2. Ruby
 
-_**Note:** The current supported Ruby version is 2.1.x. Ruby 2.2 and 2.3 are
-currently not supported._
+_**Note:** The current supported Ruby version is 2.3.x.
 
 The use of Ruby version managers such as [RVM], [rbenv] or [chruby] with GitLab
 in production, frequently leads to hard to diagnose problems. For example,
@@ -124,9 +123,9 @@ Remove the old Ruby 1.8 if present:
 Download Ruby and compile it:
 
     mkdir /tmp/ruby && cd /tmp/ruby
-    curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.1/ruby-2.1.8.tar.gz
-    echo 'c7e50159357afd87b13dc5eaf4ac486a70011149  ruby-2.1.8.tar.gz' | shasum -c - && tar xzf ruby-2.1.8.tar.gz
-    cd ruby-2.1.8
+    curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.1.tar.gz
+    echo 'c39b4001f7acb4e334cb60a0f4df72d434bef711  ruby-2.3.1.tar.gz' | shasum -c - && tar xzf ruby-2.3.1.tar.gz
+    cd ruby-2.3.1
     ./configure --disable-install-rdoc
     make
     sudo make install
@@ -591,13 +590,13 @@ for the changes to take effect.
 If you'd like to connect to a Redis server on a non-standard port or on a different host, you can configure its connection string via the `config/resque.yml` file.
 
     # example
-    production: 
+    production:
       url: redis://redis.example.tld:6379
 
 If you want to connect the Redis server via socket, then use the "unix:" URL scheme and the path to the Redis socket file in the `config/resque.yml` file.
 
     # example
-    production: 
+    production:
       url: unix:/path/to/redis/socket
 
 ### Custom SSH Connection
-- 
GitLab


From 100df6c3f40d053983e8adf0d7e9e162fceba41a Mon Sep 17 00:00:00 2001
From: "Z.J. van de Weg" 
Date: Mon, 27 Jun 2016 17:54:19 +0200
Subject: [PATCH 136/153] Update gitlab ci tests to test ruby 2.1 now too

---
 .gitlab-ci.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index d89a0875440a92..2a63ee15af0bfc 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -154,7 +154,7 @@ spinach 9 10: *spinach-knapsack
   <<: *rspec-knapsack
   <<: *ruby-21
 
-.spinach-knapsack-ruby23: &spinach-knapsack-ruby21
+.spinach-knapsack-ruby21: &spinach-knapsack-ruby21
   <<: *spinach-knapsack
   <<: *ruby-21
 
-- 
GitLab


From 9f9e38fff5fc682f05286fe7e88d8e688fa3e5b3 Mon Sep 17 00:00:00 2001
From: Connor Shea 
Date: Tue, 28 Jun 2016 09:34:22 -0600
Subject: [PATCH 137/153] Update currently supported Ruby version info.

---
 CHANGELOG                   | 2 ++
 doc/install/installation.md | 2 +-
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG b/CHANGELOG
index 5067304f561c64..efd2d164dbd300 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -174,6 +174,8 @@ v 8.10.0
   - Add an API for downloading latest successful build from a particular branch or tag. !5347
   - Avoid data-integrity issue when cleaning up repository archive cache.
   - Add link to profile to commit avatar. !5163 (winniehell)
+  - Remove pinTo from Flash and make inline flash messages look nicer !4854 (winniehell)
+  - Update to Ruby 2.3.1. !4948
   - Wrap code blocks on Activies and Todos page. !4783 (winniehell)
   - Align flash messages with left side of page content. !4959 (winniehell)
   - Display tooltip for "Copy to Clipboard" button. !5164 (winniehell)
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 118d1fc3966b9a..eb9606934cd873 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -108,7 +108,7 @@ Then select 'Internet Site' and press enter to confirm the hostname.
 
 ## 2. Ruby
 
-_**Note:** The current supported Ruby version is 2.3.x.
+_**Note:** The current supported Ruby versions are 2.1.x and 2.3.x. 2.3.x is preferred, and support for 2.1.x will be dropped in the future.
 
 The use of Ruby version managers such as [RVM], [rbenv] or [chruby] with GitLab
 in production, frequently leads to hard to diagnose problems. For example,
-- 
GitLab


From 3b973d1898eeb660f85d70affdf0e933328f985d Mon Sep 17 00:00:00 2001
From: "Z.J. van de Weg" 
Date: Thu, 11 Aug 2016 18:54:14 +0200
Subject: [PATCH 138/153] Target 8.11 for CHANGELOG entry Ruby 2.3

---
 CHANGELOG | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/CHANGELOG b/CHANGELOG
index efd2d164dbd300..8cefaf5d70ab92 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -6,6 +6,7 @@ v 8.11.0 (unreleased)
   - Fix rename `add_users_into_project` and `projects_ids`. !20512 (herminiotorres)
   - Fix the title of the toggle dropdown button. !5515 (herminiotorres)
   - Rename `markdown_preview` routes to `preview_markdown`. (Christopher Bartz)
+  - Update to Ruby 2.3.1. !4948
   - Improve diff performance by eliminating redundant checks for text blobs
   - Ensure that branch names containing escapable characters (e.g. %20) aren't unescaped indiscriminately. !5770 (ewiltshi)
   - Convert switch icon into icon font (ClemMakesApps)
@@ -174,8 +175,6 @@ v 8.10.0
   - Add an API for downloading latest successful build from a particular branch or tag. !5347
   - Avoid data-integrity issue when cleaning up repository archive cache.
   - Add link to profile to commit avatar. !5163 (winniehell)
-  - Remove pinTo from Flash and make inline flash messages look nicer !4854 (winniehell)
-  - Update to Ruby 2.3.1. !4948
   - Wrap code blocks on Activies and Todos page. !4783 (winniehell)
   - Align flash messages with left side of page content. !4959 (winniehell)
   - Display tooltip for "Copy to Clipboard" button. !5164 (winniehell)
-- 
GitLab


From d983c5bd4671d989edf3741d0db0a54dcef9c3b6 Mon Sep 17 00:00:00 2001
From: Kamil Trzcinski 
Date: Thu, 11 Aug 2016 18:37:36 +0200
Subject: [PATCH 139/153] Verify the pipeline status after executing events on
 builds

---
 app/models/ci/pipeline.rb       |  3 ++-
 app/models/commit_status.rb     |  6 +++++
 spec/models/ci/pipeline_spec.rb | 45 +++++++++++++++++++++++++++++++++
 3 files changed, 53 insertions(+), 1 deletion(-)

diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 718fe3290c1a17..8de799d60885fe 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -185,6 +185,7 @@ def notes
 
     def process!
       Ci::ProcessPipelineService.new(project, user).execute(self)
+
       reload_status!
     end
 
@@ -195,7 +196,7 @@ def predefined_variables
     end
 
     def reload_status!
-      statuses.reload
+      reload
       self.status =
         if yaml_errors.blank?
           statuses.latest.status || 'skipped'
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 20713314a252ad..3ab44461179065 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -76,6 +76,12 @@ class CommitStatus < ActiveRecord::Base
 
       commit_status.pipeline.process! if commit_status.pipeline
     end
+
+    around_transition any => [:pending, :running] do |commit_status, block|
+      block.call
+
+      commit_status.pipeline.reload_status! if commit_status.pipeline
+    end
   end
 
   delegate :sha, :short_sha, to: :pipeline
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index fdb579ab45c871..9a9720cfbfc546 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -254,4 +254,49 @@
       end
     end
   end
+
+  describe '#status' do
+    let!(:build) { create(:ci_build, :created, pipeline: pipeline, name: 'test') }
+
+    subject { pipeline.reload.status }
+
+    context 'on queuing' do
+      before { build.queue }
+
+      it { is_expected.to eq('pending') }
+    end
+
+    context 'on run' do
+      before do
+        build.queue
+        build.run
+      end
+
+      it { is_expected.to eq('running') }
+    end
+
+    context 'on drop' do
+      before do
+        build.drop
+      end
+
+      it { is_expected.to eq('failed') }
+    end
+
+    context 'on success' do
+      before do
+        build.success
+      end
+
+      it { is_expected.to eq('success') }
+    end
+
+    context 'on cancel' do
+      before do
+        build.cancel
+      end
+
+      it { is_expected.to eq('canceled') }
+    end
+  end
 end
-- 
GitLab


From 97ef1ee38c82aaf6e29c8cbf06e347f9c59ee758 Mon Sep 17 00:00:00 2001
From: Douwe Maan 
Date: Thu, 11 Aug 2016 13:28:02 -0500
Subject: [PATCH 140/153] Update gitlab-shell to v3.3.3

---
 GITLAB_SHELL_VERSION       | 2 +-
 doc/update/8.10-to-8.11.md | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index e4604e3afd0dd7..619b537668489e 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-3.2.1
+3.3.3
diff --git a/doc/update/8.10-to-8.11.md b/doc/update/8.10-to-8.11.md
index 25343d484bae3c..84c624cbcb7006 100644
--- a/doc/update/8.10-to-8.11.md
+++ b/doc/update/8.10-to-8.11.md
@@ -46,7 +46,7 @@ sudo -u git -H git checkout 8-11-stable-ee
 ```bash
 cd /home/git/gitlab-shell
 sudo -u git -H git fetch --all --tags
-sudo -u git -H git checkout v3.2.1
+sudo -u git -H git checkout v3.3.3
 ```
 
 ### 5. Update gitlab-workhorse
-- 
GitLab


From 954918d452ce1403ea22a635021264a99a61c23e Mon Sep 17 00:00:00 2001
From: Connor Shea 
Date: Thu, 11 Aug 2016 12:29:10 -0600
Subject: [PATCH 141/153] Upgrade Rails to 4.2.7.1 for security fixes.

More information available at: http://weblog.rubyonrails.org/2016/8/11/Rails-5-0-0-1-4-2-7-2-and-3-2-22-3-have-been-released/
---
 Gemfile      |  2 +-
 Gemfile.lock | 62 ++++++++++++++++++++++++++--------------------------
 2 files changed, 32 insertions(+), 32 deletions(-)

diff --git a/Gemfile b/Gemfile
index 2809fca34381cc..8b44b54e22c0ca 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,6 +1,6 @@
 source 'https://rubygems.org'
 
-gem 'rails', '4.2.7'
+gem 'rails', '4.2.7.1'
 gem 'rails-deprecated_sanitizer', '~> 1.0.3'
 
 # Responders respond_to and respond_with
diff --git a/Gemfile.lock b/Gemfile.lock
index 3f39bbd754df4a..3ba6048143cd14 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -3,34 +3,34 @@ GEM
   specs:
     RedCloth (4.3.2)
     ace-rails-ap (4.0.2)
-    actionmailer (4.2.7)
-      actionpack (= 4.2.7)
-      actionview (= 4.2.7)
-      activejob (= 4.2.7)
+    actionmailer (4.2.7.1)
+      actionpack (= 4.2.7.1)
+      actionview (= 4.2.7.1)
+      activejob (= 4.2.7.1)
       mail (~> 2.5, >= 2.5.4)
       rails-dom-testing (~> 1.0, >= 1.0.5)
-    actionpack (4.2.7)
-      actionview (= 4.2.7)
-      activesupport (= 4.2.7)
+    actionpack (4.2.7.1)
+      actionview (= 4.2.7.1)
+      activesupport (= 4.2.7.1)
       rack (~> 1.6)
       rack-test (~> 0.6.2)
       rails-dom-testing (~> 1.0, >= 1.0.5)
       rails-html-sanitizer (~> 1.0, >= 1.0.2)
-    actionview (4.2.7)
-      activesupport (= 4.2.7)
+    actionview (4.2.7.1)
+      activesupport (= 4.2.7.1)
       builder (~> 3.1)
       erubis (~> 2.7.0)
       rails-dom-testing (~> 1.0, >= 1.0.5)
       rails-html-sanitizer (~> 1.0, >= 1.0.2)
-    activejob (4.2.7)
-      activesupport (= 4.2.7)
+    activejob (4.2.7.1)
+      activesupport (= 4.2.7.1)
       globalid (>= 0.3.0)
-    activemodel (4.2.7)
-      activesupport (= 4.2.7)
+    activemodel (4.2.7.1)
+      activesupport (= 4.2.7.1)
       builder (~> 3.1)
-    activerecord (4.2.7)
-      activemodel (= 4.2.7)
-      activesupport (= 4.2.7)
+    activerecord (4.2.7.1)
+      activemodel (= 4.2.7.1)
+      activesupport (= 4.2.7.1)
       arel (~> 6.0)
     activerecord-session_store (1.0.0)
       actionpack (>= 4.0, < 5.1)
@@ -38,7 +38,7 @@ GEM
       multi_json (~> 1.11, >= 1.11.2)
       rack (>= 1.5.2, < 3)
       railties (>= 4.0, < 5.1)
-    activesupport (4.2.7)
+    activesupport (4.2.7.1)
       i18n (~> 0.7)
       json (~> 1.7, >= 1.7.7)
       minitest (~> 5.1)
@@ -289,7 +289,7 @@ GEM
       omniauth (~> 1.0)
       pyu-ruby-sasl (~> 0.0.3.1)
       rubyntlm (~> 0.3)
-    globalid (0.3.6)
+    globalid (0.3.7)
       activesupport (>= 4.1.0)
     gollum-grit_adapter (1.0.1)
       gitlab-grit (~> 2.7, >= 2.7.1)
@@ -518,16 +518,16 @@ GEM
       rack
     rack-test (0.6.3)
       rack (>= 1.0)
-    rails (4.2.7)
-      actionmailer (= 4.2.7)
-      actionpack (= 4.2.7)
-      actionview (= 4.2.7)
-      activejob (= 4.2.7)
-      activemodel (= 4.2.7)
-      activerecord (= 4.2.7)
-      activesupport (= 4.2.7)
+    rails (4.2.7.1)
+      actionmailer (= 4.2.7.1)
+      actionpack (= 4.2.7.1)
+      actionview (= 4.2.7.1)
+      activejob (= 4.2.7.1)
+      activemodel (= 4.2.7.1)
+      activerecord (= 4.2.7.1)
+      activesupport (= 4.2.7.1)
       bundler (>= 1.3.0, < 2.0)
-      railties (= 4.2.7)
+      railties (= 4.2.7.1)
       sprockets-rails
     rails-deprecated_sanitizer (1.0.3)
       activesupport (>= 4.2.0.alpha)
@@ -537,9 +537,9 @@ GEM
       rails-deprecated_sanitizer (>= 1.0.1)
     rails-html-sanitizer (1.0.3)
       loofah (~> 2.0)
-    railties (4.2.7)
-      actionpack (= 4.2.7)
-      activesupport (= 4.2.7)
+    railties (4.2.7.1)
+      actionpack (= 4.2.7.1)
+      activesupport (= 4.2.7.1)
       rake (>= 0.8.7)
       thor (>= 0.18.1, < 2.0)
     rainbow (2.1.0)
@@ -914,7 +914,7 @@ DEPENDENCIES
   rack-attack (~> 4.3.1)
   rack-cors (~> 0.4.0)
   rack-oauth2 (~> 1.2.1)
-  rails (= 4.2.7)
+  rails (= 4.2.7.1)
   rails-deprecated_sanitizer (~> 1.0.3)
   rainbow (~> 2.1.0)
   rblineprof (~> 0.3.6)
-- 
GitLab


From 6a6a69f4afbe0107a75df018b662f02b5ec0166a Mon Sep 17 00:00:00 2001
From: Kamil Trzcinski 
Date: Thu, 11 Aug 2016 20:54:02 +0200
Subject: [PATCH 142/153] Use state machine for pipeline event processing

---
 app/models/ci/pipeline.rb                     | 61 +++++++++++++------
 app/models/commit_status.rb                   |  8 +--
 app/services/ci/create_pipeline_service.rb    |  5 +-
 features/steps/shared/builds.rb               |  2 -
 spec/features/pipelines_spec.rb               |  4 --
 spec/lib/ci/charts_spec.rb                    |  1 -
 spec/models/ci/pipeline_spec.rb               | 38 ++----------
 spec/requests/api/builds_spec.rb              |  4 --
 .../ci/image_for_build_service_spec.rb        |  2 -
 9 files changed, 54 insertions(+), 71 deletions(-)

diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 8de799d60885fe..7a0430f277a297 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -19,6 +19,37 @@ class Pipeline < ActiveRecord::Base
 
     after_save :keep_around_commits
 
+    state_machine :status, initial: :created do
+      event :skip do
+        transition any => :skipped
+      end
+
+      event :drop do
+        transition any => :failed
+      end
+
+      event :update_status do
+        transition any => :pending, if: ->(pipeline) { pipeline.can_transition_to?('pending') }
+        transition any => :running, if: ->(pipeline) { pipeline.can_transition_to?('running') }
+        transition any => :failed, if: ->(pipeline) { pipeline.can_transition_to?('failed') }
+        transition any => :success, if: ->(pipeline) { pipeline.can_transition_to?('success') }
+        transition any => :canceled, if: ->(pipeline) { pipeline.can_transition_to?('canceled') }
+        transition any => :skipped, if: ->(pipeline) { pipeline.can_transition_to?('skipped') }
+      end
+
+      after_transition [:created, :pending] => :running do |pipeline|
+        pipeline.update(started_at: Time.now)
+      end
+
+      after_transition any => [:success, :failed, :canceled] do |pipeline|
+        pipeline.update(finished_at: Time.now)
+      end
+
+      after_transition do |pipeline|
+        pipeline.update_duration
+      end
+    end
+
     # ref can't be HEAD or SHA, can only be branch/tag name
     scope :latest_successful_for, ->(ref = default_branch) do
       where(ref: ref).success.order(id: :desc).limit(1)
@@ -89,16 +120,12 @@ def cancelable?
 
     def cancel_running
       builds.running_or_pending.each(&:cancel)
-
-      reload_status!
     end
 
     def retry_failed(user)
       builds.latest.failed.select(&:retryable?).each do |build|
         Ci::Build.retry(build, user)
       end
-
-      reload_status!
     end
 
     def latest?
@@ -185,8 +212,6 @@ def notes
 
     def process!
       Ci::ProcessPipelineService.new(project, user).execute(self)
-
-      reload_status!
     end
 
     def predefined_variables
@@ -195,22 +220,22 @@ def predefined_variables
       ]
     end
 
-    def reload_status!
-      reload
-      self.status =
-        if yaml_errors.blank?
-          statuses.latest.status || 'skipped'
-        else
-          'failed'
-        end
-      self.started_at = statuses.started_at
-      self.finished_at = statuses.finished_at
-      self.duration = statuses.latest.duration
-      save
+    def can_transition_to?(expected_status)
+      latest_status == expected_status
+    end
+
+    def update_duration
+      update(duration: statuses.latest.duration)
     end
 
     private
 
+    def latest_status
+      return 'failed' unless yaml_errors.blank?
+
+      statuses.latest.status || 'skipped'
+    end
+
     def keep_around_commits
       return unless project
 
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 3ab44461179065..64ce5431d636c8 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -74,13 +74,13 @@ class CommitStatus < ActiveRecord::Base
     around_transition any => [:success, :failed, :canceled] do |commit_status, block|
       block.call
 
-      commit_status.pipeline.process! if commit_status.pipeline
+      commit_status.pipeline.try(:process!)
     end
 
-    around_transition any => [:pending, :running] do |commit_status, block|
-      block.call
+    # Try to update the pipeline status
 
-      commit_status.pipeline.reload_status! if commit_status.pipeline
+    after_transition do |commit_status, transition|
+      commit_status.pipeline.try(:update_status) unless transition.loopback?
     end
   end
 
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index 7398fd8e10af2b..cde856b0186833 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -37,7 +37,8 @@ def execute(ignore_skip_ci: false, save_on_errors: true, trigger_request: nil)
       end
 
       if !ignore_skip_ci && skip_ci?
-        return error('Creation of pipeline is skipped', save: save_on_errors)
+        pipeline.skip if save_on_errors
+        return pipeline
       end
 
       unless pipeline.config_builds_attributes.present?
@@ -93,7 +94,7 @@ def valid_sha?
 
     def error(message, save: false)
       pipeline.errors.add(:base, message)
-      pipeline.reload_status! if save
+      pipeline.drop if save
       pipeline
     end
   end
diff --git a/features/steps/shared/builds.rb b/features/steps/shared/builds.rb
index c7f61da05fab02..5ed5cdb759f9aa 100644
--- a/features/steps/shared/builds.rb
+++ b/features/steps/shared/builds.rb
@@ -12,7 +12,6 @@ module SharedBuilds
   step 'project has a recent build' do
     @pipeline = create(:ci_empty_pipeline, project: @project, sha: @project.commit.sha, ref: 'master')
     @build = create(:ci_build_with_coverage, pipeline: @pipeline)
-    @pipeline.reload_status!
   end
 
   step 'recent build is successful' do
@@ -25,7 +24,6 @@ module SharedBuilds
 
   step 'project has another build that is running' do
     create(:ci_build, pipeline: @pipeline, name: 'second build', status: 'running')
-    @pipeline.reload_status!
   end
 
   step 'I visit recent build details page' do
diff --git a/spec/features/pipelines_spec.rb b/spec/features/pipelines_spec.rb
index f88b8f8e60b0ef..248e44a93aa78b 100644
--- a/spec/features/pipelines_spec.rb
+++ b/spec/features/pipelines_spec.rb
@@ -34,7 +34,6 @@
       let!(:running) { create(:ci_build, :running, pipeline: pipeline, stage: 'test', commands: 'test') }
 
       before do
-        pipeline.reload_status!
         visit namespace_project_pipelines_path(project.namespace, project)
       end
 
@@ -53,7 +52,6 @@
       let!(:failed) { create(:ci_build, :failed, pipeline: pipeline, stage: 'test', commands: 'test') }
 
       before do
-        pipeline.reload_status!
         visit namespace_project_pipelines_path(project.namespace, project)
       end
 
@@ -87,7 +85,6 @@
         let!(:running) { create(:generic_commit_status, status: 'running', pipeline: pipeline, stage: 'test') }
 
         before do
-          pipeline.reload_status!
           visit namespace_project_pipelines_path(project.namespace, project)
         end
 
@@ -104,7 +101,6 @@
         let!(:failed) { create(:generic_commit_status, status: 'failed', pipeline: pipeline, stage: 'test') }
 
         before do
-          pipeline.reload_status!
           visit namespace_project_pipelines_path(project.namespace, project)
         end
 
diff --git a/spec/lib/ci/charts_spec.rb b/spec/lib/ci/charts_spec.rb
index 2cd6b00dad6b2b..034ea098193adf 100644
--- a/spec/lib/ci/charts_spec.rb
+++ b/spec/lib/ci/charts_spec.rb
@@ -5,7 +5,6 @@
     before do
       @pipeline = FactoryGirl.create(:ci_pipeline)
       FactoryGirl.create(:ci_build, pipeline: @pipeline)
-      @pipeline.reload_status!
     end
 
     it 'returns build times in minutes' do
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 9a9720cfbfc546..eb762276cbe309 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -2,7 +2,7 @@
 
 describe Ci::Pipeline, models: true do
   let(:project) { FactoryGirl.create :empty_project }
-  let(:pipeline) { FactoryGirl.create :ci_pipeline, project: project }
+  let(:pipeline) { FactoryGirl.create :ci_empty_pipeline, project: project }
 
   it { is_expected.to belong_to(:project) }
   it { is_expected.to belong_to(:user) }
@@ -51,25 +51,6 @@
     end
   end
 
-  describe "#finished_at" do
-    let(:pipeline) { FactoryGirl.create :ci_pipeline }
-
-    it "returns finished_at of latest build" do
-      build = FactoryGirl.create :ci_build, pipeline: pipeline, finished_at: Time.now - 60
-      FactoryGirl.create :ci_build, pipeline: pipeline, finished_at: Time.now - 120
-      pipeline.reload_status!
-
-      expect(pipeline.finished_at.to_i).to eq(build.finished_at.to_i)
-    end
-
-    it "returns nil if there is no finished build" do
-      FactoryGirl.create :ci_not_started_build, pipeline: pipeline
-      pipeline.reload_status!
-
-      expect(pipeline.finished_at).to be_nil
-    end
-  end
-
   describe "coverage" do
     let(:project) { FactoryGirl.create :empty_project, build_coverage_regex: "/.*/" }
     let(:pipeline) { FactoryGirl.create :ci_empty_pipeline, project: project }
@@ -139,31 +120,20 @@
     end
   end
 
-  describe '#reload_status!' do
+  describe '#update_counters' do
     let(:pipeline) { create :ci_empty_pipeline, project: project }
 
-    context 'dependent objects' do
-      let(:commit_status) { create :commit_status, :pending, pipeline: pipeline }
-
-      it 'executes reload_status! after succeeding dependent object' do
-        expect(pipeline).to receive(:reload_status!).and_return(true)
-
-        commit_status.success
-      end
-    end
-
     context 'updates' do
       let(:current) { Time.now.change(usec: 0) }
       let(:build) { FactoryGirl.create :ci_build, pipeline: pipeline, started_at: current - 120, finished_at: current - 60 }
 
       before do
-        build
-        pipeline.reload_status!
+        build.skip
       end
 
       [:status, :started_at, :finished_at, :duration].each do |param|
         it "#{param}" do
-          expect(pipeline.send(param)).to eq(build.send(param))
+          expect(pipeline.reload.send(param)).to eq(build.send(param))
         end
       end
     end
diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb
index a4cdd8f3140a38..966d302dfd3acf 100644
--- a/spec/requests/api/builds_spec.rb
+++ b/spec/requests/api/builds_spec.rb
@@ -238,10 +238,6 @@ def path_for_ref(ref = pipeline.ref, job = build.name)
         it { expect(response.headers).to include(download_headers) }
       end
 
-      before do
-        pipeline.reload_status!
-      end
-
       context 'with regular branch' do
         before do
           pipeline.update(ref: 'master',
diff --git a/spec/services/ci/image_for_build_service_spec.rb b/spec/services/ci/image_for_build_service_spec.rb
index 259062406c7370..c931c3e4829d2e 100644
--- a/spec/services/ci/image_for_build_service_spec.rb
+++ b/spec/services/ci/image_for_build_service_spec.rb
@@ -14,7 +14,6 @@ module Ci
       context 'branch name' do
         before { allow(project).to receive(:commit).and_return(OpenStruct.new(sha: commit_sha)) }
         before { build.run! }
-        before { pipeline.reload_status! }
         let(:image) { service.execute(project, ref: 'master') }
 
         it { expect(image).to be_kind_of(OpenStruct) }
@@ -32,7 +31,6 @@ module Ci
 
       context 'commit sha' do
         before { build.run! }
-        before { pipeline.reload_status! }
         let(:image) { service.execute(project, sha: build.sha) }
 
         it { expect(image).to be_kind_of(OpenStruct) }
-- 
GitLab


From 4ccf39cde7356bf98bef5aae694257fb2c001e75 Mon Sep 17 00:00:00 2001
From: Kamil Trzcinski 
Date: Thu, 11 Aug 2016 22:54:25 +0200
Subject: [PATCH 143/153] Fix test failures, that did occur because of missing
 previously used `reload_status!` call

---
 app/models/ci/build.rb           | 37 ++++++++++++++++----------------
 app/models/commit_status.rb      | 18 +++++++---------
 features/steps/shared/builds.rb  |  2 +-
 spec/features/pipelines_spec.rb  | 11 ++++++----
 spec/lib/ci/charts_spec.rb       | 16 ++++++++------
 spec/models/ci/pipeline_spec.rb  | 24 +++++++++------------
 spec/requests/api/builds_spec.rb |  8 +++++--
 7 files changed, 60 insertions(+), 56 deletions(-)

diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 88a340379b890e..92dad9377c9613 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -42,24 +42,25 @@ def create_from(build)
       end
 
       def retry(build, user = nil)
-        new_build = Ci::Build.new(status: 'pending')
-        new_build.ref = build.ref
-        new_build.tag = build.tag
-        new_build.options = build.options
-        new_build.commands = build.commands
-        new_build.tag_list = build.tag_list
-        new_build.project = build.project
-        new_build.pipeline = build.pipeline
-        new_build.name = build.name
-        new_build.allow_failure = build.allow_failure
-        new_build.stage = build.stage
-        new_build.stage_idx = build.stage_idx
-        new_build.trigger_request = build.trigger_request
-        new_build.yaml_variables = build.yaml_variables
-        new_build.when = build.when
-        new_build.user = user
-        new_build.environment = build.environment
-        new_build.save
+        new_build = Ci::Build.create(
+          ref: build.ref,
+          tag: build.tag,
+          options: build.options,
+          commands: build.commands,
+          tag_list: build.tag_list,
+          project: build.project,
+          pipeline: build.pipeline,
+          name: build.name,
+          allow_failure: build.allow_failure,
+          stage: build.stage,
+          stage_idx: build.stage_idx,
+          trigger_request: build.trigger_request,
+          yaml_variables: build.yaml_variables,
+          when: build.when,
+          user: user,
+          environment: build.environment,
+          status_event: 'queue'
+        )
         MergeRequests::AddTodoWhenBuildFailsService.new(build.project, nil).close(new_build)
         new_build
       end
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 64ce5431d636c8..522fa5d69110a8 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -62,14 +62,6 @@ class CommitStatus < ActiveRecord::Base
       commit_status.update_attributes finished_at: Time.now
     end
 
-    after_transition [:created, :pending, :running] => :success do |commit_status|
-      MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.pipeline.project, nil).trigger(commit_status)
-    end
-
-    after_transition any => :failed do |commit_status|
-      MergeRequests::AddTodoWhenBuildFailsService.new(commit_status.pipeline.project, nil).execute(commit_status)
-    end
-
     # We use around_transition to process pipeline on next stages as soon as possible, before the `after_*` is executed
     around_transition any => [:success, :failed, :canceled] do |commit_status, block|
       block.call
@@ -77,11 +69,17 @@ class CommitStatus < ActiveRecord::Base
       commit_status.pipeline.try(:process!)
     end
 
-    # Try to update the pipeline status
-
     after_transition do |commit_status, transition|
       commit_status.pipeline.try(:update_status) unless transition.loopback?
     end
+
+    after_transition [:created, :pending, :running] => :success do |commit_status|
+      MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.pipeline.project, nil).trigger(commit_status)
+    end
+
+    after_transition any => :failed do |commit_status|
+      MergeRequests::AddTodoWhenBuildFailsService.new(commit_status.pipeline.project, nil).execute(commit_status)
+    end
   end
 
   delegate :sha, :short_sha, to: :pipeline
diff --git a/features/steps/shared/builds.rb b/features/steps/shared/builds.rb
index 5ed5cdb759f9aa..70e6d4836b2463 100644
--- a/features/steps/shared/builds.rb
+++ b/features/steps/shared/builds.rb
@@ -23,7 +23,7 @@ module SharedBuilds
   end
 
   step 'project has another build that is running' do
-    create(:ci_build, pipeline: @pipeline, name: 'second build', status: 'running')
+    create(:ci_build, pipeline: @pipeline, name: 'second build', status_event: 'run')
   end
 
   step 'I visit recent build details page' do
diff --git a/spec/features/pipelines_spec.rb b/spec/features/pipelines_spec.rb
index 248e44a93aa78b..4e75f888176393 100644
--- a/spec/features/pipelines_spec.rb
+++ b/spec/features/pipelines_spec.rb
@@ -12,7 +12,7 @@
   end
 
   describe 'GET /:project/pipelines' do
-    let!(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', status: 'running') }
+    let!(:pipeline) { create(:ci_empty_pipeline, project: project, ref: 'master', status: 'running') }
 
     [:all, :running, :branches].each do |scope|
       context "displaying #{scope}" do
@@ -31,9 +31,10 @@
     end
 
     context 'cancelable pipeline' do
-      let!(:running) { create(:ci_build, :running, pipeline: pipeline, stage: 'test', commands: 'test') }
+      let!(:build) { create(:ci_build, pipeline: pipeline, stage: 'test', commands: 'test') }
 
       before do
+        build.run
         visit namespace_project_pipelines_path(project.namespace, project)
       end
 
@@ -49,9 +50,10 @@
     end
 
     context 'retryable pipelines' do
-      let!(:failed) { create(:ci_build, :failed, pipeline: pipeline, stage: 'test', commands: 'test') }
+      let!(:build) { create(:ci_build, pipeline: pipeline, stage: 'test', commands: 'test') }
 
       before do
+        build.drop
         visit namespace_project_pipelines_path(project.namespace, project)
       end
 
@@ -98,9 +100,10 @@
       end
 
       context 'when failed' do
-        let!(:failed) { create(:generic_commit_status, status: 'failed', pipeline: pipeline, stage: 'test') }
+        let!(:status) { create(:generic_commit_status, :pending, pipeline: pipeline, stage: 'test') }
 
         before do
+          status.drop
           visit namespace_project_pipelines_path(project.namespace, project)
         end
 
diff --git a/spec/lib/ci/charts_spec.rb b/spec/lib/ci/charts_spec.rb
index 034ea098193adf..fb6cc398307807 100644
--- a/spec/lib/ci/charts_spec.rb
+++ b/spec/lib/ci/charts_spec.rb
@@ -2,21 +2,23 @@
 
 describe Ci::Charts, lib: true do
   context "build_times" do
+    let(:project) { create(:empty_project) }
+    let(:chart) { Ci::Charts::BuildTime.new(project) }
+
+    subject { chart.build_times }
+
     before do
-      @pipeline = FactoryGirl.create(:ci_pipeline)
-      FactoryGirl.create(:ci_build, pipeline: @pipeline)
+      create(:ci_empty_pipeline, project: project, duration: 120)
     end
 
     it 'returns build times in minutes' do
-      chart = Ci::Charts::BuildTime.new(@pipeline.project)
-      expect(chart.build_times).to eq([2])
+      is_expected.to contain_exactly(2)
     end
 
     it 'handles nil build times' do
-      create(:ci_pipeline, duration: nil, project: @pipeline.project)
+      create(:ci_empty_pipeline, project: project, duration: nil)
 
-      chart = Ci::Charts::BuildTime.new(@pipeline.project)
-      expect(chart.build_times).to eq([2, 0])
+      is_expected.to contain_exactly(2, 0)
     end
   end
 end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index eb762276cbe309..adfe4bdd0c8e3f 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -120,22 +120,18 @@
     end
   end
 
-  describe '#update_counters' do
-    let(:pipeline) { create :ci_empty_pipeline, project: project }
+  describe '#duration' do
+    let(:current) { Time.now.change(usec: 0) }
+    let!(:build) { create :ci_build, name: 'build1', pipeline: pipeline, started_at: current - 60, finished_at: current }
+    let!(:build2) { create :ci_build, name: 'build2', pipeline: pipeline, started_at: current - 60, finished_at: current }
 
-    context 'updates' do
-      let(:current) { Time.now.change(usec: 0) }
-      let(:build) { FactoryGirl.create :ci_build, pipeline: pipeline, started_at: current - 120, finished_at: current - 60 }
-
-      before do
-        build.skip
-      end
+    before do
+      build.skip
+      build2.skip
+    end
 
-      [:status, :started_at, :finished_at, :duration].each do |param|
-        it "#{param}" do
-          expect(pipeline.reload.send(param)).to eq(build.send(param))
-        end
-      end
+    it 'matches sum of builds duration' do
+      expect(pipeline.reload.duration).to eq(build.duration + build2.duration)
     end
   end
 
diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb
index 966d302dfd3acf..41503885dd9454 100644
--- a/spec/requests/api/builds_spec.rb
+++ b/spec/requests/api/builds_spec.rb
@@ -9,7 +9,7 @@
   let!(:developer) { create(:project_member, :developer, user: user, project: project) }
   let(:reporter) { create(:project_member, :reporter, project: project) }
   let(:guest) { create(:project_member, :guest, project: project) }
-  let!(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) }
+  let!(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) }
   let!(:build) { create(:ci_build, pipeline: pipeline) }
 
   describe 'GET /projects/:id/builds ' do
@@ -174,7 +174,11 @@
 
   describe 'GET /projects/:id/artifacts/:ref_name/download?job=name' do
     let(:api_user) { reporter.user }
-    let(:build) { create(:ci_build, :success, :artifacts, pipeline: pipeline) }
+    let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
+
+    before do
+      build.success
+    end
 
     def path_for_ref(ref = pipeline.ref, job = build.name)
       api("/projects/#{project.id}/builds/artifacts/#{ref}/download?job=#{job}", api_user)
-- 
GitLab


From 957632b72531e30637c94af9c1f7d3246cc5a6cd Mon Sep 17 00:00:00 2001
From: ubudzisz 
Date: Thu, 7 Jul 2016 11:02:57 +0200
Subject: [PATCH 144/153] render only commit title

update CHANGELOG

add auto-completion into pipeline

add auto-completion into pipeline

add auto-completion into pipeline

update changelog

modify tests

remove empty lines

add auto-completion into pipeline

update changelog

modify tests

switch text_field_tag into text_field

add test to new field

switch context into describe

Update CHANGELOG

render only commit title

update CHANGELOG

add auto-completion into pipeline

add auto-completion into pipeline

add auto-completion into pipeline

update changelog

modify tests

remove empty lines

add auto-completion into pipeline

update changelog

modify tests

update changelog

Update CHANGELOG

add indetation

add tests to pipeline ref

change file name for tests

change file name for spec tests

remove empty line

rename test it

rename test name

removing unexpected changes

removing unexpected changes2

update changelog
---
 CHANGELOG                                     |  1 +
 app/views/projects/pipelines/new.html.haml    |  2 +-
 .../features/{ => projects}/pipelines_spec.rb | 30 +++++++++++++++++--
 3 files changed, 30 insertions(+), 3 deletions(-)
 rename spec/features/{ => projects}/pipelines_spec.rb (89%)

diff --git a/CHANGELOG b/CHANGELOG
index 599ff678f822a2..55cffc6152e7c4 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -105,6 +105,7 @@ v 8.11.0 (unreleased)
   - Sort folders with submodules in Files view !5521
   - Each `File::exists?` replaced to `File::exist?` because of deprecate since ruby version 2.2.0
   - Print urls to create (or view) merge requests after git push !5542 (Scott Le)
+  - Add auto-completition in pipeline (Katarzyna Kobierska Ula Budziszewska)
 
 v 8.10.5
   - Add a data migration to fix some missing timestamps in the members table. !5670
diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml
index 5f4ec2e40c85e9..55202725b9ee74 100644
--- a/app/views/projects/pipelines/new.html.haml
+++ b/app/views/projects/pipelines/new.html.haml
@@ -9,7 +9,7 @@
   .form-group
     = f.label :ref, 'Create for', class: 'control-label'
     .col-sm-10
-      = f.text_field :ref, required: true, tabindex: 2, class: 'form-control'
+      = f.text_field :ref, required: true, tabindex: 2, class: 'form-control js-branch-name ui-autocomplete-input', autocomplete: :false, id: :ref
       .help-block Existing branch name, tag
   .form-actions
     = f.submit 'Create pipeline', class: 'btn btn-create', tabindex: 3
diff --git a/spec/features/pipelines_spec.rb b/spec/features/projects/pipelines_spec.rb
similarity index 89%
rename from spec/features/pipelines_spec.rb
rename to spec/features/projects/pipelines_spec.rb
index f88b8f8e60b0ef..c39bb7ffa377e4 100644
--- a/spec/features/pipelines_spec.rb
+++ b/spec/features/projects/pipelines_spec.rb
@@ -206,7 +206,7 @@
     before { visit new_namespace_project_pipeline_path(project.namespace, project) }
 
     context 'for valid commit' do
-      before { fill_in('Create for', with: 'master') }
+      before { fill_in('pipeline[ref]', with: 'master') }
 
       context 'with gitlab-ci.yml' do
         before { stub_ci_pipeline_to_return_yaml_file }
@@ -223,11 +223,37 @@
 
     context 'for invalid commit' do
       before do
-        fill_in('Create for', with: 'invalid-reference')
+        fill_in('pipeline[ref]', with: 'invalid-reference')
         click_on 'Create pipeline'
       end
 
       it { expect(page).to have_content('Reference not found') }
     end
   end
+
+  describe 'Create pipelines', feature: true do
+    let(:project) { create(:project) }
+
+    before do
+      visit new_namespace_project_pipeline_path(project.namespace, project)
+    end
+
+    describe 'new pipeline page' do
+      it 'has field to add a new pipeline' do
+        expect(page).to have_field('pipeline[ref]')
+        expect(page).to have_content('Create for')
+      end
+    end
+
+    describe 'find pipelines' do
+      it 'shows filtered pipelines', js: true do
+        fill_in('pipeline[ref]', with: 'fix')
+        find('input#ref').native.send_keys(:keydown)
+
+        within('.ui-autocomplete') do
+          expect(page).to have_selector('li', text: 'fix')
+        end
+      end
+    end
+  end
 end
-- 
GitLab


From c5aca8b2349a0877297f8021a8cbcfd8fd29d4d2 Mon Sep 17 00:00:00 2001
From: ula budziszewska 
Date: Thu, 11 Aug 2016 20:54:33 +0000
Subject: [PATCH 145/153] Update CHANGELOG

---
 CHANGELOG | 1 -
 1 file changed, 1 deletion(-)

diff --git a/CHANGELOG b/CHANGELOG
index 55cffc6152e7c4..28834c1129ad58 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -104,7 +104,6 @@ v 8.11.0 (unreleased)
   - Fix importing GitLab projects with an invalid MR source project
   - Sort folders with submodules in Files view !5521
   - Each `File::exists?` replaced to `File::exist?` because of deprecate since ruby version 2.2.0
-  - Print urls to create (or view) merge requests after git push !5542 (Scott Le)
   - Add auto-completition in pipeline (Katarzyna Kobierska Ula Budziszewska)
 
 v 8.10.5
-- 
GitLab


From e1f05b932de5553462793fb88fdea2ca54072d40 Mon Sep 17 00:00:00 2001
From: Kamil Trzcinski 
Date: Fri, 12 Aug 2016 11:36:51 +0200
Subject: [PATCH 146/153] Use explicit events to transition between states

---
 app/models/ci/pipeline.rb   | 44 +++++++++++++++++++++++++++----------
 app/models/commit_status.rb |  2 +-
 2 files changed, 33 insertions(+), 13 deletions(-)

diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 7a0430f277a297..6aef91804a228a 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -20,6 +20,14 @@ class Pipeline < ActiveRecord::Base
     after_save :keep_around_commits
 
     state_machine :status, initial: :created do
+      event :queue do
+        transition :created => :pending
+      end
+
+      event :run do
+        transition [:pending, :success, :failed, :canceled, :skipped] => :running
+      end
+
       event :skip do
         transition any => :skipped
       end
@@ -28,13 +36,12 @@ class Pipeline < ActiveRecord::Base
         transition any => :failed
       end
 
-      event :update_status do
-        transition any => :pending, if: ->(pipeline) { pipeline.can_transition_to?('pending') }
-        transition any => :running, if: ->(pipeline) { pipeline.can_transition_to?('running') }
-        transition any => :failed, if: ->(pipeline) { pipeline.can_transition_to?('failed') }
-        transition any => :success, if: ->(pipeline) { pipeline.can_transition_to?('success') }
-        transition any => :canceled, if: ->(pipeline) { pipeline.can_transition_to?('canceled') }
-        transition any => :skipped, if: ->(pipeline) { pipeline.can_transition_to?('skipped') }
+      event :succeed do
+        transition any => :success
+      end
+
+      event :cancel do
+        transition any => :canceled
       end
 
       after_transition [:created, :pending] => :running do |pipeline|
@@ -214,23 +221,36 @@ def process!
       Ci::ProcessPipelineService.new(project, user).execute(self)
     end
 
+    def build_updated
+      case latest_builds_status
+      when 'pending'
+        queue
+      when 'running'
+        run
+      when 'success'
+        succeed
+      when 'failed'
+        drop
+      when 'canceled'
+        cancel
+      when 'skipped'
+        skip
+      end
+    end
+
     def predefined_variables
       [
         { key: 'CI_PIPELINE_ID', value: id.to_s, public: true }
       ]
     end
 
-    def can_transition_to?(expected_status)
-      latest_status == expected_status
-    end
-
     def update_duration
       update(duration: statuses.latest.duration)
     end
 
     private
 
-    def latest_status
+    def latest_builds_status
       return 'failed' unless yaml_errors.blank?
 
       statuses.latest.status || 'skipped'
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 522fa5d69110a8..c21c8ce18dbe07 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -70,7 +70,7 @@ class CommitStatus < ActiveRecord::Base
     end
 
     after_transition do |commit_status, transition|
-      commit_status.pipeline.try(:update_status) unless transition.loopback?
+      commit_status.pipeline.try(:build_updated) unless transition.loopback?
     end
 
     after_transition [:created, :pending, :running] => :success do |commit_status|
-- 
GitLab


From ad3e1edcfce1e24fb9889d5d73852680cf4facf9 Mon Sep 17 00:00:00 2001
From: Kamil Trzcinski 
Date: Fri, 12 Aug 2016 11:53:27 +0200
Subject: [PATCH 147/153] Added specs for started_at and finished_at

---
 app/models/ci/pipeline.rb       | 15 ++++++-----
 spec/models/ci/pipeline_spec.rb | 46 +++++++++++++++++++++++++++------
 2 files changed, 46 insertions(+), 15 deletions(-)

diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 6aef91804a228a..92fae78fe4e6bb 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -22,10 +22,11 @@ class Pipeline < ActiveRecord::Base
     state_machine :status, initial: :created do
       event :queue do
         transition :created => :pending
+        transition any - [:created, :pending] => :running
       end
 
       event :run do
-        transition [:pending, :success, :failed, :canceled, :skipped] => :running
+        transition any => :running
       end
 
       event :skip do
@@ -44,15 +45,15 @@ class Pipeline < ActiveRecord::Base
         transition any => :canceled
       end
 
-      after_transition [:created, :pending] => :running do |pipeline|
-        pipeline.update(started_at: Time.now)
+      before_transition [:created, :pending] => :running do |pipeline|
+        pipeline.started_at = Time.now
       end
 
-      after_transition any => [:success, :failed, :canceled] do |pipeline|
-        pipeline.update(finished_at: Time.now)
+      before_transition any => [:success, :failed, :canceled] do |pipeline|
+        pipeline.finished_at = Time.now
       end
 
-      after_transition do |pipeline|
+      before_transition do |pipeline|
         pipeline.update_duration
       end
     end
@@ -245,7 +246,7 @@ def predefined_variables
     end
 
     def update_duration
-      update(duration: statuses.latest.duration)
+      self.duration = statuses.latest.duration
     end
 
     private
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index adfe4bdd0c8e3f..28d07f67b26f9f 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -120,18 +120,48 @@
     end
   end
 
-  describe '#duration' do
+  describe 'state machine' do
     let(:current) { Time.now.change(usec: 0) }
-    let!(:build) { create :ci_build, name: 'build1', pipeline: pipeline, started_at: current - 60, finished_at: current }
-    let!(:build2) { create :ci_build, name: 'build2', pipeline: pipeline, started_at: current - 60, finished_at: current }
+    let(:build) { create :ci_build, name: 'build1', pipeline: pipeline, started_at: current - 60, finished_at: current }
+    let(:build2) { create :ci_build, name: 'build2', pipeline: pipeline, started_at: current - 60, finished_at: current }
 
-    before do
-      build.skip
-      build2.skip
+    describe '#duration' do
+      before do
+        build.skip
+        build2.skip
+      end
+
+      it 'matches sum of builds duration' do
+        expect(pipeline.reload.duration).to eq(build.duration + build2.duration)
+      end
     end
 
-    it 'matches sum of builds duration' do
-      expect(pipeline.reload.duration).to eq(build.duration + build2.duration)
+    describe '#started_at' do
+      it 'updates on transitioning to running' do
+        build.run
+
+        expect(pipeline.reload.started_at).not_to be_nil
+      end
+
+      it 'do not update on transitioning to success' do
+        build.success
+
+        expect(pipeline.reload.started_at).to be_nil
+      end
+    end
+
+    describe '#finished_at' do
+      it 'updates on transitioning to success' do
+        build.success
+
+        expect(pipeline.reload.finished_at).not_to be_nil
+      end
+
+      it 'do not update on transitioning to running' do
+        build.run
+
+        expect(pipeline.reload.finished_at).to be_nil
+      end
     end
   end
 
-- 
GitLab


From d7b681512bb738b9b2ca0c56e761616a1a761295 Mon Sep 17 00:00:00 2001
From: Kamil Trzcinski 
Date: Fri, 12 Aug 2016 12:23:47 +0200
Subject: [PATCH 148/153] Fix test failures

---
 app/models/ci/pipeline.rb                | 2 +-
 spec/features/projects/pipelines_spec.rb | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 92fae78fe4e6bb..6820b2d41a7556 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -21,7 +21,7 @@ class Pipeline < ActiveRecord::Base
 
     state_machine :status, initial: :created do
       event :queue do
-        transition :created => :pending
+        transition created: :pending
         transition any - [:created, :pending] => :running
       end
 
diff --git a/spec/features/projects/pipelines_spec.rb b/spec/features/projects/pipelines_spec.rb
index b57652b3ea243f..29d150bc5971be 100644
--- a/spec/features/projects/pipelines_spec.rb
+++ b/spec/features/projects/pipelines_spec.rb
@@ -64,7 +64,7 @@
         before { click_link('Retry') }
 
         it { expect(page).not_to have_link('Retry') }
-        it { expect(page).to have_selector('.ci-pending') }
+        it { expect(page).to have_selector('.ci-running') }
       end
     end
 
-- 
GitLab


From ea4ac578534d3a233c3525bf8351eb2045f6e632 Mon Sep 17 00:00:00 2001
From: Kamil Trzcinski 
Date: Fri, 12 Aug 2016 13:57:58 +0200
Subject: [PATCH 149/153] Use event `enqueue` instead of `queue`

---
 app/models/ci/build.rb                      |  4 ++--
 app/models/ci/pipeline.rb                   | 20 +++++++-----------
 app/models/commit_status.rb                 |  2 +-
 app/services/ci/process_pipeline_service.rb |  2 +-
 spec/models/build_spec.rb                   |  6 ++++--
 spec/models/ci/pipeline_spec.rb             | 23 +++++++++++++++++----
 6 files changed, 34 insertions(+), 23 deletions(-)

diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 92dad9377c9613..3d6c6ea3209af6 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -59,7 +59,7 @@ def retry(build, user = nil)
           when: build.when,
           user: user,
           environment: build.environment,
-          status_event: 'queue'
+          status_event: 'enqueue'
         )
         MergeRequests::AddTodoWhenBuildFailsService.new(build.project, nil).close(new_build)
         new_build
@@ -102,7 +102,7 @@ def playable?
 
     def play(current_user = nil)
       # Try to queue a current build
-      if self.queue
+      if self.enqueue
         self.update(user: current_user)
         self
       else
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 6820b2d41a7556..d00de56bf075e4 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -20,7 +20,7 @@ class Pipeline < ActiveRecord::Base
     after_save :keep_around_commits
 
     state_machine :status, initial: :created do
-      event :queue do
+      event :enqueue do
         transition created: :pending
         transition any - [:created, :pending] => :running
       end
@@ -224,18 +224,12 @@ def process!
 
     def build_updated
       case latest_builds_status
-      when 'pending'
-        queue
-      when 'running'
-        run
-      when 'success'
-        succeed
-      when 'failed'
-        drop
-      when 'canceled'
-        cancel
-      when 'skipped'
-        skip
+      when 'pending' then enqueue
+      when 'running' then run
+      when 'success' then succeed
+      when 'failed' then drop
+      when 'canceled' then cancel
+      when 'skipped' then skip
       end
     end
 
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index c21c8ce18dbe07..703ca90edb6982 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -26,7 +26,7 @@ class CommitStatus < ActiveRecord::Base
   scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) }
 
   state_machine :status do
-    event :queue do
+    event :enqueue do
       transition [:created, :skipped] => :pending
     end
 
diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb
index 86c4823d18a453..6f7610d42ba0ce 100644
--- a/app/services/ci/process_pipeline_service.rb
+++ b/app/services/ci/process_pipeline_service.rb
@@ -37,7 +37,7 @@ def process_build(build, current_status)
       return false unless Statuseable::COMPLETED_STATUSES.include?(current_status)
 
       if valid_statuses_for_when(build.when).include?(current_status)
-        build.queue
+        build.enqueue
         true
       else
         build.skip
diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb
index 60a221eba50670..04fb074cfd86a2 100644
--- a/spec/models/build_spec.rb
+++ b/spec/models/build_spec.rb
@@ -886,8 +886,10 @@ def create_mr(build, pipeline, factory: :merge_request, created_at: Time.now)
       is_expected.to eq(build)
     end
 
-    context 'for success build' do
-      before { build.queue }
+    context 'for successful build' do
+      before do
+        build.success
+      end
 
       it 'creates a new build' do
         is_expected.to be_pending
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 28d07f67b26f9f..950833cb219a24 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -143,7 +143,7 @@
         expect(pipeline.reload.started_at).not_to be_nil
       end
 
-      it 'do not update on transitioning to success' do
+      it 'does not update on transitioning to success' do
         build.success
 
         expect(pipeline.reload.started_at).to be_nil
@@ -157,7 +157,7 @@
         expect(pipeline.reload.finished_at).not_to be_nil
       end
 
-      it 'do not update on transitioning to running' do
+      it 'does not update on transitioning to running' do
         build.run
 
         expect(pipeline.reload.finished_at).to be_nil
@@ -257,14 +257,16 @@
     subject { pipeline.reload.status }
 
     context 'on queuing' do
-      before { build.queue }
+      before do
+        build.enqueue
+      end
 
       it { is_expected.to eq('pending') }
     end
 
     context 'on run' do
       before do
-        build.queue
+        build.enqueue
         build.run
       end
 
@@ -294,5 +296,18 @@
 
       it { is_expected.to eq('canceled') }
     end
+
+    context 'on failure and build retry' do
+      before do
+        build.drop
+        Ci::Build.retry(build)
+      end
+
+      # We are changing a state: created > failed > running
+      # Instead of: created > failed > pending
+      # Since the pipeline already run, so it should not be pending anymore
+
+      it { is_expected.to eq('running') }
+    end
   end
 end
-- 
GitLab


From a7f84d1a03978243c4fd5b8a878a4fea2b246f87 Mon Sep 17 00:00:00 2001
From: Kamil Trzcinski 
Date: Fri, 12 Aug 2016 13:59:20 +0200
Subject: [PATCH 150/153] Improve transition between states for event `enqueue`

---
 app/models/ci/pipeline.rb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index d00de56bf075e4..8cfba92ae9b332 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -22,7 +22,7 @@ class Pipeline < ActiveRecord::Base
     state_machine :status, initial: :created do
       event :enqueue do
         transition created: :pending
-        transition any - [:created, :pending] => :running
+        transition [:success, :failed, :canceled, :skipped] => :running
       end
 
       event :run do
-- 
GitLab


From 6397ecd4e4e1d6170fd4b6fbb0390364567284d2 Mon Sep 17 00:00:00 2001
From: "Z.J. van de Weg" 
Date: Fri, 12 Aug 2016 14:41:27 +0200
Subject: [PATCH 151/153] Update ruby 2.3.1

---
 .gitlab-ci.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 2a63ee15af0bfc..e8d54e768d3095 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,7 +1,7 @@
-image: "ruby:2.3"
+image: "ruby:2.3.1"
 
 cache:
-  key: "ruby-23"
+  key: "ruby-231"
   paths:
   - vendor/apt
   - vendor/ruby
-- 
GitLab


From 7cfc47432170be14f9449a77f893c4662634019d Mon Sep 17 00:00:00 2001
From: Kamil Trzcinski 
Date: Fri, 12 Aug 2016 15:09:35 +0200
Subject: [PATCH 152/153] Fix build play failure

---
 spec/models/build_spec.rb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb
index 04fb074cfd86a2..5980f6ddc32b1a 100644
--- a/spec/models/build_spec.rb
+++ b/spec/models/build_spec.rb
@@ -888,7 +888,7 @@ def create_mr(build, pipeline, factory: :merge_request, created_at: Time.now)
 
     context 'for successful build' do
       before do
-        build.success
+        build.update(status: 'success')
       end
 
       it 'creates a new build' do
-- 
GitLab


From e0fc43ebe2d174c97062e79de36161914dafbb66 Mon Sep 17 00:00:00 2001
From: Yorick Peterse 
Date: Fri, 12 Aug 2016 16:43:10 +0200
Subject: [PATCH 153/153] Instrument Project.visible_to_user

Because this method is a Rails scope we have to instrument it manually
as regular the instrumentation methods only instrument methods defined
directly on a Class or Module.
---
 CHANGELOG                      | 1 +
 config/initializers/metrics.rb | 3 +++
 2 files changed, 4 insertions(+)

diff --git a/CHANGELOG b/CHANGELOG
index 28834c1129ad58..6fe1720796dbf1 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -25,6 +25,7 @@ v 8.11.0 (unreleased)
   - Pre-create all builds for a Pipeline when the new Pipeline is created !5295
   - Fix of 'Commits being passed to custom hooks are already reachable when using the UI'
   - Show member roles to all users on members page
+  - Project.visible_to_user is instrumented again
   - Fix awardable button mutuality loading spinners (ClemMakesApps)
   - Add support for using RequestStore within Sidekiq tasks via SIDEKIQ_REQUEST_STORE env variable
   - Optimize maximum user access level lookup in loading of notes
diff --git a/config/initializers/metrics.rb b/config/initializers/metrics.rb
index cc8208db3c1dd9..52522e099e7de0 100644
--- a/config/initializers/metrics.rb
+++ b/config/initializers/metrics.rb
@@ -148,6 +148,9 @@
 
     config.instrument_methods(Gitlab::Highlight)
     config.instrument_instance_methods(Gitlab::Highlight)
+
+    # This is a Rails scope so we have to instrument it manually.
+    config.instrument_method(Project, :visible_to_user)
   end
 
   GC::Profiler.enable
-- 
GitLab