From b982f6f94b6576a4e0a9a507c391ebdb07ed360d Mon Sep 17 00:00:00 2001 From: Jacopo Date: Thu, 10 Aug 2017 12:05:44 +0200 Subject: [PATCH 01/32] Added tests for commits API with an unauthenticated user and a public/private project Added test to API commits in order to handle cases for authenticated/unauthenticated user in a private and public project. --- ...mits-should-not-require-authentication.yml | 4 + spec/requests/api/commits_spec.rb | 223 +++++++++--------- 2 files changed, 122 insertions(+), 105 deletions(-) create mode 100644 changelogs/unreleased/34049-public-commits-should-not-require-authentication.yml diff --git a/changelogs/unreleased/34049-public-commits-should-not-require-authentication.yml b/changelogs/unreleased/34049-public-commits-should-not-require-authentication.yml new file mode 100644 index 00000000000..278ef2a8acb --- /dev/null +++ b/changelogs/unreleased/34049-public-commits-should-not-require-authentication.yml @@ -0,0 +1,4 @@ +--- +title: Added tests for commits API unauthenticated user and public/private project +merge_request: 13287 +author: Jacopo Beschi @jacopo-beschi diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 992a6e8d76a..dafe3f466a2 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -16,11 +16,13 @@ describe API::Commits do end describe 'GET /projects/:id/repository/commits' do - context 'authorized user' do + let(:route) { "/projects/#{project_id}/repository/commits" } + + shared_examples_for 'project commits' do it "returns project commits" do commit = project.repository.commit - get api("/projects/#{project_id}/repository/commits", user) + get api(route, current_user) expect(response).to have_http_status(200) expect(response).to match_response_schema('public_api/v4/commits') @@ -32,7 +34,7 @@ describe API::Commits do it 'include correct pagination headers' do commit_count = project.repository.count_commits(ref: 'master').to_s - get api("/projects/#{project_id}/repository/commits", user) + get api(route, current_user) expect(response).to include_pagination_headers expect(response.headers['X-Total']).to eq(commit_count) @@ -40,140 +42,151 @@ describe API::Commits do end end - context "unauthorized user" do - it "does not return project commits" do - get api("/projects/#{project_id}/repository/commits") + context 'when unauthenticated', 'and project is public' do + let(:project) { create(:project, :public, :repository) } - expect(response).to have_http_status(404) + it_behaves_like 'project commits' + end + + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get api(route) } + let(:message) { '404 Project Not Found' } end end - context "since optional parameter" do - it "returns project commits since provided parameter" do - commits = project.repository.commits("master") - after = commits.second.created_at + context 'when authenticated', 'as a master' do + let(:current_user) { user } - get api("/projects/#{project_id}/repository/commits?since=#{after.utc.iso8601}", user) + it_behaves_like 'project commits' - expect(json_response.size).to eq 2 - expect(json_response.first["id"]).to eq(commits.first.id) - expect(json_response.second["id"]).to eq(commits.second.id) - end + context "since optional parameter" do + it "returns project commits since provided parameter" do + commits = project.repository.commits("master") + after = commits.second.created_at - it 'include correct pagination headers' do - commits = project.repository.commits("master") - after = commits.second.created_at - commit_count = project.repository.count_commits(ref: 'master', after: after).to_s + get api("/projects/#{project_id}/repository/commits?since=#{after.utc.iso8601}", user) - get api("/projects/#{project_id}/repository/commits?since=#{after.utc.iso8601}", user) - - expect(response).to include_pagination_headers - expect(response.headers['X-Total']).to eq(commit_count) - expect(response.headers['X-Page']).to eql('1') - end - end - - context "until optional parameter" do - it "returns project commits until provided parameter" do - commits = project.repository.commits("master") - before = commits.second.created_at - - get api("/projects/#{project_id}/repository/commits?until=#{before.utc.iso8601}", user) - - if commits.size >= 20 - expect(json_response.size).to eq(20) - else - expect(json_response.size).to eq(commits.size - 1) + expect(json_response.size).to eq 2 + expect(json_response.first["id"]).to eq(commits.first.id) + expect(json_response.second["id"]).to eq(commits.second.id) end - expect(json_response.first["id"]).to eq(commits.second.id) - expect(json_response.second["id"]).to eq(commits.third.id) + it 'include correct pagination headers' do + commits = project.repository.commits("master") + after = commits.second.created_at + commit_count = project.repository.count_commits(ref: 'master', after: after).to_s + + get api("/projects/#{project_id}/repository/commits?since=#{after.utc.iso8601}", user) + + expect(response).to include_pagination_headers + expect(response.headers['X-Total']).to eq(commit_count) + expect(response.headers['X-Page']).to eql('1') + end end - it 'include correct pagination headers' do - commits = project.repository.commits("master") - before = commits.second.created_at - commit_count = project.repository.count_commits(ref: 'master', before: before).to_s + context "until optional parameter" do + it "returns project commits until provided parameter" do + commits = project.repository.commits("master") + before = commits.second.created_at - get api("/projects/#{project_id}/repository/commits?until=#{before.utc.iso8601}", user) + get api("/projects/#{project_id}/repository/commits?until=#{before.utc.iso8601}", user) - expect(response).to include_pagination_headers - expect(response.headers['X-Total']).to eq(commit_count) - expect(response.headers['X-Page']).to eql('1') - end - end + if commits.size >= 20 + expect(json_response.size).to eq(20) + else + expect(json_response.size).to eq(commits.size - 1) + end - context "invalid xmlschema date parameters" do - it "returns an invalid parameter error message" do - get api("/projects/#{project_id}/repository/commits?since=invalid-date", user) + expect(json_response.first["id"]).to eq(commits.second.id) + expect(json_response.second["id"]).to eq(commits.third.id) + end - expect(response).to have_http_status(400) - expect(json_response['error']).to eq('since is invalid') - end - end + it 'include correct pagination headers' do + commits = project.repository.commits("master") + before = commits.second.created_at + commit_count = project.repository.count_commits(ref: 'master', before: before).to_s - context "path optional parameter" do - it "returns project commits matching provided path parameter" do - path = 'files/ruby/popen.rb' - commit_count = project.repository.count_commits(ref: 'master', path: path).to_s + get api("/projects/#{project_id}/repository/commits?until=#{before.utc.iso8601}", user) - get api("/projects/#{project_id}/repository/commits?path=#{path}", user) - - expect(json_response.size).to eq(3) - expect(json_response.first["id"]).to eq("570e7b2abdd848b95f2f578043fc23bd6f6fd24d") - expect(response).to include_pagination_headers - expect(response.headers['X-Total']).to eq(commit_count) + expect(response).to include_pagination_headers + expect(response.headers['X-Total']).to eq(commit_count) + expect(response.headers['X-Page']).to eql('1') + end end - it 'include correct pagination headers' do - path = 'files/ruby/popen.rb' - commit_count = project.repository.count_commits(ref: 'master', path: path).to_s + context "invalid xmlschema date parameters" do + it "returns an invalid parameter error message" do + get api("/projects/#{project_id}/repository/commits?since=invalid-date", user) - get api("/projects/#{project_id}/repository/commits?path=#{path}", user) - - expect(response).to include_pagination_headers - expect(response.headers['X-Total']).to eq(commit_count) - expect(response.headers['X-Page']).to eql('1') - end - end - - context 'with pagination params' do - let(:page) { 1 } - let(:per_page) { 5 } - let(:ref_name) { 'master' } - let!(:request) do - get api("/projects/#{project_id}/repository/commits?page=#{page}&per_page=#{per_page}&ref_name=#{ref_name}", user) + expect(response).to have_http_status(400) + expect(json_response['error']).to eq('since is invalid') + end end - it 'returns correct headers' do - commit_count = project.repository.count_commits(ref: ref_name).to_s + context "path optional parameter" do + it "returns project commits matching provided path parameter" do + path = 'files/ruby/popen.rb' + commit_count = project.repository.count_commits(ref: 'master', path: path).to_s - expect(response).to include_pagination_headers - expect(response.headers['X-Total']).to eq(commit_count) - expect(response.headers['X-Page']).to eq('1') - expect(response.headers['Link']).to match(/page=1&per_page=5/) - expect(response.headers['Link']).to match(/page=2&per_page=5/) + get api("/projects/#{project_id}/repository/commits?path=#{path}", user) + + expect(json_response.size).to eq(3) + expect(json_response.first["id"]).to eq("570e7b2abdd848b95f2f578043fc23bd6f6fd24d") + expect(response).to include_pagination_headers + expect(response.headers['X-Total']).to eq(commit_count) + end + + it 'include correct pagination headers' do + path = 'files/ruby/popen.rb' + commit_count = project.repository.count_commits(ref: 'master', path: path).to_s + + get api("/projects/#{project_id}/repository/commits?path=#{path}", user) + + expect(response).to include_pagination_headers + expect(response.headers['X-Total']).to eq(commit_count) + expect(response.headers['X-Page']).to eql('1') + end end - context 'viewing the first page' do - it 'returns the first 5 commits' do - commit = project.repository.commit + context 'with pagination params' do + let(:page) { 1 } + let(:per_page) { 5 } + let(:ref_name) { 'master' } + let!(:request) do + get api("/projects/#{project_id}/repository/commits?page=#{page}&per_page=#{per_page}&ref_name=#{ref_name}", user) + end - expect(json_response.size).to eq(per_page) - expect(json_response.first['id']).to eq(commit.id) + it 'returns correct headers' do + commit_count = project.repository.count_commits(ref: ref_name).to_s + + expect(response).to include_pagination_headers + expect(response.headers['X-Total']).to eq(commit_count) expect(response.headers['X-Page']).to eq('1') + expect(response.headers['Link']).to match(/page=1&per_page=5/) + expect(response.headers['Link']).to match(/page=2&per_page=5/) end - end - context 'viewing the third page' do - let(:page) { 3 } + context 'viewing the first page' do + it 'returns the first 5 commits' do + commit = project.repository.commit - it 'returns the third 5 commits' do - commit = project.repository.commits('HEAD', offset: (page - 1) * per_page).first + expect(json_response.size).to eq(per_page) + expect(json_response.first['id']).to eq(commit.id) + expect(response.headers['X-Page']).to eq('1') + end + end - expect(json_response.size).to eq(per_page) - expect(json_response.first['id']).to eq(commit.id) - expect(response.headers['X-Page']).to eq('3') + context 'viewing the third page' do + let(:page) { 3 } + + it 'returns the third 5 commits' do + commit = project.repository.commits('HEAD', offset: (page - 1) * per_page).first + + expect(json_response.size).to eq(per_page) + expect(json_response.first['id']).to eq(commit.id) + expect(response.headers['X-Page']).to eq('3') + end end end end From e70c4e44327ad7b2067a10955e9d3c1375a22910 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Thu, 10 Aug 2017 17:54:45 +0100 Subject: [PATCH 02/32] Document "Pick into Backports" label process --- PROCESS.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/PROCESS.md b/PROCESS.md index e5b17784d20..538e4389e00 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -141,18 +141,22 @@ the stable branch are: * Fixes for security issues * New or updated translations (as long as they do not touch application code) -Any merge requests cherry-picked into the stable branch for a previous release -will also be picked into the latest stable branch. These fixes will be shipped -in the next RC for that release if it is before the 22nd. If the fixes are are -completed on or after the 22nd, they will be shipped in a patch for that -release. - During the feature freeze all merge requests that are meant to go into the upcoming release should have the correct milestone assigned _and_ have the label ~"Pick into Stable" set, so that release managers can find and pick them. Merge requests without a milestone and this label will not be merged into any stable branches. +Fixes marked like this will be shipped in the next RC for that release. Once +the final RC has been prepared ready for release on the 22nd, further fixes +marked ~"Pick into Stable" will go into a patch for that release. + +If a merge request is to be picked into more than one release it will also need +the ~"Pick into Backports" label set to remind the release manager to change +the milestone after cherry-picking. As before, it should still have the +~"Pick into Stable" label and the milestone of the highest release it will be +picked into. + ### Asking for an exception If you think a merge request should go into an RC or patch even though it does not meet these requirements, From 260c8da060a6039cbd47cfe31c8ec6d6f9b43de0 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 10 Aug 2017 12:39:26 -0400 Subject: [PATCH 03/32] Whitelist or fix additional `Gitlab/PublicSend` cop violations An upcoming update to rubocop-gitlab-security added additional violations. --- .rubocop.yml | 14 +++++++++----- app/controllers/concerns/issuable_actions.rb | 2 +- app/controllers/import/github_controller.rb | 2 +- app/controllers/uploads_controller.rb | 2 +- app/helpers/commits_helper.rb | 6 +++--- app/helpers/import_helper.rb | 2 +- app/helpers/issuables_helper.rb | 13 ++++++++----- app/helpers/milestones_helper.rb | 13 ++++++++++++- app/helpers/projects_helper.rb | 7 ++++--- app/models/commit.rb | 2 +- app/models/concerns/cache_markdown_field.rb | 6 +++--- app/models/concerns/internal_id.rb | 2 +- app/models/concerns/mentionable.rb | 4 ++-- app/models/concerns/participable.rb | 2 +- .../concerns/project_features_compatibility.rb | 2 +- app/models/network/commit.rb | 2 +- app/models/project.rb | 4 ++-- .../project_services/chat_notification_service.rb | 2 +- app/models/project_services/hipchat_service.rb | 2 +- app/models/protectable_dropdown.rb | 8 ++++++-- app/models/repository.rb | 10 ++++++---- app/models/user.rb | 2 +- app/services/akismet_service.rb | 2 +- app/services/ci/retry_build_service.rb | 2 +- app/services/commits/change_service.rb | 1 + app/services/issuable_base_service.rb | 2 +- app/services/members/destroy_service.rb | 2 +- app/services/notification_service.rb | 2 ++ app/services/system_hooks_service.rb | 2 +- app/services/test_hooks/base_service.rb | 2 +- app/workers/gitlab_shell_worker.rb | 2 +- config/initializers/1_settings.rb | 2 ++ lib/api/api_guard.rb | 2 +- lib/api/entities.rb | 5 +++-- lib/api/runners.rb | 2 +- lib/api/v3/notes.rb | 6 +++--- .../filter/external_issue_reference_filter.rb | 4 ++-- lib/banzai/object_renderer.rb | 2 +- lib/banzai/pipeline/base_pipeline.rb | 2 +- lib/banzai/renderer.rb | 4 ++-- lib/bitbucket/collection.rb | 2 +- lib/ci/ansi2html.rb | 2 +- lib/declarative_policy/base.rb | 2 +- lib/declarative_policy/dsl.rb | 2 +- lib/file_size_validator.rb | 4 ++-- lib/gitlab/auth.rb | 4 ++-- lib/gitlab/cache/request_cache.rb | 2 +- lib/gitlab/diff/line_mapper.rb | 6 +++--- lib/gitlab/git/blob.rb | 2 +- lib/gitlab/git/tree.rb | 2 +- lib/gitlab/gitaly_client.rb | 2 +- lib/gitlab/github_import/base_formatter.rb | 4 +++- lib/gitlab/github_import/client.rb | 2 +- lib/gitlab/github_import/importer.rb | 2 +- lib/gitlab/lazy.rb | 2 +- lib/gitlab/ldap/person.rb | 4 ++-- lib/gitlab/markdown/pipeline.rb | 2 +- lib/uploaded_file.rb | 2 +- qa/qa/runtime/release.rb | 2 +- spec/lib/file_size_validator_spec.rb | 4 ++-- spec/models/protectable_dropdown_spec.rb | 7 +++++++ 61 files changed, 128 insertions(+), 88 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index d25b4ac39c9..583648bb877 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1174,29 +1174,33 @@ RSpec/VerifiedDoubles: GitlabSecurity/DeepMunge: Enabled: true Exclude: - - 'spec/**/*' - 'lib/**/*.rake' + - 'spec/**/*' GitlabSecurity/PublicSend: Enabled: true Exclude: - - 'spec/**/*' + - 'config/**/*' + - 'db/**/*' + - 'features/**/*' - 'lib/**/*.rake' + - 'qa/**/*' + - 'spec/**/*' GitlabSecurity/RedirectToParamsUpdate: Enabled: true Exclude: - - 'spec/**/*' - 'lib/**/*.rake' + - 'spec/**/*' GitlabSecurity/SqlInjection: Enabled: true Exclude: - - 'spec/**/*' - 'lib/**/*.rake' + - 'spec/**/*' GitlabSecurity/SystemCommandInjection: Enabled: true Exclude: - - 'spec/**/*' - 'lib/**/*.rake' + - 'spec/**/*' diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb index 0c3b68a7ac3..4079072a930 100644 --- a/app/controllers/concerns/issuable_actions.rb +++ b/app/controllers/concerns/issuable_actions.rb @@ -10,7 +10,7 @@ module IssuableActions def destroy issuable.destroy destroy_method = "destroy_#{issuable.class.name.underscore}".to_sym - TodoService.new.public_send(destroy_method, issuable, current_user) + TodoService.new.public_send(destroy_method, issuable, current_user) # rubocop:disable GitlabSecurity/PublicSend name = issuable.human_class_name flash[:notice] = "The #{name} was successfully deleted." diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb index baa6645e5ce..ab18d86dcae 100644 --- a/app/controllers/import/github_controller.rb +++ b/app/controllers/import/github_controller.rb @@ -64,7 +64,7 @@ class Import::GithubController < Import::BaseController end def import_enabled? - __send__("#{provider}_import_enabled?") + __send__("#{provider}_import_enabled?") # rubocop:disable GitlabSecurity/PublicSend end def new_import_url diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb index dc882b17143..16a74f82d3f 100644 --- a/app/controllers/uploads_controller.rb +++ b/app/controllers/uploads_controller.rb @@ -89,7 +89,7 @@ class UploadsController < ApplicationController @uploader.retrieve_from_store!(params[:filename]) else - @uploader = @model.send(upload_mount) + @uploader = @model.public_send(upload_mount) # rubocop:disable GitlabSecurity/PublicSend redirect_to @uploader.url unless @uploader.file_storage? end diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 69220a1c0f6..72e26b64e60 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -128,10 +128,10 @@ module CommitsHelper # avatar: true will prepend the avatar image # size: size of the avatar image in px def commit_person_link(commit, options = {}) - user = commit.send(options[:source]) + user = commit.public_send(options[:source]) # rubocop:disable GitlabSecurity/PublicSend - source_name = clean(commit.send "#{options[:source]}_name".to_sym) - source_email = clean(commit.send "#{options[:source]}_email".to_sym) + source_name = clean(commit.public_send(:"#{options[:source]}_name")) # rubocop:disable GitlabSecurity/PublicSend + source_email = clean(commit.public_send(:"#{options[:source]}_email")) # rubocop:disable GitlabSecurity/PublicSend person_name = user.try(:name) || source_name diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb index a57b5a8fea5..a18ebfb6030 100644 --- a/app/helpers/import_helper.rb +++ b/app/helpers/import_helper.rb @@ -5,7 +5,7 @@ module ImportHelper end def provider_project_link(provider, path_with_namespace) - url = __send__("#{provider}_project_url", path_with_namespace) + url = __send__("#{provider}_project_url", path_with_namespace) # rubocop:disable GitlabSecurity/PublicSend link_to path_with_namespace, url, target: '_blank', rel: 'noopener noreferrer' end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 70ea35fab1e..197c90c4081 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -174,7 +174,14 @@ module IssuablesHelper end def assigned_issuables_count(issuable_type) - current_user.public_send("assigned_open_#{issuable_type}_count") + case issuable_type + when :issues + current_user.assigned_open_issues_count + when :merge_requests + current_user.assigned_open_merge_requests_count + else + raise ArgumentError, "invalid issuable `#{issuable_type}`" + end end def issuable_filter_params @@ -298,10 +305,6 @@ module IssuablesHelper cookies[:collapsed_gutter] == 'true' end - def base_issuable_scope(issuable) - issuable.project.send(issuable.class.table_name).send(issuable_state_scope(issuable)) - end - def issuable_state_scope(issuable) if issuable.respond_to?(:merged?) && issuable.merged? :merged diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index f8860bfee99..86666022a2a 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -32,7 +32,18 @@ module MilestonesHelper end def milestone_issues_by_label_count(milestone, label, state:) - milestone.issues.with_label(label.title).send(state).size + issues = milestone.issues.with_label(label.title) + issues = + case state + when :opened + issues.opened + when :closed + issues.closed + else + raise ArgumentError, "invalid milestone state `#{state}`" + end + + issues.size end # Returns count of milestones for different states diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index a268413e84f..6c5f98f74dc 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -149,15 +149,16 @@ module ProjectsHelper # Don't show option "everyone with access" if project is private options = project_feature_options + level = @project.project_feature.public_send(field) # rubocop:disable GitlabSecurity/PublicSend + if @project.private? - level = @project.project_feature.send(field) disabled_option = ProjectFeature::ENABLED highest_available_option = ProjectFeature::PRIVATE if level == disabled_option end options = options_for_select( options.invert, - selected: highest_available_option || @project.project_feature.public_send(field), + selected: highest_available_option || level, disabled: disabled_option ) @@ -486,7 +487,7 @@ module ProjectsHelper end def filename_path(project, filename) - if project && blob = project.repository.send(filename) + if project && blob = project.repository.public_send(filename) # rubocop:disable GitlabSecurity/PublicSend project_blob_path( project, tree_join(project.default_branch, blob.name) diff --git a/app/models/commit.rb b/app/models/commit.rb index 638fddc5d3d..5ca2f150247 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -200,7 +200,7 @@ class Commit end def method_missing(m, *args, &block) - @raw.send(m, *args, &block) + @raw.__send__(m, *args, &block) # rubocop:disable GitlabSecurity/PublicSend end def respond_to_missing?(method, include_private = false) diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb index 48547a938fc..193e459977a 100644 --- a/app/models/concerns/cache_markdown_field.rb +++ b/app/models/concerns/cache_markdown_field.rb @@ -78,7 +78,7 @@ module CacheMarkdownField def cached_html_up_to_date?(markdown_field) html_field = cached_markdown_fields.html_field(markdown_field) - cached = cached_html_for(markdown_field).present? && __send__(markdown_field).present? + cached = cached_html_for(markdown_field).present? && __send__(markdown_field).present? # rubocop:disable GitlabSecurity/PublicSend return false unless cached markdown_changed = attribute_changed?(markdown_field) || false @@ -93,14 +93,14 @@ module CacheMarkdownField end def attribute_invalidated?(attr) - __send__("#{attr}_invalidated?") + __send__("#{attr}_invalidated?") # rubocop:disable GitlabSecurity/PublicSend end def cached_html_for(markdown_field) raise ArgumentError.new("Unknown field: #{field}") unless cached_markdown_fields.markdown_fields.include?(markdown_field) - __send__(cached_markdown_fields.html_field(markdown_field)) + __send__(cached_markdown_fields.html_field(markdown_field)) # rubocop:disable GitlabSecurity/PublicSend end included do diff --git a/app/models/concerns/internal_id.rb b/app/models/concerns/internal_id.rb index 67a0adfcd56..a3d0ac8d862 100644 --- a/app/models/concerns/internal_id.rb +++ b/app/models/concerns/internal_id.rb @@ -9,7 +9,7 @@ module InternalId def set_iid if iid.blank? parent = project || group - records = parent.send(self.class.name.tableize) + records = parent.public_send(self.class.name.tableize) # rubocop:disable GitlabSecurity/PublicSend records = records.with_deleted if self.paranoid? max_iid = records.maximum(:iid) diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index c034bf9cbc0..1db6b2d2fa2 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -56,7 +56,7 @@ module Mentionable end self.class.mentionable_attrs.each do |attr, options| - text = __send__(attr) + text = __send__(attr) # rubocop:disable GitlabSecurity/PublicSend options = options.merge( cache_key: [self, attr], author: author, @@ -100,7 +100,7 @@ module Mentionable end self.class.mentionable_attrs.any? do |attr, _| - __send__(attr) =~ reference_pattern + __send__(attr) =~ reference_pattern # rubocop:disable GitlabSecurity/PublicSend end end diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb index 4865c0a14b1..ce69fd34ac5 100644 --- a/app/models/concerns/participable.rb +++ b/app/models/concerns/participable.rb @@ -82,7 +82,7 @@ module Participable if attr.respond_to?(:call) source.instance_exec(current_user, ext, &attr) else - process << source.__send__(attr) + process << source.__send__(attr) # rubocop:disable GitlabSecurity/PublicSend end end when Enumerable, ActiveRecord::Relation diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb index 60734bc6660..cb59b4da3d7 100644 --- a/app/models/concerns/project_features_compatibility.rb +++ b/app/models/concerns/project_features_compatibility.rb @@ -32,6 +32,6 @@ module ProjectFeaturesCompatibility build_project_feature unless project_feature access_level = Gitlab::Utils.to_boolean(value) ? ProjectFeature::ENABLED : ProjectFeature::DISABLED - project_feature.send(:write_attribute, field, access_level) + project_feature.__send__(:write_attribute, field, access_level) # rubocop:disable GitlabSecurity/PublicSend end end diff --git a/app/models/network/commit.rb b/app/models/network/commit.rb index 8417f200e36..9357e55b419 100644 --- a/app/models/network/commit.rb +++ b/app/models/network/commit.rb @@ -12,7 +12,7 @@ module Network end def method_missing(m, *args, &block) - @commit.send(m, *args, &block) + @commit.__send__(m, *args, &block) # rubocop:disable GitlabSecurity/PublicSend end def space diff --git a/app/models/project.rb b/app/models/project.rb index 7010664e1c8..e04663a31f3 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -921,14 +921,14 @@ class Project < ActiveRecord::Base end def execute_hooks(data, hooks_scope = :push_hooks) - hooks.send(hooks_scope).each do |hook| + hooks.public_send(hooks_scope).each do |hook| # rubocop:disable GitlabSecurity/PublicSend hook.async_execute(data, hooks_scope.to_s) end end def execute_services(data, hooks_scope = :push_hooks) # Call only service hooks that are active for this scope - services.send(hooks_scope).each do |service| + services.public_send(hooks_scope).each do |service| # rubocop:disable GitlabSecurity/PublicSend service.async_execute(data) end end diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb index 6d1a321f651..7b15a5dd04d 100644 --- a/app/models/project_services/chat_notification_service.rb +++ b/app/models/project_services/chat_notification_service.rb @@ -115,7 +115,7 @@ class ChatNotificationService < Service def get_channel_field(event) field_name = event_channel_name(event) - self.public_send(field_name) + self.public_send(field_name) # rubocop:disable GitlabSecurity/PublicSend end def build_event_channels diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index e3906943ecd..f422e0ea036 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -53,7 +53,7 @@ class HipchatService < Service return unless supported_events.include?(data[:object_kind]) message = create_message(data) return unless message.present? - gate[room].send('GitLab', message, message_options(data)) + gate[room].send('GitLab', message, message_options(data)) # rubocop:disable GitlabSecurity/PublicSend end def test(data) diff --git a/app/models/protectable_dropdown.rb b/app/models/protectable_dropdown.rb index 122fbce257d..c96edc5a259 100644 --- a/app/models/protectable_dropdown.rb +++ b/app/models/protectable_dropdown.rb @@ -1,5 +1,9 @@ class ProtectableDropdown + REF_TYPES = %i[branches tags].freeze + def initialize(project, ref_type) + raise ArgumentError, "invalid ref type `#{ref_type}`" unless ref_type.in?(REF_TYPES) + @project = project @ref_type = ref_type end @@ -16,7 +20,7 @@ class ProtectableDropdown private def refs - @project.repository.public_send(@ref_type) + @project.repository.public_send(@ref_type) # rubocop:disable GitlabSecurity/PublicSend end def ref_names @@ -24,7 +28,7 @@ class ProtectableDropdown end def protections - @project.public_send("protected_#{@ref_type}") + @project.public_send("protected_#{@ref_type}") # rubocop:disable GitlabSecurity/PublicSend end def non_wildcard_protected_ref_names diff --git a/app/models/repository.rb b/app/models/repository.rb index 049bebdbe42..0ac3c382f17 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -48,7 +48,9 @@ class Repository alias_method(original, name) define_method(name) do - cache_method_output(name, fallback: fallback, memoize_only: memoize_only) { __send__(original) } + cache_method_output(name, fallback: fallback, memoize_only: memoize_only) do + __send__(original) # rubocop:disable GitlabSecurity/PublicSend + end end end @@ -439,9 +441,9 @@ class Repository def method_missing(m, *args, &block) if m == :lookup && !block_given? lookup_cache[m] ||= {} - lookup_cache[m][args.join(":")] ||= raw_repository.send(m, *args, &block) + lookup_cache[m][args.join(":")] ||= raw_repository.__send__(m, *args, &block) # rubocop:disable GitlabSecurity/PublicSend else - raw_repository.send(m, *args, &block) + raw_repository.__send__(m, *args, &block) # rubocop:disable GitlabSecurity/PublicSend end end @@ -772,7 +774,7 @@ class Repository end actions.each do |options| - index.public_send(options.delete(:action), options) + index.public_send(options.delete(:action), options) # rubocop:disable GitlabSecurity/PublicSend end options = { diff --git a/app/models/user.rb b/app/models/user.rb index a4615436245..0a2cfeb7f3e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1070,7 +1070,7 @@ class User < ActiveRecord::Base # Added according to https://github.com/plataformatec/devise/blob/7df57d5081f9884849ca15e4fde179ef164a575f/README.md#activejob-integration def send_devise_notification(notification, *args) return true unless can?(:receive_notifications) - devise_mailer.send(notification, self, *args).deliver_later + devise_mailer.__send__(notification, self, *args).deliver_later # rubocop:disable GitlabSecurity/PublicSend end # This works around a bug in Devise 4.2.0 that erroneously causes a user to diff --git a/app/services/akismet_service.rb b/app/services/akismet_service.rb index 8e11a2a36a7..59153cbbc0a 100644 --- a/app/services/akismet_service.rb +++ b/app/services/akismet_service.rb @@ -58,7 +58,7 @@ class AkismetService } begin - akismet_client.public_send(type, options[:ip_address], options[:user_agent], params) + akismet_client.public_send(type, options[:ip_address], options[:user_agent], params) # rubocop:disable GitlabSecurity/PublicSend true rescue => e Rails.logger.error("Unable to connect to Akismet: #{e}, skipping!") diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb index 6372e5755db..ea3b8d66ed9 100644 --- a/app/services/ci/retry_build_service.rb +++ b/app/services/ci/retry_build_service.rb @@ -23,7 +23,7 @@ module Ci end attributes = CLONE_ACCESSORS.map do |attribute| - [attribute, build.send(attribute)] + [attribute, build.public_send(attribute)] # rubocop:disable GitlabSecurity/PublicSend end attributes.push([:user, current_user]) diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb index a48d6a976f0..85c2fcf9ea6 100644 --- a/app/services/commits/change_service.rb +++ b/app/services/commits/change_service.rb @@ -11,6 +11,7 @@ module Commits def commit_change(action) raise NotImplementedError unless repository.respond_to?(action) + # rubocop:disable GitlabSecurity/PublicSend repository.public_send( action, current_user, diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index b84a6fd2b7d..4a4f2b91182 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -338,7 +338,7 @@ class IssuableBaseService < BaseService def invalidate_cache_counts(issuable, users: [], skip_project_cache: false) users.each do |user| - user.public_send("invalidate_#{issuable.model_name.singular}_cache_counts") + user.public_send("invalidate_#{issuable.model_name.singular}_cache_counts") # rubocop:disable GitlabSecurity/PublicSend end unless skip_project_cache diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb index 2e089149ca8..46c505baf8b 100644 --- a/app/services/members/destroy_service.rb +++ b/app/services/members/destroy_service.rb @@ -31,7 +31,7 @@ module Members source.members.find_by(condition) || source.requesters.find_by!(condition) else - source.public_send(scope).find_by!(condition) + source.public_send(scope).find_by!(condition) # rubocop:disable GitlabSecurity/PublicSend end end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 4267879b03d..e2a80db06a6 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -1,3 +1,5 @@ +# rubocop:disable GitlabSecurity/PublicSend + # NotificationService class # # Used for notifying users with emails about different events diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb index cbcd4478af6..a1c2f8d0180 100644 --- a/app/services/system_hooks_service.rb +++ b/app/services/system_hooks_service.rb @@ -4,7 +4,7 @@ class SystemHooksService end def execute_hooks(data, hooks_scope = :all) - SystemHook.public_send(hooks_scope).find_each do |hook| + SystemHook.public_send(hooks_scope).find_each do |hook| # rubocop:disable GitlabSecurity/PublicSend hook.async_execute(data, 'system_hooks') end end diff --git a/app/services/test_hooks/base_service.rb b/app/services/test_hooks/base_service.rb index 74ba814afff..4abd2c44b2f 100644 --- a/app/services/test_hooks/base_service.rb +++ b/app/services/test_hooks/base_service.rb @@ -18,7 +18,7 @@ module TestHooks end error_message = catch(:validation_error) do - sample_data = self.__send__(trigger_data_method) + sample_data = self.__send__(trigger_data_method) # rubocop:disable GitlabSecurity/PublicSend return hook.execute(sample_data, trigger) end diff --git a/app/workers/gitlab_shell_worker.rb b/app/workers/gitlab_shell_worker.rb index 964287a1793..0ec871e00e1 100644 --- a/app/workers/gitlab_shell_worker.rb +++ b/app/workers/gitlab_shell_worker.rb @@ -4,6 +4,6 @@ class GitlabShellWorker include DedicatedSidekiqQueue def perform(action, *arg) - gitlab_shell.send(action, *arg) + gitlab_shell.__send__(action, *arg) # rubocop:disable GitlabSecurity/PublicSend end end diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 5c6578d3531..38ade18bdc0 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -1,3 +1,5 @@ +# rubocop:disable GitlabSecurity/PublicSend + require_dependency Rails.root.join('lib/gitlab') # Load Gitlab as soon as possible class Settings < Settingslogic diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb index 0d2d71e336a..c4c0fdda665 100644 --- a/lib/api/api_guard.rb +++ b/lib/api/api_guard.rb @@ -122,7 +122,7 @@ module API error_classes = [MissingTokenError, TokenNotFoundError, ExpiredError, RevokedError, InsufficientScopeError] - base.send :rescue_from, *error_classes, oauth2_bearer_token_error_handler + base.__send__(:rescue_from, *error_classes, oauth2_bearer_token_error_handler) # rubocop:disable GitlabSecurity/PublicSend end def oauth2_bearer_token_error_handler diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 18cd604a216..716e3f11744 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -541,8 +541,9 @@ module API target_url = "namespace_project_#{target_type}_url" target_anchor = "note_#{todo.note_id}" if todo.note_id? - Gitlab::Routing.url_helpers.public_send(target_url, - todo.project.namespace, todo.project, todo.target, anchor: target_anchor) + Gitlab::Routing + .url_helpers + .public_send(target_url, todo.project.namespace, todo.project, todo.target, anchor: target_anchor) # rubocop:disable GitlabSecurity/PublicSend end expose :body diff --git a/lib/api/runners.rb b/lib/api/runners.rb index 5bf5a18e42f..31f940fe96b 100644 --- a/lib/api/runners.rb +++ b/lib/api/runners.rb @@ -153,7 +153,7 @@ module API render_api_error!('Scope contains invalid value', 400) end - runners.send(scope) + runners.public_send(scope) # rubocop:disable GitlabSecurity/PublicSend end def get_runner(id) diff --git a/lib/api/v3/notes.rb b/lib/api/v3/notes.rb index 23fe95e42e4..d49772b92f2 100644 --- a/lib/api/v3/notes.rb +++ b/lib/api/v3/notes.rb @@ -22,7 +22,7 @@ module API use :pagination end get ":id/#{noteables_str}/:noteable_id/notes" do - noteable = user_project.send(noteables_str.to_sym).find(params[:noteable_id]) + noteable = user_project.public_send(noteables_str.to_sym).find(params[:noteable_id]) # rubocop:disable GitlabSecurity/PublicSend if can?(current_user, noteable_read_ability_name(noteable), noteable) # We exclude notes that are cross-references and that cannot be viewed @@ -50,7 +50,7 @@ module API requires :noteable_id, type: Integer, desc: 'The ID of the noteable' end get ":id/#{noteables_str}/:noteable_id/notes/:note_id" do - noteable = user_project.send(noteables_str.to_sym).find(params[:noteable_id]) + noteable = user_project.public_send(noteables_str.to_sym).find(params[:noteable_id]) # rubocop:disable GitlabSecurity/PublicSend note = noteable.notes.find(params[:note_id]) can_read_note = can?(current_user, noteable_read_ability_name(noteable), noteable) && !note.cross_reference_not_visible_for?(current_user) @@ -76,7 +76,7 @@ module API noteable_id: params[:noteable_id] } - noteable = user_project.send(noteables_str.to_sym).find(params[:noteable_id]) + noteable = user_project.public_send(noteables_str.to_sym).find(params[:noteable_id]) # rubocop:disable GitlabSecurity/PublicSend if can?(current_user, noteable_read_ability_name(noteable), noteable) if params[:created_at] && (current_user.admin? || user_project.owner == current_user) diff --git a/lib/banzai/filter/external_issue_reference_filter.rb b/lib/banzai/filter/external_issue_reference_filter.rb index 53a229256a5..ed01a72ff9f 100644 --- a/lib/banzai/filter/external_issue_reference_filter.rb +++ b/lib/banzai/filter/external_issue_reference_filter.rb @@ -95,10 +95,10 @@ module Banzai private def external_issues_cached(attribute) - return project.public_send(attribute) unless RequestStore.active? + return project.public_send(attribute) unless RequestStore.active? # rubocop:disable GitlabSecurity/PublicSend cached_attributes = RequestStore[:banzai_external_issues_tracker_attributes] ||= Hash.new { |h, k| h[k] = {} } - cached_attributes[project.id][attribute] = project.public_send(attribute) if cached_attributes[project.id][attribute].nil? + cached_attributes[project.id][attribute] = project.public_send(attribute) if cached_attributes[project.id][attribute].nil? # rubocop:disable GitlabSecurity/PublicSend cached_attributes[project.id][attribute] end end diff --git a/lib/banzai/object_renderer.rb b/lib/banzai/object_renderer.rb index 002a3341ccd..2196a92474c 100644 --- a/lib/banzai/object_renderer.rb +++ b/lib/banzai/object_renderer.rb @@ -37,7 +37,7 @@ module Banzai objects.each_with_index do |object, index| redacted_data = redacted[index] - object.__send__("redacted_#{attribute}_html=", redacted_data[:document].to_html.html_safe) + object.__send__("redacted_#{attribute}_html=", redacted_data[:document].to_html.html_safe) # rubocop:disable GitlabSecurity/PublicSend object.user_visible_reference_count = redacted_data[:visible_reference_count] end end diff --git a/lib/banzai/pipeline/base_pipeline.rb b/lib/banzai/pipeline/base_pipeline.rb index 321fd5bbe14..3ae3bed570d 100644 --- a/lib/banzai/pipeline/base_pipeline.rb +++ b/lib/banzai/pipeline/base_pipeline.rb @@ -18,7 +18,7 @@ module Banzai define_method(meth) do |text, context| context = transform_context(context) - html_pipeline.send(meth, text, context) + html_pipeline.__send__(meth, text, context) # rubocop:disable GitlabSecurity/PublicSend end end end diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb index ad08c0905e2..95d82d17658 100644 --- a/lib/banzai/renderer.rb +++ b/lib/banzai/renderer.rb @@ -43,7 +43,7 @@ module Banzai # Same as +render_field+, but without consulting or updating the cache field def self.cacheless_render_field(object, field, options = {}) - text = object.__send__(field) + text = object.__send__(field) # rubocop:disable GitlabSecurity/PublicSend context = object.banzai_render_context(field).merge(options) cacheless_render(text, context) @@ -156,7 +156,7 @@ module Banzai # method. def self.full_cache_multi_key(cache_key, pipeline_name) return unless cache_key - Rails.cache.send(:expanded_key, full_cache_key(cache_key, pipeline_name)) + Rails.cache.__send__(:expanded_key, full_cache_key(cache_key, pipeline_name)) # rubocop:disable GitlabSecurity/PublicSend end # GitLab EE needs to disable updates on GET requests in Geo diff --git a/lib/bitbucket/collection.rb b/lib/bitbucket/collection.rb index 3a9379ff680..a78495dbf5e 100644 --- a/lib/bitbucket/collection.rb +++ b/lib/bitbucket/collection.rb @@ -13,7 +13,7 @@ module Bitbucket def method_missing(method, *args) return super unless self.respond_to?(method) - self.send(method, *args) do |item| + self.__send__(method, *args) do |item| # rubocop:disable GitlabSecurity/PublicSend block_given? ? yield(item) : item end end diff --git a/lib/ci/ansi2html.rb b/lib/ci/ansi2html.rb index 8354fc8d595..b9e9f9f7f4a 100644 --- a/lib/ci/ansi2html.rb +++ b/lib/ci/ansi2html.rb @@ -208,7 +208,7 @@ module Ci return unless command = stack.shift() if self.respond_to?("on_#{command}", true) - self.send("on_#{command}", stack) + self.__send__("on_#{command}", stack) # rubocop:disable GitlabSecurity/PublicSend end evaluate_command_stack(stack) diff --git a/lib/declarative_policy/base.rb b/lib/declarative_policy/base.rb index df94cafb6a1..e544aefa63a 100644 --- a/lib/declarative_policy/base.rb +++ b/lib/declarative_policy/base.rb @@ -109,7 +109,7 @@ module DeclarativePolicy name = name.to_sym if delegation_block.nil? - delegation_block = proc { @subject.__send__(name) } + delegation_block = proc { @subject.__send__(name) } # rubocop:disable GitlabSecurity/PublicSend end own_delegations[name] = delegation_block diff --git a/lib/declarative_policy/dsl.rb b/lib/declarative_policy/dsl.rb index b26807a7622..6ba1e7a3c5c 100644 --- a/lib/declarative_policy/dsl.rb +++ b/lib/declarative_policy/dsl.rb @@ -93,7 +93,7 @@ module DeclarativePolicy def method_missing(m, *a, &b) return super unless @context_class.respond_to?(m) - @context_class.__send__(m, *a, &b) + @context_class.__send__(m, *a, &b) # rubocop:disable GitlabSecurity/PublicSend end def respond_to_missing?(m) diff --git a/lib/file_size_validator.rb b/lib/file_size_validator.rb index eb19ab45ac3..de391de9059 100644 --- a/lib/file_size_validator.rb +++ b/lib/file_size_validator.rb @@ -44,13 +44,13 @@ class FileSizeValidator < ActiveModel::EachValidator when Integer check_value when Symbol - record.send(check_value) + record.public_send(check_value) # rubocop:disable GitlabSecurity/PublicSend end value ||= [] if key == :maximum value_size = value.size - next if value_size.send(validity_check, check_value) + next if value_size.public_send(validity_check, check_value) # rubocop:disable GitlabSecurity/PublicSend errors_options = options.except(*RESERVED_OPTIONS) errors_options[:file_size] = help.number_to_human_size check_value diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index 7d3aa532750..8cb4060cd97 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -101,7 +101,7 @@ module Gitlab if Service.available_services_names.include?(underscored_service) # We treat underscored_service as a trusted input because it is included # in the Service.available_services_names whitelist. - service = project.public_send("#{underscored_service}_service") + service = project.public_send("#{underscored_service}_service") # rubocop:disable GitlabSecurity/PublicSend if service && service.activated? && service.valid_token?(password) Gitlab::Auth::Result.new(nil, project, :ci, build_authentication_abilities) @@ -149,7 +149,7 @@ module Gitlab def abilities_for_scope(scopes) scopes.map do |scope| - self.public_send(:"#{scope}_scope_authentication_abilities") + self.public_send(:"#{scope}_scope_authentication_abilities") # rubocop:disable GitlabSecurity/PublicSend end.flatten.uniq end diff --git a/lib/gitlab/cache/request_cache.rb b/lib/gitlab/cache/request_cache.rb index f1a04affd38..754a45c3257 100644 --- a/lib/gitlab/cache/request_cache.rb +++ b/lib/gitlab/cache/request_cache.rb @@ -69,7 +69,7 @@ module Gitlab instance_variable_set(ivar_name, {}) end - key = __send__(cache_key_method_name, args) + key = __send__(cache_key_method_name, args) # rubocop:disable GitlabSecurity/PublicSend store.fetch(key) { store[key] = super(*args) } end diff --git a/lib/gitlab/diff/line_mapper.rb b/lib/gitlab/diff/line_mapper.rb index 576a761423e..cf71d47df8e 100644 --- a/lib/gitlab/diff/line_mapper.rb +++ b/lib/gitlab/diff/line_mapper.rb @@ -38,7 +38,7 @@ module Gitlab # - The first diff line with a higher line number, if it falls between diff contexts # - The last known diff line, if it falls after the last diff context diff_line = diff_lines.find do |diff_line| - diff_from_line = diff_line.send(from) + diff_from_line = diff_line.public_send(from) # rubocop:disable GitlabSecurity/PublicSend diff_from_line && diff_from_line >= from_line end diff_line ||= diff_lines.last @@ -47,8 +47,8 @@ module Gitlab # mapped line number is the same as the specified line number. return from_line unless diff_line - diff_from_line = diff_line.send(from) - diff_to_line = diff_line.send(to) + diff_from_line = diff_line.public_send(from) # rubocop:disable GitlabSecurity/PublicSend + diff_to_line = diff_line.public_send(to) # rubocop:disable GitlabSecurity/PublicSend # If the line was removed, there is no mapped line number. return unless diff_to_line diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb index 77b81d2d437..59e95191464 100644 --- a/lib/gitlab/git/blob.rb +++ b/lib/gitlab/git/blob.rb @@ -173,7 +173,7 @@ module Gitlab def initialize(options) %w(id name path size data mode commit_id binary).each do |key| - self.send("#{key}=", options[key.to_sym]) + self.__send__("#{key}=", options[key.to_sym]) # rubocop:disable GitlabSecurity/PublicSend end @loaded_all_data = false diff --git a/lib/gitlab/git/tree.rb b/lib/gitlab/git/tree.rb index 8e959c57c7c..b54962a4456 100644 --- a/lib/gitlab/git/tree.rb +++ b/lib/gitlab/git/tree.rb @@ -89,7 +89,7 @@ module Gitlab def initialize(options) %w(id root_id name path type mode commit_id).each do |key| - self.send("#{key}=", options[key.to_sym]) + self.send("#{key}=", options[key.to_sym]) # rubocop:disable GitlabSecurity/PublicSend end end diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb index 70177cd0fec..9a5f4f598b2 100644 --- a/lib/gitlab/gitaly_client.rb +++ b/lib/gitlab/gitaly_client.rb @@ -55,7 +55,7 @@ module Gitlab def self.call(storage, service, rpc, request) metadata = request_metadata(storage) metadata = yield(metadata) if block_given? - stub(service, storage).send(rpc, request, metadata) + stub(service, storage).__send__(rpc, request, metadata) # rubocop:disable GitlabSecurity/PublicSend end def self.request_metadata(storage) diff --git a/lib/gitlab/github_import/base_formatter.rb b/lib/gitlab/github_import/base_formatter.rb index 8c80791e7c9..f330041cc00 100644 --- a/lib/gitlab/github_import/base_formatter.rb +++ b/lib/gitlab/github_import/base_formatter.rb @@ -11,7 +11,9 @@ module Gitlab end def create! - project.public_send(project_association).find_or_create_by!(find_condition) do |record| + association = project.public_send(project_association) # rubocop:disable GitlabSecurity/PublicSend + + association.find_or_create_by!(find_condition) do |record| record.attributes = attributes end end diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb index 7dbeec5b010..0550f9695bd 100644 --- a/lib/gitlab/github_import/client.rb +++ b/lib/gitlab/github_import/client.rb @@ -120,7 +120,7 @@ module Gitlab def request(method, *args, &block) sleep rate_limit_sleep_time if rate_limit_exceed? - data = api.send(method, *args) + data = api.__send__(method, *args) # rubocop:disable GitlabSecurity/PublicSend return data unless data.is_a?(Array) last_response = api.last_response diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 266b1a6fece..373062b354b 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -289,7 +289,7 @@ module Gitlab opts.last[:page] = current_page(resource_type) - client.public_send(resource_type, *opts) do |resources| + client.public_send(resource_type, *opts) do |resources| # rubocop:disable GitlabSecurity/PublicSend yield resources increment_page(resource_type) end diff --git a/lib/gitlab/lazy.rb b/lib/gitlab/lazy.rb index 2a659ae4c74..99594577141 100644 --- a/lib/gitlab/lazy.rb +++ b/lib/gitlab/lazy.rb @@ -16,7 +16,7 @@ module Gitlab def method_missing(name, *args, &block) __evaluate__ - @result.__send__(name, *args, &block) + @result.__send__(name, *args, &block) # rubocop:disable GitlabSecurity/PublicSend end def respond_to_missing?(name, include_private = false) diff --git a/lib/gitlab/ldap/person.rb b/lib/gitlab/ldap/person.rb index 43eb73250b7..e138b466a34 100644 --- a/lib/gitlab/ldap/person.rb +++ b/lib/gitlab/ldap/person.rb @@ -32,7 +32,7 @@ module Gitlab end def uid - entry.send(config.uid).first + entry.public_send(config.uid).first # rubocop:disable GitlabSecurity/PublicSend end def username @@ -65,7 +65,7 @@ module Gitlab return nil unless selected_attr - entry.public_send(selected_attr) + entry.public_send(selected_attr) # rubocop:disable GitlabSecurity/PublicSend end end end diff --git a/lib/gitlab/markdown/pipeline.rb b/lib/gitlab/markdown/pipeline.rb index 699d8b9fc07..306923902e0 100644 --- a/lib/gitlab/markdown/pipeline.rb +++ b/lib/gitlab/markdown/pipeline.rb @@ -23,7 +23,7 @@ module Gitlab define_method(meth) do |text, context| context = transform_context(context) - html_pipeline.send(meth, text, context) + html_pipeline.__send__(meth, text, context) # rubocop:disable GitlabSecurity/PublicSend end end end diff --git a/lib/uploaded_file.rb b/lib/uploaded_file.rb index 41dee5fdc06..4a3c40f88eb 100644 --- a/lib/uploaded_file.rb +++ b/lib/uploaded_file.rb @@ -27,7 +27,7 @@ class UploadedFile alias_method :local_path, :path def method_missing(method_name, *args, &block) #:nodoc: - @tempfile.__send__(method_name, *args, &block) + @tempfile.__send__(method_name, *args, &block) # rubocop:disable GitlabSecurity/PublicSend end def respond_to?(method_name, include_private = false) #:nodoc: diff --git a/qa/qa/runtime/release.rb b/qa/qa/runtime/release.rb index 4f83a773645..12e56404cf6 100644 --- a/qa/qa/runtime/release.rb +++ b/qa/qa/runtime/release.rb @@ -21,7 +21,7 @@ module QA end def self.method_missing(name, *args) - self.new.strategy.public_send(name, *args) + self.new.strategy.public_send(name, *args) # rubocop:disable GitlabSecurity/PublicSend end end end diff --git a/spec/lib/file_size_validator_spec.rb b/spec/lib/file_size_validator_spec.rb index 49501931dd2..c44bc1840df 100644 --- a/spec/lib/file_size_validator_spec.rb +++ b/spec/lib/file_size_validator_spec.rb @@ -24,13 +24,13 @@ describe FileSizeValidator do describe 'options uses a symbol' do let(:options) do { - maximum: :test, + maximum: :max_attachment_size, attributes: { attachment: attachment } } end before do - allow(note).to receive(:test) { 10 } + expect(note).to receive(:max_attachment_size) { 10 } end it 'attachment exceeds maximum limit' do diff --git a/spec/models/protectable_dropdown_spec.rb b/spec/models/protectable_dropdown_spec.rb index 5c5dcd9f5c9..d4433a88a15 100644 --- a/spec/models/protectable_dropdown_spec.rb +++ b/spec/models/protectable_dropdown_spec.rb @@ -4,6 +4,13 @@ describe ProtectableDropdown do let(:project) { create(:project, :repository) } let(:subject) { described_class.new(project, :branches) } + describe 'initialize' do + it 'raises ArgumentError for invalid ref type' do + expect { described_class.new(double, :foo) } + .to raise_error(ArgumentError, "invalid ref type `foo`") + end + end + describe '#protectable_ref_names' do before do project.protected_branches.create(name: 'master') From e99444bb2d3a249461825550fc6271f4e0ee8874 Mon Sep 17 00:00:00 2001 From: vanadium23 Date: Mon, 7 Aug 2017 21:00:11 +0300 Subject: [PATCH 04/32] Fix CI_PROJECT_PATH_SLUG slugify --- app/models/ci/build.rb | 5 +---- app/models/project.rb | 6 +++++- .../34643-fix-project-path-slugify.yml | 4 ++++ lib/gitlab/utils.rb | 13 +++++++++++++ spec/lib/gitlab/utils_spec.rb | 16 +++++++++++++++- spec/models/ci/build_spec.rb | 2 +- 6 files changed, 39 insertions(+), 7 deletions(-) create mode 100644 changelogs/unreleased/34643-fix-project-path-slugify.yml diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 8be2dee6479..4692fb5644a 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -194,10 +194,7 @@ module Ci # * Maximum length is 63 bytes # * First/Last Character is not a hyphen def ref_slug - ref.to_s - .downcase - .gsub(/[^a-z0-9]/, '-')[0..62] - .gsub(/(\A-+|-+\z)/, '') + Gitlab::Utils.slugify(ref.to_s) end # Variables whose value does not depend on environment diff --git a/app/models/project.rb b/app/models/project.rb index 7cdd00bc17b..6cb04478b6f 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1283,12 +1283,16 @@ class Project < ActiveRecord::Base status.zero? end + def full_path_slug + Gitlab::Utils.slugify(full_path.to_s) + end + def predefined_variables [ { key: 'CI_PROJECT_ID', value: id.to_s, public: true }, { key: 'CI_PROJECT_NAME', value: path, public: true }, { key: 'CI_PROJECT_PATH', value: full_path, public: true }, - { key: 'CI_PROJECT_PATH_SLUG', value: full_path.parameterize, public: true }, + { key: 'CI_PROJECT_PATH_SLUG', value: full_path_slug, public: true }, { key: 'CI_PROJECT_NAMESPACE', value: namespace.full_path, public: true }, { key: 'CI_PROJECT_URL', value: web_url, public: true } ] diff --git a/changelogs/unreleased/34643-fix-project-path-slugify.yml b/changelogs/unreleased/34643-fix-project-path-slugify.yml new file mode 100644 index 00000000000..f7018a1aca5 --- /dev/null +++ b/changelogs/unreleased/34643-fix-project-path-slugify.yml @@ -0,0 +1,4 @@ +--- +title: Fix CI_PROJECT_PATH_SLUG slugify +merge_request: 13350 +author: Ivan Chernov diff --git a/lib/gitlab/utils.rb b/lib/gitlab/utils.rb index fa182c4deda..9670c93759e 100644 --- a/lib/gitlab/utils.rb +++ b/lib/gitlab/utils.rb @@ -14,6 +14,19 @@ module Gitlab str.force_encoding(Encoding::UTF_8) end + # A slugified version of the string, suitable for inclusion in URLs and + # domain names. Rules: + # + # * Lowercased + # * Anything not matching [a-z0-9-] is replaced with a - + # * Maximum length is 63 bytes + # * First/Last Character is not a hyphen + def slugify(str) + return str.downcase + .gsub(/[^a-z0-9]/, '-')[0..62] + .gsub(/(\A-+|-+\z)/, '') + end + def to_boolean(value) return value if [true, false].include?(value) return true if value =~ /^(true|t|yes|y|1|on)$/i diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb index 111c873f79c..92787bb262e 100644 --- a/spec/lib/gitlab/utils_spec.rb +++ b/spec/lib/gitlab/utils_spec.rb @@ -1,7 +1,21 @@ require 'spec_helper' describe Gitlab::Utils do - delegate :to_boolean, :boolean_to_yes_no, to: :described_class + delegate :to_boolean, :boolean_to_yes_no, :slugify, to: :described_class + + describe '.slugify' do + { + 'TEST' => 'test', + 'project_with_underscores' => 'project-with-underscores', + 'namespace/project' => 'namespace-project', + 'a' * 70 => 'a' * 63, + 'test_trailing_' => 'test-trailing' + }.each do |original, expected| + it "slugifies #{original} to #{expected}" do + expect(slugify(original)).to eq(expected) + end + end + end describe '.to_boolean' do it 'accepts booleans' do diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 86afa856ea7..767f0ad9e65 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1220,7 +1220,7 @@ describe Ci::Build do { key: 'CI_PROJECT_ID', value: project.id.to_s, public: true }, { key: 'CI_PROJECT_NAME', value: project.path, public: true }, { key: 'CI_PROJECT_PATH', value: project.full_path, public: true }, - { key: 'CI_PROJECT_PATH_SLUG', value: project.full_path.parameterize, public: true }, + { key: 'CI_PROJECT_PATH_SLUG', value: project.full_path_slug, public: true }, { key: 'CI_PROJECT_NAMESPACE', value: project.namespace.full_path, public: true }, { key: 'CI_PROJECT_URL', value: project.web_url, public: true }, { key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true }, From b5a773923e49d40fd06806541b89ecb947b45884 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 10 Aug 2017 09:10:25 +0100 Subject: [PATCH 05/32] Fix the fly-out menu in the sidebar not displaying in Safari --- app/assets/javascripts/fly_out_nav.js | 8 +- app/assets/stylesheets/new_sidebar.scss | 8 +- .../layouts/nav/_new_admin_sidebar.html.haml | 283 +++++------ .../layouts/nav/_new_group_sidebar.html.haml | 159 +++---- .../nav/_new_profile_sidebar.html.haml | 153 +++--- .../nav/_new_project_sidebar.html.haml | 441 +++++++++--------- spec/javascripts/fly_out_nav_spec.js | 6 +- 7 files changed, 538 insertions(+), 520 deletions(-) diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js index cbc3ad23990..32cb42c8b10 100644 --- a/app/assets/javascripts/fly_out_nav.js +++ b/app/assets/javascripts/fly_out_nav.js @@ -15,6 +15,10 @@ export const setOpenMenu = (menu = null) => { currentOpenMenu = menu; }; export const slope = (a, b) => (b.y - a.y) / (b.x - a.x); +let headerHeight = 50; + +export const getHeaderHeight = () => headerHeight; + export const canShowActiveSubItems = (el) => { const isHiddenByMedia = bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md'; @@ -74,7 +78,7 @@ export const moveSubItemsToPosition = (el, subItems) => { const isAbove = top < boundingRect.top; subItems.classList.add('fly-out-list'); - subItems.style.transform = `translate3d(0, ${Math.floor(top)}px, 0)`; // eslint-disable-line no-param-reassign + subItems.style.transform = `translate3d(0, ${Math.floor(top) - headerHeight}px, 0)`; // eslint-disable-line no-param-reassign const subItemsRect = subItems.getBoundingClientRect(); @@ -153,6 +157,8 @@ export default () => { }, getHideSubItemsInterval()); }); + headerHeight = document.querySelector('.nav-sidebar').offsetTop; + items.forEach((el) => { const subItems = el.querySelector('.sidebar-sub-level-items'); diff --git a/app/assets/stylesheets/new_sidebar.scss b/app/assets/stylesheets/new_sidebar.scss index faedd207e01..d078c8b956b 100644 --- a/app/assets/stylesheets/new_sidebar.scss +++ b/app/assets/stylesheets/new_sidebar.scss @@ -97,9 +97,9 @@ $new-sidebar-collapsed-width: 50px; top: $header-height; bottom: 0; left: 0; - overflow: auto; background-color: $gray-normal; box-shadow: inset -2px 0 0 $border-color; + transform: translate3d(0, 0, 0); &.sidebar-icons-only { width: $new-sidebar-collapsed-width; @@ -176,6 +176,12 @@ $new-sidebar-collapsed-width: 50px; } } +.nav-sidebar-inner-scroll { + height: 100%; + width: 100%; + overflow: auto; +} + .with-performance-bar .nav-sidebar { top: $header-height + $performance-bar-height; } diff --git a/app/views/layouts/nav/_new_admin_sidebar.html.haml b/app/views/layouts/nav/_new_admin_sidebar.html.haml index 0b4a9d92bea..3cbcd841aff 100644 --- a/app/views/layouts/nav/_new_admin_sidebar.html.haml +++ b/app/views/layouts/nav/_new_admin_sidebar.html.haml @@ -1,150 +1,151 @@ .nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) } - .context-header - = link_to admin_root_path, title: 'Admin Overview' do - .avatar-container.s40.settings-avatar - = icon('wrench') - .project-title Admin Area - %ul.sidebar-top-level-items - = nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts), html_options: {class: 'home'}) do - = link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do - .nav-icon-container - = custom_icon('overview') - %span.nav-item-name - Overview - - %ul.sidebar-sub-level-items - = nav_link(controller: :dashboard, html_options: {class: 'home'}) do - = link_to admin_root_path, title: 'Overview' do - %span - Dashboard - = nav_link(controller: [:admin, :projects]) do - = link_to admin_projects_path, title: 'Projects' do - %span - Projects - = nav_link(controller: :users) do - = link_to admin_users_path, title: 'Users' do - %span - Users - = nav_link(controller: :groups) do - = link_to admin_groups_path, title: 'Groups' do - %span - Groups - = nav_link path: 'jobs#index' do - = link_to admin_jobs_path, title: 'Jobs' do - %span - Jobs - = nav_link path: ['runners#index', 'runners#show'] do - = link_to admin_runners_path, title: 'Runners' do - %span - Runners - = nav_link path: 'cohorts#index' do - = link_to admin_cohorts_path, title: 'Cohorts' do - %span - Cohorts - - = nav_link(controller: %w(conversational_development_index system_info background_jobs logs health_check requests_profiles)) do - = link_to admin_conversational_development_index_path, title: 'Monitoring' do - .nav-icon-container - = custom_icon('monitoring') - %span.nav-item-name - Monitoring - - %ul.sidebar-sub-level-items - = nav_link(controller: :conversational_development_index) do - = link_to admin_conversational_development_index_path, title: 'ConvDev Index' do - %span - ConvDev Index - = nav_link(controller: :system_info) do - = link_to admin_system_info_path, title: 'System Info' do - %span - System Info - = nav_link(controller: :background_jobs) do - = link_to admin_background_jobs_path, title: 'Background Jobs' do - %span - Background Jobs - = nav_link(controller: :logs) do - = link_to admin_logs_path, title: 'Logs' do - %span - Logs - = nav_link(controller: :health_check) do - = link_to admin_health_check_path, title: 'Health Check' do - %span - Health Check - = nav_link(controller: :requests_profiles) do - = link_to admin_requests_profiles_path, title: 'Requests Profiles' do - %span - Requests Profiles - - = nav_link(controller: :broadcast_messages) do - = link_to admin_broadcast_messages_path, title: 'Messages' do - .nav-icon-container - = custom_icon('messages') - %span.nav-item-name - Messages - = nav_link(controller: [:hooks, :hook_logs]) do - = link_to admin_hooks_path, title: 'Hooks' do - .nav-icon-container - = custom_icon('system_hooks') - %span.nav-item-name - System Hooks - - = nav_link(controller: :applications) do - = link_to admin_applications_path, title: 'Applications' do - .nav-icon-container - = custom_icon('applications') - %span.nav-item-name - Applications - - = nav_link(controller: :abuse_reports) do - = link_to admin_abuse_reports_path, title: "Abuse Reports" do - .nav-icon-container - = custom_icon('abuse_reports') - %span.nav-item-name - Abuse Reports - %span.badge.count= number_with_delimiter(AbuseReport.count(:all)) - - - if akismet_enabled? - = nav_link(controller: :spam_logs) do - = link_to admin_spam_logs_path, title: "Spam Logs" do + .nav-sidebar-inner-scroll + .context-header + = link_to admin_root_path, title: 'Admin Overview' do + .avatar-container.s40.settings-avatar + = icon('wrench') + .project-title Admin Area + %ul.sidebar-top-level-items + = nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts), html_options: {class: 'home'}) do + = link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do .nav-icon-container - = custom_icon('spam_logs') + = custom_icon('overview') %span.nav-item-name - Spam Logs + Overview - = nav_link(controller: :deploy_keys) do - = link_to admin_deploy_keys_path, title: 'Deploy Keys' do - .nav-icon-container - = custom_icon('key') - %span.nav-item-name - Deploy Keys + %ul.sidebar-sub-level-items + = nav_link(controller: :dashboard, html_options: {class: 'home'}) do + = link_to admin_root_path, title: 'Overview' do + %span + Dashboard + = nav_link(controller: [:admin, :projects]) do + = link_to admin_projects_path, title: 'Projects' do + %span + Projects + = nav_link(controller: :users) do + = link_to admin_users_path, title: 'Users' do + %span + Users + = nav_link(controller: :groups) do + = link_to admin_groups_path, title: 'Groups' do + %span + Groups + = nav_link path: 'jobs#index' do + = link_to admin_jobs_path, title: 'Jobs' do + %span + Jobs + = nav_link path: ['runners#index', 'runners#show'] do + = link_to admin_runners_path, title: 'Runners' do + %span + Runners + = nav_link path: 'cohorts#index' do + = link_to admin_cohorts_path, title: 'Cohorts' do + %span + Cohorts - = nav_link(controller: :services) do - = link_to admin_application_settings_services_path, title: 'Service Templates' do - .nav-icon-container - = custom_icon('service_templates') - %span.nav-item-name - Service Templates + = nav_link(controller: %w(conversational_development_index system_info background_jobs logs health_check requests_profiles)) do + = link_to admin_conversational_development_index_path, title: 'Monitoring' do + .nav-icon-container + = custom_icon('monitoring') + %span.nav-item-name + Monitoring - = nav_link(controller: :labels) do - = link_to admin_labels_path, title: 'Labels' do - .nav-icon-container - = custom_icon('labels') - %span.nav-item-name - Labels + %ul.sidebar-sub-level-items + = nav_link(controller: :conversational_development_index) do + = link_to admin_conversational_development_index_path, title: 'ConvDev Index' do + %span + ConvDev Index + = nav_link(controller: :system_info) do + = link_to admin_system_info_path, title: 'System Info' do + %span + System Info + = nav_link(controller: :background_jobs) do + = link_to admin_background_jobs_path, title: 'Background Jobs' do + %span + Background Jobs + = nav_link(controller: :logs) do + = link_to admin_logs_path, title: 'Logs' do + %span + Logs + = nav_link(controller: :health_check) do + = link_to admin_health_check_path, title: 'Health Check' do + %span + Health Check + = nav_link(controller: :requests_profiles) do + = link_to admin_requests_profiles_path, title: 'Requests Profiles' do + %span + Requests Profiles - = nav_link(controller: :appearances) do - = link_to admin_appearances_path, title: 'Appearances' do - .nav-icon-container - = custom_icon('appearance') - %span.nav-item-name - Appearance + = nav_link(controller: :broadcast_messages) do + = link_to admin_broadcast_messages_path, title: 'Messages' do + .nav-icon-container + = custom_icon('messages') + %span.nav-item-name + Messages + = nav_link(controller: [:hooks, :hook_logs]) do + = link_to admin_hooks_path, title: 'Hooks' do + .nav-icon-container + = custom_icon('system_hooks') + %span.nav-item-name + System Hooks - %li.divider - = nav_link(controller: :application_settings) do - = link_to admin_application_settings_path, title: 'Settings' do - .nav-icon-container - = custom_icon('settings') - %span.nav-item-name - Settings + = nav_link(controller: :applications) do + = link_to admin_applications_path, title: 'Applications' do + .nav-icon-container + = custom_icon('applications') + %span.nav-item-name + Applications - = render 'shared/sidebar_toggle_button' + = nav_link(controller: :abuse_reports) do + = link_to admin_abuse_reports_path, title: "Abuse Reports" do + .nav-icon-container + = custom_icon('abuse_reports') + %span.nav-item-name + Abuse Reports + %span.badge.count= number_with_delimiter(AbuseReport.count(:all)) + + - if akismet_enabled? + = nav_link(controller: :spam_logs) do + = link_to admin_spam_logs_path, title: "Spam Logs" do + .nav-icon-container + = custom_icon('spam_logs') + %span.nav-item-name + Spam Logs + + = nav_link(controller: :deploy_keys) do + = link_to admin_deploy_keys_path, title: 'Deploy Keys' do + .nav-icon-container + = custom_icon('key') + %span.nav-item-name + Deploy Keys + + = nav_link(controller: :services) do + = link_to admin_application_settings_services_path, title: 'Service Templates' do + .nav-icon-container + = custom_icon('service_templates') + %span.nav-item-name + Service Templates + + = nav_link(controller: :labels) do + = link_to admin_labels_path, title: 'Labels' do + .nav-icon-container + = custom_icon('labels') + %span.nav-item-name + Labels + + = nav_link(controller: :appearances) do + = link_to admin_appearances_path, title: 'Appearances' do + .nav-icon-container + = custom_icon('appearance') + %span.nav-item-name + Appearance + + %li.divider + = nav_link(controller: :application_settings) do + = link_to admin_application_settings_path, title: 'Settings' do + .nav-icon-container + = custom_icon('settings') + %span.nav-item-name + Settings + + = render 'shared/sidebar_toggle_button' diff --git a/app/views/layouts/nav/_new_group_sidebar.html.haml b/app/views/layouts/nav/_new_group_sidebar.html.haml index c7dabbd8237..ed5793f09fe 100644 --- a/app/views/layouts/nav/_new_group_sidebar.html.haml +++ b/app/views/layouts/nav/_new_group_sidebar.html.haml @@ -1,89 +1,90 @@ .nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) } - .context-header - = link_to group_path(@group), title: @group.name do - .avatar-container.s40.group-avatar - = image_tag group_icon(@group), class: "avatar s40 avatar-tile" - .group-title - = @group.name - %ul.sidebar-top-level-items - = nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups'], html_options: { class: 'home' }) do - = link_to group_path(@group), title: 'Group overview' do - .nav-icon-container - = custom_icon('project') - %span.nav-item-name - Overview - - %ul.sidebar-sub-level-items - = nav_link(path: ['groups#show', 'groups#subgroups'], html_options: { class: 'home' }) do - = link_to group_path(@group), title: 'Group details' do - %span - Details - - = nav_link(path: 'groups#activity') do - = link_to activity_group_path(@group), title: 'Activity' do - %span - Activity - - = nav_link(path: ['groups#issues', 'labels#index', 'milestones#index']) do - = link_to issues_group_path(@group), title: 'Issues' do - .nav-icon-container - = custom_icon('issues') - %span.nav-item-name - - issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute - Issues - %span.badge.count= number_with_delimiter(issues.count) - - %ul.sidebar-sub-level-items - = nav_link(path: 'groups#issues', html_options: { class: 'home' }) do - = link_to issues_group_path(@group), title: 'List' do - %span - List - - = nav_link(path: 'labels#index') do - = link_to group_labels_path(@group), title: 'Labels' do - %span - Labels - - = nav_link(path: 'milestones#index') do - = link_to group_milestones_path(@group), title: 'Milestones' do - %span - Milestones - - = nav_link(path: 'groups#merge_requests') do - = link_to merge_requests_group_path(@group), title: 'Merge Requests' do - .nav-icon-container - = custom_icon('mr_bold') - %span.nav-item-name - - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute - Merge Requests - %span.badge.count= number_with_delimiter(merge_requests.count) - = nav_link(path: 'group_members#index') do - = link_to group_group_members_path(@group), title: 'Members' do - .nav-icon-container - = custom_icon('members') - %span.nav-item-name - Members - - if current_user && can?(current_user, :admin_group, @group) - = nav_link(path: %w[groups#projects groups#edit ci_cd#show]) do - = link_to edit_group_path(@group), title: 'Settings' do + .nav-sidebar-inner-scroll + .context-header + = link_to group_path(@group), title: @group.name do + .avatar-container.s40.group-avatar + = image_tag group_icon(@group), class: "avatar s40 avatar-tile" + .group-title + = @group.name + %ul.sidebar-top-level-items + = nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups'], html_options: { class: 'home' }) do + = link_to group_path(@group), title: 'Group overview' do .nav-icon-container - = custom_icon('settings') + = custom_icon('project') %span.nav-item-name - Settings + Overview + %ul.sidebar-sub-level-items - = nav_link(path: 'groups#edit') do - = link_to edit_group_path(@group), title: 'General' do + = nav_link(path: ['groups#show', 'groups#subgroups'], html_options: { class: 'home' }) do + = link_to group_path(@group), title: 'Group details' do %span - General + Details - = nav_link(path: 'groups#projects') do - = link_to projects_group_path(@group), title: 'Projects' do + = nav_link(path: 'groups#activity') do + = link_to activity_group_path(@group), title: 'Activity' do %span - Projects + Activity - = nav_link(controller: :ci_cd) do - = link_to group_settings_ci_cd_path(@group), title: 'CI / CD' do + = nav_link(path: ['groups#issues', 'labels#index', 'milestones#index']) do + = link_to issues_group_path(@group), title: 'Issues' do + .nav-icon-container + = custom_icon('issues') + %span.nav-item-name + - issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute + Issues + %span.badge.count= number_with_delimiter(issues.count) + + %ul.sidebar-sub-level-items + = nav_link(path: 'groups#issues', html_options: { class: 'home' }) do + = link_to issues_group_path(@group), title: 'List' do %span - CI / CD + List - = render 'shared/sidebar_toggle_button' + = nav_link(path: 'labels#index') do + = link_to group_labels_path(@group), title: 'Labels' do + %span + Labels + + = nav_link(path: 'milestones#index') do + = link_to group_milestones_path(@group), title: 'Milestones' do + %span + Milestones + + = nav_link(path: 'groups#merge_requests') do + = link_to merge_requests_group_path(@group), title: 'Merge Requests' do + .nav-icon-container + = custom_icon('mr_bold') + %span.nav-item-name + - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute + Merge Requests + %span.badge.count= number_with_delimiter(merge_requests.count) + = nav_link(path: 'group_members#index') do + = link_to group_group_members_path(@group), title: 'Members' do + .nav-icon-container + = custom_icon('members') + %span.nav-item-name + Members + - if current_user && can?(current_user, :admin_group, @group) + = nav_link(path: %w[groups#projects groups#edit ci_cd#show]) do + = link_to edit_group_path(@group), title: 'Settings' do + .nav-icon-container + = custom_icon('settings') + %span.nav-item-name + Settings + %ul.sidebar-sub-level-items + = nav_link(path: 'groups#edit') do + = link_to edit_group_path(@group), title: 'General' do + %span + General + + = nav_link(path: 'groups#projects') do + = link_to projects_group_path(@group), title: 'Projects' do + %span + Projects + + = nav_link(controller: :ci_cd) do + = link_to group_settings_ci_cd_path(@group), title: 'CI / CD' do + %span + CI / CD + + = render 'shared/sidebar_toggle_button' diff --git a/app/views/layouts/nav/_new_profile_sidebar.html.haml b/app/views/layouts/nav/_new_profile_sidebar.html.haml index edae009a28e..4234df56d1d 100644 --- a/app/views/layouts/nav/_new_profile_sidebar.html.haml +++ b/app/views/layouts/nav/_new_profile_sidebar.html.haml @@ -1,84 +1,85 @@ .nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) } - .context-header - = link_to profile_path, title: 'Profile Settings' do - .avatar-container.s40.settings-avatar - = icon('user') - .project-title User Settings - %ul.sidebar-top-level-items - = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do + .nav-sidebar-inner-scroll + .context-header = link_to profile_path, title: 'Profile Settings' do - .nav-icon-container - = custom_icon('profile') - %span.nav-item-name - Profile - = nav_link(controller: [:accounts, :two_factor_auths]) do - = link_to profile_account_path, title: 'Account' do - .nav-icon-container - = custom_icon('account') - %span.nav-item-name - Account - - if current_application_settings.user_oauth_applications? - = nav_link(controller: 'oauth/applications') do - = link_to applications_profile_path, title: 'Applications' do + .avatar-container.s40.settings-avatar + = icon('user') + .project-title User Settings + %ul.sidebar-top-level-items + = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do + = link_to profile_path, title: 'Profile Settings' do .nav-icon-container - = custom_icon('applications') + = custom_icon('profile') %span.nav-item-name - Applications - = nav_link(controller: :chat_names) do - = link_to profile_chat_names_path, title: 'Chat' do - .nav-icon-container - = custom_icon('chat') - %span.nav-item-name - Chat - = nav_link(controller: :personal_access_tokens) do - = link_to profile_personal_access_tokens_path, title: 'Access Tokens' do - .nav-icon-container - = custom_icon('access_tokens') - %span.nav-item-name - Access Tokens - = nav_link(controller: :emails) do - = link_to profile_emails_path, title: 'Emails' do - .nav-icon-container - = custom_icon('emails') - %span.nav-item-name - Emails - - unless current_user.ldap_user? - = nav_link(controller: :passwords) do - = link_to edit_profile_password_path, title: 'Password' do + Profile + = nav_link(controller: [:accounts, :two_factor_auths]) do + = link_to profile_account_path, title: 'Account' do .nav-icon-container - = custom_icon('lock') + = custom_icon('account') %span.nav-item-name - Password - = nav_link(controller: :notifications) do - = link_to profile_notifications_path, title: 'Notifications' do - .nav-icon-container - = custom_icon('notifications') - %span.nav-item-name - Notifications + Account + - if current_application_settings.user_oauth_applications? + = nav_link(controller: 'oauth/applications') do + = link_to applications_profile_path, title: 'Applications' do + .nav-icon-container + = custom_icon('applications') + %span.nav-item-name + Applications + = nav_link(controller: :chat_names) do + = link_to profile_chat_names_path, title: 'Chat' do + .nav-icon-container + = custom_icon('chat') + %span.nav-item-name + Chat + = nav_link(controller: :personal_access_tokens) do + = link_to profile_personal_access_tokens_path, title: 'Access Tokens' do + .nav-icon-container + = custom_icon('access_tokens') + %span.nav-item-name + Access Tokens + = nav_link(controller: :emails) do + = link_to profile_emails_path, title: 'Emails' do + .nav-icon-container + = custom_icon('emails') + %span.nav-item-name + Emails + - unless current_user.ldap_user? + = nav_link(controller: :passwords) do + = link_to edit_profile_password_path, title: 'Password' do + .nav-icon-container + = custom_icon('lock') + %span.nav-item-name + Password + = nav_link(controller: :notifications) do + = link_to profile_notifications_path, title: 'Notifications' do + .nav-icon-container + = custom_icon('notifications') + %span.nav-item-name + Notifications - = nav_link(controller: :keys) do - = link_to profile_keys_path, title: 'SSH Keys' do - .nav-icon-container - = custom_icon('key') - %span.nav-item-name - SSH Keys - = nav_link(controller: :gpg_keys) do - = link_to profile_gpg_keys_path, title: 'GPG Keys' do - .nav-icon-container - = custom_icon('key_2') - %span.nav-item-name - GPG Keys - = nav_link(controller: :preferences) do - = link_to profile_preferences_path, title: 'Preferences' do - .nav-icon-container - = custom_icon('preferences') - %span.nav-item-name - Preferences - = nav_link(path: 'profiles#audit_log') do - = link_to audit_log_profile_path, title: 'Authentication log' do - .nav-icon-container - = custom_icon('authentication_log') - %span.nav-item-name - Authentication log + = nav_link(controller: :keys) do + = link_to profile_keys_path, title: 'SSH Keys' do + .nav-icon-container + = custom_icon('key') + %span.nav-item-name + SSH Keys + = nav_link(controller: :gpg_keys) do + = link_to profile_gpg_keys_path, title: 'GPG Keys' do + .nav-icon-container + = custom_icon('key_2') + %span.nav-item-name + GPG Keys + = nav_link(controller: :preferences) do + = link_to profile_preferences_path, title: 'Preferences' do + .nav-icon-container + = custom_icon('preferences') + %span.nav-item-name + Preferences + = nav_link(path: 'profiles#audit_log') do + = link_to audit_log_profile_path, title: 'Authentication log' do + .nav-icon-container + = custom_icon('authentication_log') + %span.nav-item-name + Authentication log - = render 'shared/sidebar_toggle_button' + = render 'shared/sidebar_toggle_button' diff --git a/app/views/layouts/nav/_new_project_sidebar.html.haml b/app/views/layouts/nav/_new_project_sidebar.html.haml index e0477c29ebe..0ef81375c3a 100644 --- a/app/views/layouts/nav/_new_project_sidebar.html.haml +++ b/app/views/layouts/nav/_new_project_sidebar.html.haml @@ -1,261 +1,262 @@ .nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) } - - can_edit = can?(current_user, :admin_project, @project) - .context-header - = link_to project_path(@project), title: @project.name do - .avatar-container.s40.project-avatar - = project_icon(@project, alt: @project.name, class: 'avatar s40 avatar-tile') - .project-title - = @project.name - %ul.sidebar-top-level-items - = nav_link(path: ['projects#show', 'projects#activity', 'cycle_analytics#show'], html_options: { class: 'home' }) do - = link_to project_path(@project), title: 'Project overview', class: 'shortcuts-project' do - .nav-icon-container - = custom_icon('project') - %span.nav-item-name - Overview - - %ul.sidebar-sub-level-items - = nav_link(path: 'projects#show') do - = link_to project_path(@project), title: _('Project details'), class: 'shortcuts-project' do - %span= _('Details') - - = nav_link(path: 'projects#activity') do - = link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity' do - %span= _('Activity') - - - if can?(current_user, :read_cycle_analytics, @project) - = nav_link(path: 'cycle_analytics#show') do - = link_to project_cycle_analytics_path(@project), title: _('Cycle Analytics'), class: 'shortcuts-project-cycle-analytics' do - %span= _('Cycle Analytics') - - - if project_nav_tab? :files - = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare projects/repositories tags branches releases graphs network)) do - = link_to project_tree_path(@project), title: 'Repository', class: 'shortcuts-tree' do + .nav-sidebar-inner-scroll + - can_edit = can?(current_user, :admin_project, @project) + .context-header + = link_to project_path(@project), title: @project.name do + .avatar-container.s40.project-avatar + = project_icon(@project, alt: @project.name, class: 'avatar s40 avatar-tile') + .project-title + = @project.name + %ul.sidebar-top-level-items + = nav_link(path: ['projects#show', 'projects#activity', 'cycle_analytics#show'], html_options: { class: 'home' }) do + = link_to project_path(@project), title: 'Project overview', class: 'shortcuts-project' do .nav-icon-container - = custom_icon('doc_text') + = custom_icon('project') %span.nav-item-name - Repository + Overview %ul.sidebar-sub-level-items - = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do - = link_to project_tree_path(@project) do - #{ _('Files') } + = nav_link(path: 'projects#show') do + = link_to project_path(@project), title: _('Project details'), class: 'shortcuts-project' do + %span= _('Details') - = nav_link(controller: [:commit, :commits]) do - = link_to project_commits_path(@project, current_ref) do - #{ _('Commits') } + = nav_link(path: 'projects#activity') do + = link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity' do + %span= _('Activity') - = nav_link(html_options: {class: branches_tab_class}) do - = link_to project_branches_path(@project) do - #{ _('Branches') } + - if can?(current_user, :read_cycle_analytics, @project) + = nav_link(path: 'cycle_analytics#show') do + = link_to project_cycle_analytics_path(@project), title: _('Cycle Analytics'), class: 'shortcuts-project-cycle-analytics' do + %span= _('Cycle Analytics') - = nav_link(controller: [:tags, :releases]) do - = link_to project_tags_path(@project) do - #{ _('Tags') } + - if project_nav_tab? :files + = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare projects/repositories tags branches releases graphs network)) do + = link_to project_tree_path(@project), title: 'Repository', class: 'shortcuts-tree' do + .nav-icon-container + = custom_icon('doc_text') + %span.nav-item-name + Repository - = nav_link(path: 'graphs#show') do - = link_to project_graph_path(@project, current_ref) do - #{ _('Contributors') } + %ul.sidebar-sub-level-items + = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do + = link_to project_tree_path(@project) do + #{ _('Files') } - = nav_link(controller: %w(network)) do - = link_to project_network_path(@project, current_ref) do - #{ s_('ProjectNetworkGraph|Graph') } + = nav_link(controller: [:commit, :commits]) do + = link_to project_commits_path(@project, current_ref) do + #{ _('Commits') } - = nav_link(controller: :compare) do - = link_to project_compare_index_path(@project, from: @repository.root_ref, to: current_ref) do - #{ _('Compare') } + = nav_link(html_options: {class: branches_tab_class}) do + = link_to project_branches_path(@project) do + #{ _('Branches') } - = nav_link(path: 'graphs#charts') do - = link_to charts_project_graph_path(@project, current_ref) do - #{ _('Charts') } + = nav_link(controller: [:tags, :releases]) do + = link_to project_tags_path(@project) do + #{ _('Tags') } - - if project_nav_tab? :container_registry - = nav_link(controller: %w[projects/registry/repositories]) do - = link_to project_container_registry_index_path(@project), title: 'Container Registry', class: 'shortcuts-container-registry' do - .nav-icon-container - = custom_icon('container_registry') - %span.nav-item-name - Registry + = nav_link(path: 'graphs#show') do + = link_to project_graph_path(@project, current_ref) do + #{ _('Contributors') } - - if project_nav_tab? :issues - = nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do - = link_to project_issues_path(@project), title: 'Issues', class: 'shortcuts-issues' do - .nav-icon-container - = custom_icon('issues') - %span.nav-item-name - Issues - - if @project.issues_enabled? - %span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count) + = nav_link(controller: %w(network)) do + = link_to project_network_path(@project, current_ref) do + #{ s_('ProjectNetworkGraph|Graph') } - %ul.sidebar-sub-level-items - = nav_link(controller: :issues) do - = link_to project_issues_path(@project), title: 'Issues' do - %span - List + = nav_link(controller: :compare) do + = link_to project_compare_index_path(@project, from: @repository.root_ref, to: current_ref) do + #{ _('Compare') } - = nav_link(controller: :boards) do - = link_to project_boards_path(@project), title: 'Board' do - %span - Board + = nav_link(path: 'graphs#charts') do + = link_to charts_project_graph_path(@project, current_ref) do + #{ _('Charts') } - = nav_link(controller: :labels) do - = link_to project_labels_path(@project), title: 'Labels' do - %span - Labels + - if project_nav_tab? :container_registry + = nav_link(controller: %w[projects/registry/repositories]) do + = link_to project_container_registry_index_path(@project), title: 'Container Registry', class: 'shortcuts-container-registry' do + .nav-icon-container + = custom_icon('container_registry') + %span.nav-item-name + Registry - = nav_link(controller: :milestones) do - = link_to project_milestones_path(@project), title: 'Milestones' do - %span - Milestones + - if project_nav_tab? :issues + = nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do + = link_to project_issues_path(@project), title: 'Issues', class: 'shortcuts-issues' do + .nav-icon-container + = custom_icon('issues') + %span.nav-item-name + Issues + - if @project.issues_enabled? + %span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count) - - if project_nav_tab? :merge_requests - = nav_link(controller: @project.issues_enabled? ? :merge_requests : [:merge_requests, :labels, :milestones]) do - = link_to project_merge_requests_path(@project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do - .nav-icon-container - = custom_icon('mr_bold') - %span.nav-item-name - Merge Requests - %span.badge.count.merge_counter.js-merge-counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count) - - - if project_nav_tab? :pipelines - = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts]) do - = link_to project_pipelines_path(@project), title: 'CI / CD', class: 'shortcuts-pipelines' do - .nav-icon-container - = custom_icon('pipeline') - %span.nav-item-name - CI / CD - - %ul.sidebar-sub-level-items - - if project_nav_tab? :pipelines - = nav_link(path: ['pipelines#index', 'pipelines#show']) do - = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do + %ul.sidebar-sub-level-items + = nav_link(controller: :issues) do + = link_to project_issues_path(@project), title: 'Issues' do %span - Pipelines + List - - if project_nav_tab? :builds - = nav_link(controller: [:jobs, :artifacts]) do - = link_to project_jobs_path(@project), title: 'Jobs', class: 'shortcuts-builds' do + = nav_link(controller: :boards) do + = link_to project_boards_path(@project), title: 'Board' do %span - Jobs + Board - - if project_nav_tab? :pipelines - = nav_link(controller: :pipeline_schedules) do - = link_to pipeline_schedules_path(@project), title: 'Schedules', class: 'shortcuts-builds' do + = nav_link(controller: :labels) do + = link_to project_labels_path(@project), title: 'Labels' do %span - Schedules + Labels - - if project_nav_tab? :environments - = nav_link(controller: :environments) do - = link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do + = nav_link(controller: :milestones) do + = link_to project_milestones_path(@project), title: 'Milestones' do %span - Environments + Milestones - - if @project.feature_available?(:builds, current_user) && !@project.empty_repo? - = nav_link(path: 'pipelines#charts') do - = link_to charts_project_pipelines_path(@project), title: 'Charts', class: 'shortcuts-pipelines-charts' do - %span - Charts + - if project_nav_tab? :merge_requests + = nav_link(controller: @project.issues_enabled? ? :merge_requests : [:merge_requests, :labels, :milestones]) do + = link_to project_merge_requests_path(@project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do + .nav-icon-container + = custom_icon('mr_bold') + %span.nav-item-name + Merge Requests + %span.badge.count.merge_counter.js-merge-counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count) - - if project_nav_tab? :wiki - = nav_link(controller: :wikis) do - = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do - .nav-icon-container - = custom_icon('wiki') - %span.nav-item-name - Wiki + - if project_nav_tab? :pipelines + = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts]) do + = link_to project_pipelines_path(@project), title: 'CI / CD', class: 'shortcuts-pipelines' do + .nav-icon-container + = custom_icon('pipeline') + %span.nav-item-name + CI / CD - - if project_nav_tab? :snippets - = nav_link(controller: :snippets) do - = link_to project_snippets_path(@project), title: 'Snippets', class: 'shortcuts-snippets' do - .nav-icon-container - = custom_icon('snippets') - %span.nav-item-name - Snippets - - - if project_nav_tab? :settings - = nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show pages#show]) do - = link_to edit_project_path(@project), title: 'Settings', class: 'shortcuts-tree' do - .nav-icon-container - = custom_icon('settings') - %span.nav-item-name - Settings - - %ul.sidebar-sub-level-items - - can_edit = can?(current_user, :admin_project, @project) - - if can_edit - = nav_link(path: %w[projects#edit]) do - = link_to edit_project_path(@project), title: 'General' do - %span - General - = nav_link(controller: :project_members) do - = link_to project_project_members_path(@project), title: 'Members' do - %span - Members - - if can_edit - = nav_link(controller: [:integrations, :services, :hooks, :hook_logs]) do - = link_to project_settings_integrations_path(@project), title: 'Integrations' do - %span - Integrations - = nav_link(controller: :repository) do - = link_to project_settings_repository_path(@project), title: 'Repository' do - %span - Repository - - if @project.feature_available?(:builds, current_user) - = nav_link(controller: :ci_cd) do - = link_to project_settings_ci_cd_path(@project), title: 'CI / CD' do + %ul.sidebar-sub-level-items + - if project_nav_tab? :pipelines + = nav_link(path: ['pipelines#index', 'pipelines#show']) do + = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do %span - CI / CD - - if Gitlab.config.pages.enabled - = nav_link(controller: :pages) do - = link_to project_pages_path(@project), title: 'Pages' do + Pipelines + + - if project_nav_tab? :builds + = nav_link(controller: [:jobs, :artifacts]) do + = link_to project_jobs_path(@project), title: 'Jobs', class: 'shortcuts-builds' do %span - Pages + Jobs - - else - = nav_link(path: %w[members#show]) do - = link_to project_settings_members_path(@project), title: 'Members', class: 'shortcuts-tree' do - .nav-icon-container - = custom_icon('members') - %span.nav-item-name - Members + - if project_nav_tab? :pipelines + = nav_link(controller: :pipeline_schedules) do + = link_to pipeline_schedules_path(@project), title: 'Schedules', class: 'shortcuts-builds' do + %span + Schedules - = render 'shared/sidebar_toggle_button' + - if project_nav_tab? :environments + = nav_link(controller: :environments) do + = link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do + %span + Environments - -# Shortcut to Project > Activity - %li.hidden - = link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do - %span - Activity + - if @project.feature_available?(:builds, current_user) && !@project.empty_repo? + = nav_link(path: 'pipelines#charts') do + = link_to charts_project_pipelines_path(@project), title: 'Charts', class: 'shortcuts-pipelines-charts' do + %span + Charts - -# Shortcut to Repository > Graph (formerly, Network) - - if project_nav_tab? :network + - if project_nav_tab? :wiki + = nav_link(controller: :wikis) do + = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do + .nav-icon-container + = custom_icon('wiki') + %span.nav-item-name + Wiki + + - if project_nav_tab? :snippets + = nav_link(controller: :snippets) do + = link_to project_snippets_path(@project), title: 'Snippets', class: 'shortcuts-snippets' do + .nav-icon-container + = custom_icon('snippets') + %span.nav-item-name + Snippets + + - if project_nav_tab? :settings + = nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show pages#show]) do + = link_to edit_project_path(@project), title: 'Settings', class: 'shortcuts-tree' do + .nav-icon-container + = custom_icon('settings') + %span.nav-item-name + Settings + + %ul.sidebar-sub-level-items + - can_edit = can?(current_user, :admin_project, @project) + - if can_edit + = nav_link(path: %w[projects#edit]) do + = link_to edit_project_path(@project), title: 'General' do + %span + General + = nav_link(controller: :project_members) do + = link_to project_project_members_path(@project), title: 'Members' do + %span + Members + - if can_edit + = nav_link(controller: [:integrations, :services, :hooks, :hook_logs]) do + = link_to project_settings_integrations_path(@project), title: 'Integrations' do + %span + Integrations + = nav_link(controller: :repository) do + = link_to project_settings_repository_path(@project), title: 'Repository' do + %span + Repository + - if @project.feature_available?(:builds, current_user) + = nav_link(controller: :ci_cd) do + = link_to project_settings_ci_cd_path(@project), title: 'CI / CD' do + %span + CI / CD + - if Gitlab.config.pages.enabled + = nav_link(controller: :pages) do + = link_to project_pages_path(@project), title: 'Pages' do + %span + Pages + + - else + = nav_link(path: %w[members#show]) do + = link_to project_settings_members_path(@project), title: 'Members', class: 'shortcuts-tree' do + .nav-icon-container + = custom_icon('members') + %span.nav-item-name + Members + + = render 'shared/sidebar_toggle_button' + + -# Shortcut to Project > Activity %li.hidden - = link_to project_network_path(@project, current_ref), title: 'Network', class: 'shortcuts-network' do - Graph + = link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do + %span + Activity - -# Shortcut to Repository > Charts (formerly, top-nav item "Graphs") - - unless @project.empty_repo? + -# Shortcut to Repository > Graph (formerly, Network) + - if project_nav_tab? :network + %li.hidden + = link_to project_network_path(@project, current_ref), title: 'Network', class: 'shortcuts-network' do + Graph + + -# Shortcut to Repository > Charts (formerly, top-nav item "Graphs") + - unless @project.empty_repo? + %li.hidden + = link_to charts_project_graph_path(@project, current_ref), title: 'Charts', class: 'shortcuts-repository-charts' do + Charts + + -# Shortcut to Issues > New Issue %li.hidden - = link_to charts_project_graph_path(@project, current_ref), title: 'Charts', class: 'shortcuts-repository-charts' do - Charts + = link_to new_project_issue_path(@project), class: 'shortcuts-new-issue' do + Create a new issue - -# Shortcut to Issues > New Issue - %li.hidden - = link_to new_project_issue_path(@project), class: 'shortcuts-new-issue' do - Create a new issue + -# Shortcut to Pipelines > Jobs + - if project_nav_tab? :builds + %li.hidden + = link_to project_jobs_path(@project), title: 'Jobs', class: 'shortcuts-builds' do + Jobs - -# Shortcut to Pipelines > Jobs - - if project_nav_tab? :builds + -# Shortcut to commits page + - if project_nav_tab? :commits + %li.hidden + = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do + Commits + + -# Shortcut to issue boards %li.hidden - = link_to project_jobs_path(@project), title: 'Jobs', class: 'shortcuts-builds' do - Jobs - - -# Shortcut to commits page - - if project_nav_tab? :commits - %li.hidden - = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do - Commits - - -# Shortcut to issue boards - %li.hidden - = link_to 'Issue Boards', project_boards_path(@project), title: 'Issue Boards', class: 'shortcuts-issue-boards' + = link_to 'Issue Boards', project_boards_path(@project), title: 'Issue Boards', class: 'shortcuts-issue-boards' diff --git a/spec/javascripts/fly_out_nav_spec.js b/spec/javascripts/fly_out_nav_spec.js index 65a7459c5ed..2e81a1b056b 100644 --- a/spec/javascripts/fly_out_nav_spec.js +++ b/spec/javascripts/fly_out_nav_spec.js @@ -10,6 +10,7 @@ import { mousePos, getHideSubItemsInterval, documentMouseMove, + getHeaderHeight, } from '~/fly_out_nav'; import bp from '~/breakpoints'; @@ -59,7 +60,7 @@ describe('Fly out sidebar navigation', () => { describe('getHideSubItemsInterval', () => { beforeEach(() => { - el.innerHTML = ''; + el.innerHTML = ''; }); it('returns 0 if currentOpenMenu is nil', () => { @@ -112,6 +113,7 @@ describe('Fly out sidebar navigation', () => { clientX: el.getBoundingClientRect().left + 20, clientY: el.getBoundingClientRect().top + 10, }); + console.log(el); expect( getHideSubItemsInterval(), @@ -245,7 +247,7 @@ describe('Fly out sidebar navigation', () => { expect( subItems.style.transform, - ).toBe(`translate3d(0px, ${Math.floor(el.getBoundingClientRect().top)}px, 0px)`); + ).toBe(`translate3d(0px, ${Math.floor(el.getBoundingClientRect().top) - getHeaderHeight()}px, 0px)`); }); it('sets is-above when element is above', () => { From 2f3eeb10814ef8c4f07cd25c9692524825d7681c Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 15 Aug 2017 09:46:16 +0100 Subject: [PATCH 06/32] Fixes sticky class being removed when over-scrolling in Safari Closes #36457 --- app/assets/javascripts/lib/utils/sticky.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/lib/utils/sticky.js b/app/assets/javascripts/lib/utils/sticky.js index 43a808b6ab3..ff2b66046b4 100644 --- a/app/assets/javascripts/lib/utils/sticky.js +++ b/app/assets/javascripts/lib/utils/sticky.js @@ -1,7 +1,7 @@ export const isSticky = (el, scrollY, stickyTop) => { const top = el.offsetTop - scrollY; - if (top === stickyTop) { + if (top <= stickyTop) { el.classList.add('is-stuck'); } else { el.classList.remove('is-stuck'); From b6811c060a2620757bcc75a2a85b430b95ef2532 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 15 Aug 2017 16:02:10 +0100 Subject: [PATCH 07/32] Fixes icon sidebar expanding causing a jump in padding Closes #36456 --- app/assets/javascripts/new_sidebar.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/new_sidebar.js b/app/assets/javascripts/new_sidebar.js index b10b074f5ac..2d1ed9e4076 100644 --- a/app/assets/javascripts/new_sidebar.js +++ b/app/assets/javascripts/new_sidebar.js @@ -43,10 +43,12 @@ export default class NewNavSidebar { } toggleCollapsedSidebar(collapsed) { - this.$sidebar.toggleClass('sidebar-icons-only', collapsed); + const breakpoint = bp.getBreakpointSize(); + if (this.$sidebar.length) { + this.$sidebar.toggleClass('sidebar-icons-only', collapsed); this.$page.toggleClass('page-with-new-sidebar', !collapsed); - this.$page.toggleClass('page-with-icon-sidebar', collapsed); + this.$page.toggleClass('page-with-icon-sidebar', breakpoint === 'sm' ? true : collapsed); } NewNavSidebar.setCollapsedCookie(collapsed); } From 554afea059446384783f3c68c09ac56afa0e7d49 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 8 Aug 2017 14:25:00 +0100 Subject: [PATCH 08/32] Fix race condition with dispatcher.js The dispatcher was trying to create a new instance of a class that is loaded in a file after main.js which would cause the filtered search to not work on issues. This would only happen on the first load when the JS is not cached. If the JS is cached, then everything will be fine. --- app/assets/javascripts/diff_notes/diff_notes_bundle.js | 4 ++++ app/assets/javascripts/dispatcher.js | 2 +- app/assets/javascripts/main.js | 4 ++-- app/assets/javascripts/render_gfm.js | 4 +--- app/helpers/version_check_helper.rb | 2 +- .../projects/merge_requests/_discussion.html.haml | 6 +++--- spec/features/merge_requests/conflicts_spec.rb | 10 +++++++--- spec/helpers/version_check_helper_spec.rb | 2 +- 8 files changed, 20 insertions(+), 14 deletions(-) diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js b/app/assets/javascripts/diff_notes/diff_notes_bundle.js index a2d33b0936e..5decfc1dc01 100644 --- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js +++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js @@ -42,6 +42,10 @@ $(() => { $components.each(function () { const $this = $(this); const noteId = $this.attr(':note-id'); + const discussionId = $this.attr(':discussion-id'); + + if ($this.is('comment-and-resolve-btn') && !discussionId) return; + const tmp = Vue.extend({ template: $this.get(0).outerHTML }); diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 265e304b957..4183916722f 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -637,7 +637,7 @@ import UserFeatureHelper from './helpers/user_feature_helper'; return Dispatcher; })(); - $(function() { + $(window).on('load', function() { new Dispatcher(); }); }).call(window); diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 42092a34c2f..35dd2add35a 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -135,8 +135,9 @@ import './project_select'; import './project_show'; import './project_variables'; import './projects_list'; -import './render_gfm'; +import './syntax_highlight'; import './render_math'; +import './render_gfm'; import './right_sidebar'; import './search'; import './search_autocomplete'; @@ -144,7 +145,6 @@ import './smart_interval'; import './star'; import './subscription'; import './subscription_select'; -import './syntax_highlight'; import './dispatcher'; diff --git a/app/assets/javascripts/render_gfm.js b/app/assets/javascripts/render_gfm.js index 2c3a9cacd38..bcdc0fd67b8 100644 --- a/app/assets/javascripts/render_gfm.js +++ b/app/assets/javascripts/render_gfm.js @@ -11,7 +11,5 @@ return this; }; - $(document).on('ready load', function() { - return $('body').renderGFM(); - }); + $(() => $('body').renderGFM()); }).call(window); diff --git a/app/helpers/version_check_helper.rb b/app/helpers/version_check_helper.rb index 3b175251446..456598b4c28 100644 --- a/app/helpers/version_check_helper.rb +++ b/app/helpers/version_check_helper.rb @@ -2,7 +2,7 @@ module VersionCheckHelper def version_status_badge if Rails.env.production? && current_application_settings.version_check_enabled image_url = VersionCheck.new.url - image_tag image_url, class: 'js-version-status-badge', lazy: false + image_tag image_url, class: 'js-version-status-badge' end end end diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml index b787edb3427..3303aa72604 100644 --- a/app/views/projects/merge_requests/_discussion.html.haml +++ b/app/views/projects/merge_requests/_discussion.html.haml @@ -4,8 +4,8 @@ = link_to 'Close merge request', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: "btn btn-nr btn-comment btn-close close-mr-link js-note-target-close", title: "Close merge request", data: { original_text: "Close merge request", alternative_text: "Comment & close merge request"} - if @merge_request.reopenable? = link_to 'Reopen merge request', merge_request_path(@merge_request, merge_request: { state_event: :reopen }), method: :put, class: "btn btn-nr btn-comment btn-reopen reopen-mr-link js-note-target-close js-note-target-reopen", title: "Reopen merge request", data: { original_text: "Reopen merge request", alternative_text: "Comment & reopen merge request"} - %comment-and-resolve-btn{ "inline-template" => true, ":discussion-id" => "" } - %button.btn.btn-nr.btn-default.append-right-10.js-comment-resolve-button{ "v-if" => "showButton", type: "submit", data: { project_path: "#{project_path(@merge_request.project)}" } } - {{ buttonText }} + %comment-and-resolve-btn{ "inline-template" => true } + %button.btn.btn-nr.btn-default.append-right-10.js-comment-resolve-button{ "v-if" => "showButton", type: "submit", data: { project_path: "#{project_path(@merge_request.project)}" } } + {{ buttonText }} #notes= render "shared/notes/notes_with_form", :autocomplete => true diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_requests/conflicts_spec.rb index 2c560632a1b..2d2c674f8fb 100644 --- a/spec/features/merge_requests/conflicts_spec.rb +++ b/spec/features/merge_requests/conflicts_spec.rb @@ -28,11 +28,12 @@ feature 'Merge request conflict resolution', js: true do end click_button 'Commit conflict resolution' - wait_for_requests expect(page).to have_content('All merge conflicts were resolved') merge_request.reload_diff + wait_for_requests + click_on 'Changes' wait_for_requests @@ -69,10 +70,12 @@ feature 'Merge request conflict resolution', js: true do end click_button 'Commit conflict resolution' - wait_for_requests + expect(page).to have_content('All merge conflicts were resolved') merge_request.reload_diff + wait_for_requests + click_on 'Changes' wait_for_requests @@ -140,12 +143,13 @@ feature 'Merge request conflict resolution', js: true do end click_button 'Commit conflict resolution' - wait_for_requests expect(page).to have_content('All merge conflicts were resolved') merge_request.reload_diff + wait_for_requests + click_on 'Changes' wait_for_requests click_link 'Expand all' diff --git a/spec/helpers/version_check_helper_spec.rb b/spec/helpers/version_check_helper_spec.rb index 889fe441171..5eba03ef576 100644 --- a/spec/helpers/version_check_helper_spec.rb +++ b/spec/helpers/version_check_helper_spec.rb @@ -23,7 +23,7 @@ describe VersionCheckHelper do end it 'should have a js prefixed css class' do - expect(@image_tag).to match(/class="js-version-status-badge"/) + expect(@image_tag).to match(/class="js-version-status-badge lazy"/) end it 'should have a VersionCheck url as the src' do From 5fc871381ad0768bb38879ab1621e538ca3008d0 Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Mon, 14 Aug 2017 12:29:47 +0100 Subject: [PATCH 09/32] Speed up project creation by inlining repository creation --- .gitlab-ci.yml | 1 - lib/gitlab/git/repository.rb | 22 ++++++++++++++++++++++ lib/gitlab/shell.rb | 12 +++++++++--- spec/lib/gitlab/shell_spec.rb | 35 ++++++++++++++++++++++++----------- 4 files changed, 55 insertions(+), 15 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index cd19b6f47ff..4fcf51fb86e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -474,7 +474,6 @@ db:rollback-mysql: variables: SIZE: "1" SETUP_DB: "false" - RAILS_ENV: "development" script: - git clone https://gitlab.com/gitlab-org/gitlab-test.git /home/git/repositories/gitlab-org/gitlab-test.git diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 38772d06dbd..1d5ca68137a 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -18,6 +18,28 @@ module Gitlab InvalidBlobName = Class.new(StandardError) InvalidRef = Class.new(StandardError) + class << self + # Unlike `new`, `create` takes the storage path, not the storage name + def create(storage_path, name, bare: true, symlink_hooks_to: nil) + repo_path = File.join(storage_path, name) + repo_path += '.git' unless repo_path.end_with?('.git') + + FileUtils.mkdir_p(repo_path, mode: 0770) + + # Equivalent to `git --git-path=#{repo_path} init [--bare]` + repo = Rugged::Repository.init_at(repo_path, bare) + repo.close + + if symlink_hooks_to.present? + hooks_path = File.join(repo_path, 'hooks') + FileUtils.rm_rf(hooks_path) + FileUtils.ln_s(symlink_hooks_to, hooks_path) + end + + true + end + end + # Full path to repo attr_reader :path diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb index 0cb28732402..280a9abf03e 100644 --- a/lib/gitlab/shell.rb +++ b/lib/gitlab/shell.rb @@ -73,8 +73,10 @@ module Gitlab # # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/387 def add_repository(storage, name) - gitlab_shell_fast_execute([gitlab_shell_projects_path, - 'add-project', storage, "#{name}.git"]) + Gitlab::Git::Repository.create(storage, name, bare: true, symlink_hooks_to: gitlab_shell_hooks_path) + rescue => err + Rails.logger.error("Failed to add repository #{storage}/#{name}: #{err}") + false end # Import repository @@ -273,7 +275,11 @@ module Gitlab protected def gitlab_shell_path - Gitlab.config.gitlab_shell.path + File.expand_path(Gitlab.config.gitlab_shell.path) + end + + def gitlab_shell_hooks_path + File.expand_path(Gitlab.config.gitlab_shell.hooks_path) end def gitlab_shell_user_home diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb index 2345874cf10..cfadee0bcf5 100644 --- a/spec/lib/gitlab/shell_spec.rb +++ b/spec/lib/gitlab/shell_spec.rb @@ -94,28 +94,41 @@ describe Gitlab::Shell do end describe 'projects commands' do - let(:projects_path) { 'tmp/tests/shell-projects-test/bin/gitlab-projects' } + let(:gitlab_shell_path) { File.expand_path('tmp/tests/gitlab-shell') } + let(:projects_path) { File.join(gitlab_shell_path, 'bin/gitlab-projects') } + let(:gitlab_shell_hooks_path) { File.join(gitlab_shell_path, 'hooks') } before do - allow(Gitlab.config.gitlab_shell).to receive(:path).and_return('tmp/tests/shell-projects-test') + allow(Gitlab.config.gitlab_shell).to receive(:path).and_return(gitlab_shell_path) + allow(Gitlab.config.gitlab_shell).to receive(:hooks_path).and_return(gitlab_shell_hooks_path) allow(Gitlab.config.gitlab_shell).to receive(:git_timeout).and_return(800) end describe '#add_repository' do - it 'returns true when the command succeeds' do - expect(Gitlab::Popen).to receive(:popen) - .with([projects_path, 'add-project', 'current/storage', 'project/path.git'], - nil, popen_vars).and_return([nil, 0]) + it 'creates a repository' do + created_path = File.join(TestEnv.repos_path, 'project', 'path.git') + hooks_path = File.join(created_path, 'hooks') - expect(gitlab_shell.add_repository('current/storage', 'project/path')).to be true + begin + result = gitlab_shell.add_repository(TestEnv.repos_path, 'project/path') + + repo_stat = File.stat(created_path) rescue nil + hooks_stat = File.lstat(hooks_path) rescue nil + hooks_dir = File.realpath(hooks_path) + ensure + FileUtils.rm_rf(created_path) + end + + expect(result).to be_truthy + expect(repo_stat.mode & 0o777).to eq(0o770) + expect(hooks_stat.symlink?).to be_truthy + expect(hooks_dir).to eq(gitlab_shell_hooks_path) end it 'returns false when the command fails' do - expect(Gitlab::Popen).to receive(:popen) - .with([projects_path, 'add-project', 'current/storage', 'project/path.git'], - nil, popen_vars).and_return(["error", 1]) + expect(FileUtils).to receive(:mkdir_p).and_raise(Errno::EEXIST) - expect(gitlab_shell.add_repository('current/storage', 'project/path')).to be false + expect(gitlab_shell.add_repository('current/storage', 'project/path')).to be_falsy end end From 4edfad96784e8f77ec8ead26f01b4012977ba58a Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 15 Aug 2017 13:44:37 -0400 Subject: [PATCH 10/32] Enable Layout/TrailingWhitespace cop and auto-correct offenses --- .rubocop.yml | 4 ++++ .rubocop_todo.yml | 5 ---- .../groups/application_controller.rb | 2 +- .../omniauth_callbacks_controller.rb | 4 ++-- .../cycle_analytics/events_controller.rb | 24 +++++++++---------- .../projects/merge_requests_controller.rb | 4 ++-- app/helpers/pipeline_schedules_helper.rb | 10 ++++---- app/models/blob_viewer/notebook.rb | 2 +- app/models/deploy_keys_project.rb | 2 +- app/models/redirect_route.rb | 2 +- app/serializers/project_entity.rb | 2 +- app/serializers/tree_root_entity.rb | 2 +- config/initializers/0_acts_as_taggable.rb | 2 +- config/initializers/static_files.rb | 10 ++++---- config/initializers/trusted_proxies.rb | 2 +- config/routes/repository.rb | 2 +- ...0075830_default_request_access_projects.rb | 2 +- ...70503004427_update_retried_for_ci_build.rb | 2 +- .../20170523083112_migrate_old_artifacts.rb | 6 ++--- features/steps/profile/emails.rb | 2 +- lib/api/entities.rb | 2 +- lib/api/protected_branches.rb | 2 +- lib/banzai/filter/image_lazy_load_filter.rb | 4 ++-- lib/constraints/project_url_constrainer.rb | 2 +- lib/declarative_policy/base.rb | 2 +- lib/gitlab/auth/ip_rate_limiter.rb | 12 +++++----- lib/gitlab/ci/build/artifacts/metadata.rb | 2 +- lib/gitlab/git/blob.rb | 2 +- lib/gitlab/import_export/attributes_finder.rb | 2 +- lib/gitlab/ldap/auth_hash.rb | 2 +- lib/gitlab/middleware/rails_queue_duration.rb | 2 +- lib/gitlab/slash_commands/presenters/help.rb | 2 +- lib/tasks/gitlab/gitaly.rake | 2 +- spec/features/admin/admin_settings_spec.rb | 2 +- spec/features/issues_spec.rb | 2 +- spec/features/milestones/show_spec.rb | 2 +- .../projects/files/undo_template_spec.rb | 2 +- .../fixtures/prometheus_service.rb | 2 +- spec/javascripts/fixtures/services.rb | 2 +- spec/lib/gitlab/ci/trace/stream_spec.rb | 2 +- .../base_event_fetcher_spec.rb | 4 ++-- spec/lib/gitlab/key_fingerprint_spec.rb | 4 ++-- spec/lib/gitlab/ldap/auth_hash_spec.rb | 12 +++++----- spec/lib/gitlab/o_auth/user_spec.rb | 2 +- .../additional_metrics_parser_spec.rb | 4 ++-- spec/migrations/migrate_old_artifacts_spec.rb | 4 ++-- spec/requests/api/protected_branches_spec.rb | 4 ++-- spec/requests/api/settings_spec.rb | 2 +- spec/requests/ci/api/builds_spec.rb | 2 +- spec/services/web_hook_service_spec.rb | 2 +- spec/support/generate-seed-repo-rb | 12 +++++----- .../access_matchers_for_controller.rb | 2 +- spec/tasks/gitlab/gitaly_rake_spec.rb | 2 +- 53 files changed, 99 insertions(+), 100 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index d25b4ac39c9..dfe5df2eb3e 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -251,6 +251,10 @@ Layout/Tab: Layout/TrailingBlankLines: Enabled: true +# Avoid trailing whitespace. +Layout/TrailingWhitespace: + Enabled: true + # Style ####################################################################### # Check the naming of accessor methods for get_/set_. diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index cf14285ec2a..4b4f14efea4 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -57,11 +57,6 @@ Layout/SpaceInsideParens: Layout/SpaceInsidePercentLiteralDelimiters: Enabled: false -# Offense count: 89 -# Cop supports --auto-correct. -Layout/TrailingWhitespace: - Enabled: false - # Offense count: 272 RSpec/EmptyLineAfterFinalLet: Enabled: false diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb index c0ac47e363d..96ce686c989 100644 --- a/app/controllers/groups/application_controller.rb +++ b/app/controllers/groups/application_controller.rb @@ -34,7 +34,7 @@ class Groups::ApplicationController < ApplicationController def build_canonical_path(group) params[:group_id] = group.to_param - + url_for(params) end end diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index b4213574561..7444826a5d1 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -142,13 +142,13 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController def oauth @oauth ||= request.env['omniauth.auth'] end - + def fail_login error_message = @user.errors.full_messages.to_sentence return redirect_to omniauth_error_path(oauth['provider'], error: error_message) end - + def fail_ldap_login flash[:alert] = 'Access denied for your LDAP account.' diff --git a/app/controllers/projects/cycle_analytics/events_controller.rb b/app/controllers/projects/cycle_analytics/events_controller.rb index b69d46f2c41..26f3c114108 100644 --- a/app/controllers/projects/cycle_analytics/events_controller.rb +++ b/app/controllers/projects/cycle_analytics/events_controller.rb @@ -2,7 +2,7 @@ module Projects module CycleAnalytics class EventsController < Projects::ApplicationController include CycleAnalyticsParams - + before_action :authorize_read_cycle_analytics! before_action :authorize_read_build!, only: [:test, :staging] before_action :authorize_read_issue!, only: [:issue, :production] @@ -11,33 +11,33 @@ module Projects def issue render_events(cycle_analytics[:issue].events) end - + def plan render_events(cycle_analytics[:plan].events) end - + def code render_events(cycle_analytics[:code].events) end - + def test options(events_params)[:branch] = events_params[:branch_name] - + render_events(cycle_analytics[:test].events) end - + def review render_events(cycle_analytics[:review].events) end - + def staging render_events(cycle_analytics[:staging].events) end - + def production render_events(cycle_analytics[:production].events) end - + private def render_events(events) @@ -46,14 +46,14 @@ module Projects format.json { render json: { events: events } } end end - + def cycle_analytics @cycle_analytics ||= ::CycleAnalytics.new(project, options(events_params)) end - + def events_params return {} unless params[:events].present? - + params[:events].permit(:start_date, :branch_name) end end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 4de814d0ca8..2a3b73577a5 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -218,8 +218,8 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo if can?(current_user, :read_environment, environment) && environment.has_metrics? metrics_project_environment_deployment_path(environment.project, environment, deployment) end - - metrics_monitoring_url = + + metrics_monitoring_url = if can?(current_user, :read_environment, environment) environment_metrics_path(environment) end diff --git a/app/helpers/pipeline_schedules_helper.rb b/app/helpers/pipeline_schedules_helper.rb index fee1edc2a1b..6edaf78de1b 100644 --- a/app/helpers/pipeline_schedules_helper.rb +++ b/app/helpers/pipeline_schedules_helper.rb @@ -1,10 +1,10 @@ module PipelineSchedulesHelper def timezone_data - ActiveSupport::TimeZone.all.map do |timezone| - { - name: timezone.name, - offset: timezone.utc_offset, - identifier: timezone.tzinfo.identifier + ActiveSupport::TimeZone.all.map do |timezone| + { + name: timezone.name, + offset: timezone.utc_offset, + identifier: timezone.tzinfo.identifier } end end diff --git a/app/models/blob_viewer/notebook.rb b/app/models/blob_viewer/notebook.rb index 8632b8a9885..e00b47e6c17 100644 --- a/app/models/blob_viewer/notebook.rb +++ b/app/models/blob_viewer/notebook.rb @@ -2,7 +2,7 @@ module BlobViewer class Notebook < Base include Rich include ClientSide - + self.partial_name = 'notebook' self.extensions = %w(ipynb) self.binary = false diff --git a/app/models/deploy_keys_project.rb b/app/models/deploy_keys_project.rb index ae8486bd9ac..b37b9bfbdac 100644 --- a/app/models/deploy_keys_project.rb +++ b/app/models/deploy_keys_project.rb @@ -12,7 +12,7 @@ class DeployKeysProject < ActiveRecord::Base def destroy_orphaned_deploy_key return unless self.deploy_key.destroyed_when_orphaned? && self.deploy_key.orphaned? - + self.deploy_key.destroy end end diff --git a/app/models/redirect_route.rb b/app/models/redirect_route.rb index 090fbd61e6f..31de204d824 100644 --- a/app/models/redirect_route.rb +++ b/app/models/redirect_route.rb @@ -14,7 +14,7 @@ class RedirectRoute < ActiveRecord::Base else 'redirect_routes.path = ? OR redirect_routes.path LIKE ?' end - + where(wheres, path, "#{sanitize_sql_like(path)}/%") end end diff --git a/app/serializers/project_entity.rb b/app/serializers/project_entity.rb index dc283ba3e7a..b3e5fd21e97 100644 --- a/app/serializers/project_entity.rb +++ b/app/serializers/project_entity.rb @@ -1,6 +1,6 @@ class ProjectEntity < Grape::Entity include RequestAwareEntity - + expose :id expose :name diff --git a/app/serializers/tree_root_entity.rb b/app/serializers/tree_root_entity.rb index 23b65aa4a4c..7eb15ea63b7 100644 --- a/app/serializers/tree_root_entity.rb +++ b/app/serializers/tree_root_entity.rb @@ -1,7 +1,7 @@ # TODO: Inherit from TreeEntity, when `Tree` implements `id` and `name` like `Gitlab::Git::Tree`. class TreeRootEntity < Grape::Entity expose :path - + expose :trees, using: TreeEntity expose :blobs, using: BlobEntity expose :submodules, using: SubmoduleEntity diff --git a/config/initializers/0_acts_as_taggable.rb b/config/initializers/0_acts_as_taggable.rb index 54e9fcc31db..50dc47673ab 100644 --- a/config/initializers/0_acts_as_taggable.rb +++ b/config/initializers/0_acts_as_taggable.rb @@ -5,5 +5,5 @@ ActsAsTaggableOn.strict_case_match = true ActsAsTaggableOn.tags_counter = false # validate that counter cache is disabled -raise "Counter cache is not disabled" if +raise "Counter cache is not disabled" if ActsAsTaggableOn::Tagging.reflections["tag"].options[:counter_cache] diff --git a/config/initializers/static_files.rb b/config/initializers/static_files.rb index 9ed96ddb0b4..943e01f1496 100644 --- a/config/initializers/static_files.rb +++ b/config/initializers/static_files.rb @@ -1,15 +1,15 @@ app = Rails.application if app.config.serve_static_files - # The `ActionDispatch::Static` middleware intercepts requests for static files - # by checking if they exist in the `/public` directory. + # The `ActionDispatch::Static` middleware intercepts requests for static files + # by checking if they exist in the `/public` directory. # We're replacing it with our `Gitlab::Middleware::Static` that does the same, # except ignoring `/uploads`, letting those go through to the GitLab Rails app. app.config.middleware.swap( - ActionDispatch::Static, - Gitlab::Middleware::Static, - app.paths["public"].first, + ActionDispatch::Static, + Gitlab::Middleware::Static, + app.paths["public"].first, app.config.static_cache_control ) diff --git a/config/initializers/trusted_proxies.rb b/config/initializers/trusted_proxies.rb index fc4f02453d7..0c32528311e 100644 --- a/config/initializers/trusted_proxies.rb +++ b/config/initializers/trusted_proxies.rb @@ -2,7 +2,7 @@ # as the ActionDispatch::Request object. This is necessary for libraries # like rack_attack where they don't use ActionDispatch, and we want them # to block/throttle requests on private networks. -# Rack Attack specific issue: https://github.com/kickstarter/rack-attack/issues/145 +# Rack Attack specific issue: https://github.com/kickstarter/rack-attack/issues/145 module Rack class Request def trusted_proxy?(ip) diff --git a/config/routes/repository.rb b/config/routes/repository.rb index 57b7c55423d..9ffdebbcff1 100644 --- a/config/routes/repository.rb +++ b/config/routes/repository.rb @@ -3,7 +3,7 @@ resource :repository, only: [:create] do member do get ':ref/archive', constraints: { format: Gitlab::PathRegex.archive_formats_regex, ref: /.+/ }, action: 'archive', as: 'archive' - + # deprecated since GitLab 9.5 get 'archive', constraints: { format: Gitlab::PathRegex.archive_formats_regex }, as: 'archive_alternative' end diff --git a/db/migrate/20161020075830_default_request_access_projects.rb b/db/migrate/20161020075830_default_request_access_projects.rb index cb790291b24..a3a53350e8d 100644 --- a/db/migrate/20161020075830_default_request_access_projects.rb +++ b/db/migrate/20161020075830_default_request_access_projects.rb @@ -1,7 +1,7 @@ class DefaultRequestAccessProjects < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers DOWNTIME = false - + def up change_column_default :projects, :request_access_enabled, false end diff --git a/db/post_migrate/20170503004427_update_retried_for_ci_build.rb b/db/post_migrate/20170503004427_update_retried_for_ci_build.rb index 705e11ed47d..3a4d6c4916b 100644 --- a/db/post_migrate/20170503004427_update_retried_for_ci_build.rb +++ b/db/post_migrate/20170503004427_update_retried_for_ci_build.rb @@ -21,7 +21,7 @@ class UpdateRetriedForCiBuild < ActiveRecord::Migration private def up_mysql - # This is a trick to overcome MySQL limitation: + # This is a trick to overcome MySQL limitation: # Mysql2::Error: Table 'ci_builds' is specified twice, both as a target for 'UPDATE' and as a separate source for data # However, this leads to create a temporary table from `max(ci_builds.id)` which is slow and do full database update execute <<-SQL.strip_heredoc diff --git a/db/post_migrate/20170523083112_migrate_old_artifacts.rb b/db/post_migrate/20170523083112_migrate_old_artifacts.rb index f2690bd0017..3a77b9751d3 100644 --- a/db/post_migrate/20170523083112_migrate_old_artifacts.rb +++ b/db/post_migrate/20170523083112_migrate_old_artifacts.rb @@ -7,7 +7,7 @@ class MigrateOldArtifacts < ActiveRecord::Migration # This uses special heuristic to find potential candidates for data migration # Read more about this here: https://gitlab.com/gitlab-org/gitlab-ce/issues/32036#note_30422345 - + def up builds_with_artifacts.find_each do |build| build.migrate_artifacts! @@ -51,14 +51,14 @@ class MigrateOldArtifacts < ActiveRecord::Migration private def source_artifacts_path - @source_artifacts_path ||= + @source_artifacts_path ||= File.join(Gitlab.config.artifacts.path, created_at.utc.strftime('%Y_%m'), ci_id.to_s, id.to_s) end def target_artifacts_path - @target_artifacts_path ||= + @target_artifacts_path ||= File.join(Gitlab.config.artifacts.path, created_at.utc.strftime('%Y_%m'), project_id.to_s, id.to_s) diff --git a/features/steps/profile/emails.rb b/features/steps/profile/emails.rb index 10ebe705365..4f44f932a6d 100644 --- a/features/steps/profile/emails.rb +++ b/features/steps/profile/emails.rb @@ -28,7 +28,7 @@ class Spinach::Features::ProfileEmails < Spinach::FeatureSteps expect(email).to be_nil expect(page).not_to have_content("my@email.com") end - + step 'I click link "Remove" for "my@email.com"' do # there should only be one remove button at this time click_link "Remove" diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 18cd604a216..4219808fdc3 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -83,7 +83,7 @@ module API expose :created_at, :last_activity_at end - class Project < BasicProjectDetails + class Project < BasicProjectDetails include ::API::Helpers::RelatedResourcesHelpers expose :_links do diff --git a/lib/api/protected_branches.rb b/lib/api/protected_branches.rb index d742f2e18d0..dccf4fa27a7 100644 --- a/lib/api/protected_branches.rb +++ b/lib/api/protected_branches.rb @@ -61,7 +61,7 @@ module API service_args = [user_project, current_user, protected_branch_params] protected_branch = ::ProtectedBranches::CreateService.new(*service_args).execute - + if protected_branch.persisted? present protected_branch, with: Entities::ProtectedBranch, project: user_project else diff --git a/lib/banzai/filter/image_lazy_load_filter.rb b/lib/banzai/filter/image_lazy_load_filter.rb index 7a81d583b82..bcb4f332267 100644 --- a/lib/banzai/filter/image_lazy_load_filter.rb +++ b/lib/banzai/filter/image_lazy_load_filter.rb @@ -6,9 +6,9 @@ module Banzai doc.xpath('descendant-or-self::img').each do |img| img['class'] ||= '' << 'lazy' img['data-src'] = img['src'] - img['src'] = LazyImageTagHelper.placeholder_image + img['src'] = LazyImageTagHelper.placeholder_image end - + doc end end diff --git a/lib/constraints/project_url_constrainer.rb b/lib/constraints/project_url_constrainer.rb index fd7b97d3167..5bef29eb1da 100644 --- a/lib/constraints/project_url_constrainer.rb +++ b/lib/constraints/project_url_constrainer.rb @@ -7,7 +7,7 @@ class ProjectUrlConstrainer return false unless DynamicPathValidator.valid_project_path?(full_path) # We intentionally allow SELECT(*) here so result of this query can be used - # as cache for further Project.find_by_full_path calls within request + # as cache for further Project.find_by_full_path calls within request Project.find_by_full_path(full_path, follow_redirects: request.get?).present? end end diff --git a/lib/declarative_policy/base.rb b/lib/declarative_policy/base.rb index df94cafb6a1..f39b5bf29ec 100644 --- a/lib/declarative_policy/base.rb +++ b/lib/declarative_policy/base.rb @@ -221,7 +221,7 @@ module DeclarativePolicy end # computes the given ability and prints a helpful debugging output - # showing which + # showing which def debug(ability, *a) runner(ability).debug(*a) end diff --git a/lib/gitlab/auth/ip_rate_limiter.rb b/lib/gitlab/auth/ip_rate_limiter.rb index 1089bc9f89e..e6173d45af3 100644 --- a/lib/gitlab/auth/ip_rate_limiter.rb +++ b/lib/gitlab/auth/ip_rate_limiter.rb @@ -11,11 +11,11 @@ module Gitlab def enabled? config.enabled end - + def reset! Rack::Attack::Allow2Ban.reset(ip, config) end - + def register_fail! # Allow2Ban.filter will return false if this IP has not failed too often yet @banned = Rack::Attack::Allow2Ban.filter(ip, config) do @@ -23,17 +23,17 @@ module Gitlab ip_can_be_banned? end end - + def banned? @banned end - + private - + def config Gitlab.config.rack_attack.git_basic_auth end - + def ip_can_be_banned? config.ip_whitelist.exclude?(ip) end diff --git a/lib/gitlab/ci/build/artifacts/metadata.rb b/lib/gitlab/ci/build/artifacts/metadata.rb index a375ccbece0..a788fb3fcbc 100644 --- a/lib/gitlab/ci/build/artifacts/metadata.rb +++ b/lib/gitlab/ci/build/artifacts/metadata.rb @@ -60,7 +60,7 @@ module Gitlab begin path = read_string(gz).force_encoding('UTF-8') meta = read_string(gz).force_encoding('UTF-8') - + next unless path.valid_encoding? && meta.valid_encoding? next unless path =~ match_pattern next if path =~ INVALID_PATH_PATTERN diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb index 77b81d2d437..28835d7f5d2 100644 --- a/lib/gitlab/git/blob.rb +++ b/lib/gitlab/git/blob.rb @@ -54,7 +54,7 @@ module Gitlab # [[commit_sha, path], [commit_sha, path], ...]. If blob_size_limit < 0 then the # full blob contents are returned. If blob_size_limit >= 0 then each blob will # contain no more than limit bytes in its data attribute. - # + # # Keep in mind that this method may allocate a lot of memory. It is up # to the caller to limit the number of blobs and blob_size_limit. # diff --git a/lib/gitlab/import_export/attributes_finder.rb b/lib/gitlab/import_export/attributes_finder.rb index d230de781d5..c4fa91ef8d6 100644 --- a/lib/gitlab/import_export/attributes_finder.rb +++ b/lib/gitlab/import_export/attributes_finder.rb @@ -1,7 +1,7 @@ module Gitlab module ImportExport class AttributesFinder - + def initialize(included_attributes:, excluded_attributes:, methods:) @included_attributes = included_attributes || {} @excluded_attributes = excluded_attributes || {} diff --git a/lib/gitlab/ldap/auth_hash.rb b/lib/gitlab/ldap/auth_hash.rb index 95378e5a769..4fbc5fa5262 100644 --- a/lib/gitlab/ldap/auth_hash.rb +++ b/lib/gitlab/ldap/auth_hash.rb @@ -17,7 +17,7 @@ module Gitlab value = value.first if value break if value.present? end - + return super unless value Gitlab::Utils.force_utf8(value) diff --git a/lib/gitlab/middleware/rails_queue_duration.rb b/lib/gitlab/middleware/rails_queue_duration.rb index 5d2d7d0026c..63c3372da51 100644 --- a/lib/gitlab/middleware/rails_queue_duration.rb +++ b/lib/gitlab/middleware/rails_queue_duration.rb @@ -8,7 +8,7 @@ module Gitlab def initialize(app) @app = app end - + def call(env) trans = Gitlab::Metrics.current_transaction proxy_start = env['HTTP_GITLAB_WORKHORSE_PROXY_START'].presence diff --git a/lib/gitlab/slash_commands/presenters/help.rb b/lib/gitlab/slash_commands/presenters/help.rb index ea611a4d629..ab855319077 100644 --- a/lib/gitlab/slash_commands/presenters/help.rb +++ b/lib/gitlab/slash_commands/presenters/help.rb @@ -14,7 +14,7 @@ module Gitlab if text.start_with?('help') header_with_list("Available commands", full_commands(trigger)) else - header_with_list("Unknown command, these commands are available", full_commands(trigger)) + header_with_list("Unknown command, these commands are available", full_commands(trigger)) end end diff --git a/lib/tasks/gitlab/gitaly.rake b/lib/tasks/gitlab/gitaly.rake index e337c67a0f5..08677a98fc1 100644 --- a/lib/tasks/gitlab/gitaly.rake +++ b/lib/tasks/gitlab/gitaly.rake @@ -18,7 +18,7 @@ namespace :gitlab do command = status.zero? ? ['gmake'] : ['make'] if Rails.env.test? - command += %W[BUNDLE_PATH=#{Bundler.bundle_path}] + command += %W[BUNDLE_PATH=#{Bundler.bundle_path}] end Dir.chdir(args.dir) do diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index 5db42175c15..dbb0ae9c86e 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -74,7 +74,7 @@ feature 'Admin updates settings' do context 'sign-in restrictions', :js do it 'de-activates oauth sign-in source' do find('.btn', text: 'GitLab.com').click - + expect(find('.btn', text: 'GitLab.com')).not_to have_css('.active') end end diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index 3c8e37ff920..3ffc80622f5 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -708,7 +708,7 @@ describe 'Issues' do end describe 'confidential issue#show', js: true do - it 'shows confidential sibebar information as confidential and can be turned off' do + it 'shows confidential sibebar information as confidential and can be turned off' do issue = create(:issue, :confidential, project: project) visit project_issue_path(project, issue) diff --git a/spec/features/milestones/show_spec.rb b/spec/features/milestones/show_spec.rb index 20303359c46..624f13922ed 100644 --- a/spec/features/milestones/show_spec.rb +++ b/spec/features/milestones/show_spec.rb @@ -8,7 +8,7 @@ describe 'Milestone show' do let(:issue_params) { { project: project, assignees: [user], author: user, milestone: milestone, labels: labels } } before do - project.add_user(user, :developer) + project.add_user(user, :developer) sign_in(user) end diff --git a/spec/features/projects/files/undo_template_spec.rb b/spec/features/projects/files/undo_template_spec.rb index 4238d25e9ee..9bcd5beabb8 100644 --- a/spec/features/projects/files/undo_template_spec.rb +++ b/spec/features/projects/files/undo_template_spec.rb @@ -20,7 +20,7 @@ feature 'Template Undo Button', js: true do end end - context 'creating a non-matching file' do + context 'creating a non-matching file' do before do visit project_new_blob_path(project, 'master') select_file_template_type('LICENSE') diff --git a/spec/javascripts/fixtures/prometheus_service.rb b/spec/javascripts/fixtures/prometheus_service.rb index 7a46e47bb15..7968c9425f2 100644 --- a/spec/javascripts/fixtures/prometheus_service.rb +++ b/spec/javascripts/fixtures/prometheus_service.rb @@ -7,7 +7,7 @@ describe Projects::ServicesController, '(JavaScript fixtures)', type: :controlle let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} let(:project) { create(:project_empty_repo, namespace: namespace, path: 'services-project') } let!(:service) { create(:prometheus_service, project: project) } - + render_views before(:all) do diff --git a/spec/javascripts/fixtures/services.rb b/spec/javascripts/fixtures/services.rb index 0a3c64d5d31..beecbb44792 100644 --- a/spec/javascripts/fixtures/services.rb +++ b/spec/javascripts/fixtures/services.rb @@ -7,7 +7,7 @@ describe Projects::ServicesController, '(JavaScript fixtures)', type: :controlle let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} let(:project) { create(:project_empty_repo, namespace: namespace, path: 'services-project') } let!(:service) { create(:custom_issue_tracker_service, project: project, title: 'Custom Issue Tracker') } - + render_views diff --git a/spec/lib/gitlab/ci/trace/stream_spec.rb b/spec/lib/gitlab/ci/trace/stream_spec.rb index ebe5af56160..e5555546fa8 100644 --- a/spec/lib/gitlab/ci/trace/stream_spec.rb +++ b/spec/lib/gitlab/ci/trace/stream_spec.rb @@ -295,7 +295,7 @@ describe Gitlab::Ci::Trace::Stream do end context 'malicious regexp' do - let(:data) { malicious_text } + let(:data) { malicious_text } let(:regex) { malicious_regexp } include_examples 'malicious regexp' diff --git a/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb index 854aaa34c73..0560c47f03f 100644 --- a/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb @@ -6,10 +6,10 @@ describe Gitlab::CycleAnalytics::BaseEventFetcher do let(:user) { create(:user, :admin) } let(:start_time_attrs) { Issue.arel_table[:created_at] } let(:end_time_attrs) { [Issue::Metrics.arel_table[:first_associated_with_milestone_at]] } - let(:options) do + let(:options) do { start_time_attrs: start_time_attrs, end_time_attrs: end_time_attrs, - from: 30.days.ago } + from: 30.days.ago } end subject do diff --git a/spec/lib/gitlab/key_fingerprint_spec.rb b/spec/lib/gitlab/key_fingerprint_spec.rb index f5fd5a96bc9..d643dc5342d 100644 --- a/spec/lib/gitlab/key_fingerprint_spec.rb +++ b/spec/lib/gitlab/key_fingerprint_spec.rb @@ -30,8 +30,8 @@ describe Gitlab::KeyFingerprint, lib: true do MD5_FINGERPRINTS = { rsa: '06:b2:8a:92:df:0e:11:2c:ca:7b:8f:a4:ba:6e:4b:fd', - ecdsa: '45:ff:5b:98:9a:b6:8a:41:13:c1:30:8b:09:5e:7b:4e', - ed25519: '2e:65:6a:c8:cf:bf:b2:8b:9a:bd:6d:9f:11:5c:12:16', + ecdsa: '45:ff:5b:98:9a:b6:8a:41:13:c1:30:8b:09:5e:7b:4e', + ed25519: '2e:65:6a:c8:cf:bf:b2:8b:9a:bd:6d:9f:11:5c:12:16', dss: '57:98:86:02:5f:9c:f4:9b:ad:5a:1e:51:92:0e:fd:2b' }.freeze diff --git a/spec/lib/gitlab/ldap/auth_hash_spec.rb b/spec/lib/gitlab/ldap/auth_hash_spec.rb index 57a91193004..8370adf9211 100644 --- a/spec/lib/gitlab/ldap/auth_hash_spec.rb +++ b/spec/lib/gitlab/ldap/auth_hash_spec.rb @@ -4,8 +4,8 @@ describe Gitlab::LDAP::AuthHash do let(:auth_hash) do described_class.new( OmniAuth::AuthHash.new( - uid: '123456', - provider: 'ldapmain', + uid: '123456', + provider: 'ldapmain', info: info, extra: { raw_info: raw_info @@ -33,11 +33,11 @@ describe Gitlab::LDAP::AuthHash do context "without overridden attributes" do it "has the correct username" do - expect(auth_hash.username).to eq("123456") + expect(auth_hash.username).to eq("123456") end it "has the correct name" do - expect(auth_hash.name).to eq("Smith, J.") + expect(auth_hash.name).to eq("Smith, J.") end end @@ -54,11 +54,11 @@ describe Gitlab::LDAP::AuthHash do end it "has the correct username" do - expect(auth_hash.username).to eq("johnsmith@example.com") + expect(auth_hash.username).to eq("johnsmith@example.com") end it "has the correct name" do - expect(auth_hash.name).to eq("John Smith") + expect(auth_hash.name).to eq("John Smith") end end end diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb index 15edb820908..2cf0f7516de 100644 --- a/spec/lib/gitlab/o_auth/user_spec.rb +++ b/spec/lib/gitlab/o_auth/user_spec.rb @@ -481,7 +481,7 @@ describe Gitlab::OAuth::User do email: 'admin@othermail.com' } end - + it 'generates the username with a counter' do expect(gl_user.username).to eq('admin1') end diff --git a/spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb b/spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb index d7df4e35c31..5589db92b1d 100644 --- a/spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb +++ b/spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb @@ -24,7 +24,7 @@ describe Gitlab::Prometheus::AdditionalMetricsParser do queries: [{ query_range: 'query_range_empty' }] - group: group_b priority: 1 - metrics: + metrics: - title: title required_metrics: ['metric_a'] weight: 1 @@ -148,7 +148,7 @@ describe Gitlab::Prometheus::AdditionalMetricsParser do - group: group_a priority: 1 metrics: - - title: + - title: required_metrics: [] weight: 1 queries: [] diff --git a/spec/migrations/migrate_old_artifacts_spec.rb b/spec/migrations/migrate_old_artifacts_spec.rb index cfe1ca481b2..81366d15b34 100644 --- a/spec/migrations/migrate_old_artifacts_spec.rb +++ b/spec/migrations/migrate_old_artifacts_spec.rb @@ -10,7 +10,7 @@ describe MigrateOldArtifacts do before do allow(Gitlab.config.artifacts).to receive(:path).and_return(directory) end - + after do FileUtils.remove_entry_secure(directory) end @@ -95,7 +95,7 @@ describe MigrateOldArtifacts do FileUtils.copy( Rails.root.join('spec/fixtures/ci_build_artifacts.zip'), File.join(legacy_path(build), "ci_build_artifacts.zip")) - + FileUtils.copy( Rails.root.join('spec/fixtures/ci_build_artifacts_metadata.gz'), File.join(legacy_path(build), "ci_build_artifacts_metadata.gz")) diff --git a/spec/requests/api/protected_branches_spec.rb b/spec/requests/api/protected_branches_spec.rb index e4f9c47fb33..1aa8a95780e 100644 --- a/spec/requests/api/protected_branches_spec.rb +++ b/spec/requests/api/protected_branches_spec.rb @@ -96,7 +96,7 @@ describe API::ProtectedBranches do describe 'POST /projects/:id/protected_branches' do let(:branch_name) { 'new_branch' } - context 'when authenticated as a master' do + context 'when authenticated as a master' do before do project.add_master(user) end @@ -221,7 +221,7 @@ describe API::ProtectedBranches do context 'when branch has a wildcard in its name' do let(:protected_name) { 'feature*' } - + it "unprotects a wildcard branch" do delete api("/projects/#{project.id}/protected_branches/#{branch_name}", user) diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index 97275b80d03..737c028ad53 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -45,7 +45,7 @@ describe API::Settings, 'Settings' do help_page_hide_commercial_content: true, help_page_support_url: 'http://example.com/help', project_export_enabled: false - + expect(response).to have_http_status(200) expect(json_response['default_projects_limit']).to eq(3) expect(json_response['password_authentication_enabled']).to be_falsey diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index ebd67eb1e94..7ccba4ba3ec 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -130,7 +130,7 @@ describe Ci::API::Builds do register_builds info: { platform: :darwin } expect(response).to have_http_status(201) - + expect(json_response["options"]).to be_empty end end diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb index 365cb6b8f09..0726e135b20 100644 --- a/spec/services/web_hook_service_spec.rb +++ b/spec/services/web_hook_service_spec.rb @@ -144,7 +144,7 @@ describe WebHookService do describe '#async_execute' do let(:system_hook) { create(:system_hook) } - + it 'enqueue WebHookWorker' do expect(Sidekiq::Client).to receive(:enqueue).with(WebHookWorker, project_hook.id, data, 'push_hooks') diff --git a/spec/support/generate-seed-repo-rb b/spec/support/generate-seed-repo-rb index c89389b90ca..ef3c8e7087f 100755 --- a/spec/support/generate-seed-repo-rb +++ b/spec/support/generate-seed-repo-rb @@ -1,16 +1,16 @@ #!/usr/bin/env ruby -# +# # # generate-seed-repo-rb -# +# # This script generates the seed_repo.rb file used by lib/gitlab/git # tests. The seed_repo.rb file needs to be updated anytime there is a # Git push to https://gitlab.com/gitlab-org/gitlab-git-test. -# +# # Usage: -# +# # ./spec/support/generate-seed-repo-rb > spec/support/seed_repo.rb -# -# +# +# require 'erb' require 'tempfile' diff --git a/spec/support/matchers/access_matchers_for_controller.rb b/spec/support/matchers/access_matchers_for_controller.rb index ff60bd0c0ae..bb6b7c63ee9 100644 --- a/spec/support/matchers/access_matchers_for_controller.rb +++ b/spec/support/matchers/access_matchers_for_controller.rb @@ -1,6 +1,6 @@ # AccessMatchersForController # -# For testing authorize_xxx in controller. +# For testing authorize_xxx in controller. module AccessMatchersForController extend RSpec::Matchers::DSL include Warden::Test::Helpers diff --git a/spec/tasks/gitlab/gitaly_rake_spec.rb b/spec/tasks/gitlab/gitaly_rake_spec.rb index b29d63c7d67..1e9b20435ec 100644 --- a/spec/tasks/gitlab/gitaly_rake_spec.rb +++ b/spec/tasks/gitlab/gitaly_rake_spec.rb @@ -89,7 +89,7 @@ describe 'gitlab:gitaly namespace rake task' do it 'calls make in the gitaly directory without BUNDLE_PATH' do expect(main_object).to receive(:run_command!).with(command_preamble + ['make']).and_return(true) - + run_rake_task('gitlab:gitaly:install', clone_path) end end From f5cb3ac14da14a527a9a259d8a1bfb372868f352 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 15 Aug 2017 15:08:56 -0400 Subject: [PATCH 11/32] Don't depend on `Rails` for Redis configuration file paths The `Rails` object was not always available in all tasks that require Redis access, such as `mail_room`, so the constant pointing to the configuration path was never defined, but we still attempted to access it in `config_file_name`, resulting in a `NameError` exception. Further, there was no benefit to defining these paths in a constant to begin with -- they're only accessed in one place, and it was within the class where they were being defined. We can just provide them at run-time instead. Further _still_, we were calling `File.expand_path` on the absolute path returned by `Rails.root.join`, which was rather pointless. --- lib/gitlab/redis/cache.rb | 5 +---- lib/gitlab/redis/queues.rb | 5 +---- lib/gitlab/redis/shared_state.rb | 5 +---- lib/gitlab/redis/wrapper.rb | 13 +++++++++---- spec/lib/gitlab/redis/wrapper_spec.rb | 7 +++++++ 5 files changed, 19 insertions(+), 16 deletions(-) diff --git a/lib/gitlab/redis/cache.rb b/lib/gitlab/redis/cache.rb index b0da516ff83..9bf019b72e6 100644 --- a/lib/gitlab/redis/cache.rb +++ b/lib/gitlab/redis/cache.rb @@ -7,9 +7,6 @@ module Gitlab CACHE_NAMESPACE = 'cache:gitlab'.freeze DEFAULT_REDIS_CACHE_URL = 'redis://localhost:6380'.freeze REDIS_CACHE_CONFIG_ENV_VAR_NAME = 'GITLAB_REDIS_CACHE_CONFIG_FILE'.freeze - if defined?(::Rails) && ::Rails.root.present? - DEFAULT_REDIS_CACHE_CONFIG_FILE_NAME = ::Rails.root.join('config', 'redis.cache.yml').freeze - end class << self def default_url @@ -22,7 +19,7 @@ module Gitlab return file_name unless file_name.nil? # otherwise, if config files exists for this class, use it - file_name = File.expand_path(DEFAULT_REDIS_CACHE_CONFIG_FILE_NAME, __dir__) + file_name = config_file_path('redis.cache.yml') return file_name if File.file?(file_name) # this will force use of DEFAULT_REDIS_QUEUES_URL when config file is absent diff --git a/lib/gitlab/redis/queues.rb b/lib/gitlab/redis/queues.rb index f9249d05565..e1695aafbeb 100644 --- a/lib/gitlab/redis/queues.rb +++ b/lib/gitlab/redis/queues.rb @@ -8,9 +8,6 @@ module Gitlab MAILROOM_NAMESPACE = 'mail_room:gitlab'.freeze DEFAULT_REDIS_QUEUES_URL = 'redis://localhost:6381'.freeze REDIS_QUEUES_CONFIG_ENV_VAR_NAME = 'GITLAB_REDIS_QUEUES_CONFIG_FILE'.freeze - if defined?(::Rails) && ::Rails.root.present? - DEFAULT_REDIS_QUEUES_CONFIG_FILE_NAME = ::Rails.root.join('config', 'redis.queues.yml').freeze - end class << self def default_url @@ -23,7 +20,7 @@ module Gitlab return file_name if file_name # otherwise, if config files exists for this class, use it - file_name = File.expand_path(DEFAULT_REDIS_QUEUES_CONFIG_FILE_NAME, __dir__) + file_name = config_file_path('redis.queues.yml') return file_name if File.file?(file_name) # this will force use of DEFAULT_REDIS_QUEUES_URL when config file is absent diff --git a/lib/gitlab/redis/shared_state.rb b/lib/gitlab/redis/shared_state.rb index 395dcf082da..10bec7a90da 100644 --- a/lib/gitlab/redis/shared_state.rb +++ b/lib/gitlab/redis/shared_state.rb @@ -7,9 +7,6 @@ module Gitlab SESSION_NAMESPACE = 'session:gitlab'.freeze DEFAULT_REDIS_SHARED_STATE_URL = 'redis://localhost:6382'.freeze REDIS_SHARED_STATE_CONFIG_ENV_VAR_NAME = 'GITLAB_REDIS_SHARED_STATE_CONFIG_FILE'.freeze - if defined?(::Rails) && ::Rails.root.present? - DEFAULT_REDIS_SHARED_STATE_CONFIG_FILE_NAME = ::Rails.root.join('config', 'redis.shared_state.yml').freeze - end class << self def default_url @@ -22,7 +19,7 @@ module Gitlab return file_name if file_name # otherwise, if config files exists for this class, use it - file_name = File.expand_path(DEFAULT_REDIS_SHARED_STATE_CONFIG_FILE_NAME, __dir__) + file_name = config_file_path('redis.shared_state.yml') return file_name if File.file?(file_name) # this will force use of DEFAULT_REDIS_SHARED_STATE_URL when config file is absent diff --git a/lib/gitlab/redis/wrapper.rb b/lib/gitlab/redis/wrapper.rb index c43b37dde74..8ad06480575 100644 --- a/lib/gitlab/redis/wrapper.rb +++ b/lib/gitlab/redis/wrapper.rb @@ -8,9 +8,6 @@ module Gitlab class Wrapper DEFAULT_REDIS_URL = 'redis://localhost:6379'.freeze REDIS_CONFIG_ENV_VAR_NAME = 'GITLAB_REDIS_CONFIG_FILE'.freeze - if defined?(::Rails) && ::Rails.root.present? - DEFAULT_REDIS_CONFIG_FILE_NAME = ::Rails.root.join('config', 'resque.yml').freeze - end class << self delegate :params, :url, to: :new @@ -49,13 +46,21 @@ module Gitlab DEFAULT_REDIS_URL end + # Return the absolute path to a Rails configuration file + # + # We use this instead of `Rails.root` because for certain tasks + # utilizing these classes, `Rails` might not be available. + def config_file_path(filename) + File.expand_path("../../../config/#{filename}", __dir__) + end + def config_file_name # if ENV set for wrapper class, use it even if it points to a file does not exist file_name = ENV[REDIS_CONFIG_ENV_VAR_NAME] return file_name unless file_name.nil? # otherwise, if config files exists for wrapper class, use it - file_name = File.expand_path(DEFAULT_REDIS_CONFIG_FILE_NAME, __dir__) + file_name = config_file_path('resque.yml') return file_name if File.file?(file_name) # nil will force use of DEFAULT_REDIS_URL when config file is absent diff --git a/spec/lib/gitlab/redis/wrapper_spec.rb b/spec/lib/gitlab/redis/wrapper_spec.rb index e1becd0a614..0c22a0d62cc 100644 --- a/spec/lib/gitlab/redis/wrapper_spec.rb +++ b/spec/lib/gitlab/redis/wrapper_spec.rb @@ -17,4 +17,11 @@ describe Gitlab::Redis::Wrapper do let(:class_redis_url) { Gitlab::Redis::Wrapper::DEFAULT_REDIS_URL } include_examples "redis_shared_examples" + + describe '.config_file_path' do + it 'returns the absolute path to the configuration file' do + expect(described_class.config_file_path('foo.yml')) + .to eq Rails.root.join('config', 'foo.yml').to_s + end + end end From 142b9ec4a0b3e5d5cbcbb2fc3b201755f0966b9f Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 15 Aug 2017 15:53:16 -0400 Subject: [PATCH 12/32] Fix two additional violations caused by previous changes --- lib/gitlab/import_export/attributes_finder.rb | 1 - spec/javascripts/fixtures/services.rb | 1 - 2 files changed, 2 deletions(-) diff --git a/lib/gitlab/import_export/attributes_finder.rb b/lib/gitlab/import_export/attributes_finder.rb index c4fa91ef8d6..56042ddecbf 100644 --- a/lib/gitlab/import_export/attributes_finder.rb +++ b/lib/gitlab/import_export/attributes_finder.rb @@ -1,7 +1,6 @@ module Gitlab module ImportExport class AttributesFinder - def initialize(included_attributes:, excluded_attributes:, methods:) @included_attributes = included_attributes || {} @excluded_attributes = excluded_attributes || {} diff --git a/spec/javascripts/fixtures/services.rb b/spec/javascripts/fixtures/services.rb index beecbb44792..80915c32a74 100644 --- a/spec/javascripts/fixtures/services.rb +++ b/spec/javascripts/fixtures/services.rb @@ -8,7 +8,6 @@ describe Projects::ServicesController, '(JavaScript fixtures)', type: :controlle let(:project) { create(:project_empty_repo, namespace: namespace, path: 'services-project') } let!(:service) { create(:custom_issue_tracker_service, project: project, title: 'Custom Issue Tracker') } - render_views before(:all) do From f0b3788149848874393c89575fd90307be14d8c1 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 15 Aug 2017 18:22:15 -0500 Subject: [PATCH 13/32] Fix username autocomplete group name with no avatar alignment --- app/assets/stylesheets/framework/markdown_area.scss | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index fcd4c72b430..e3920b5d3d9 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -204,6 +204,16 @@ } } + div.avatar { + display: inline-flex; + justify-content: center; + align-items: center; + + .center { + line-height: 14px; + } + } + strong { color: $gl-text-color; } From 878fa8feba28df7708e31dedf06138f05cb75731 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 16 Aug 2017 08:59:54 +0000 Subject: [PATCH 14/32] Add documentation for groups issue filtered search --- doc/user/search/img/group_issues_filter.png | Bin 0 -> 45288 bytes doc/user/search/index.md | 8 ++++++++ 2 files changed, 8 insertions(+) create mode 100644 doc/user/search/img/group_issues_filter.png diff --git a/doc/user/search/img/group_issues_filter.png b/doc/user/search/img/group_issues_filter.png new file mode 100644 index 0000000000000000000000000000000000000000..45eced79b9906e90d42814abba38777210ef98d0 GIT binary patch literal 45288 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 z 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 literal 0 HcmV?d00001 diff --git a/doc/user/search/index.md b/doc/user/search/index.md index 6d59dcc6c75..79f34fd29ba 100644 --- a/doc/user/search/index.md +++ b/doc/user/search/index.md @@ -40,6 +40,14 @@ The same process is valid for merge requests. Navigate to your project's **Merge and click **Search or filter results...**. Merge requests can be filtered by author, assignee, milestone, and label. +## Issues and merge requests per group + +Similar to **Issues and merge requests per project**, you can also search for issues +within a group. Navigate to a group's **Issues** tab and query search results in +the same way as you do for projects. + +![filter issues in a group](img/group_issues_filter.png) + ## Search history You can view recent searches by clicking on the little arrow-clock icon, which is to the left of the search input. Click the search entry to run that search again. This feature is available for issues and merge requests. Searches are stored locally in your browser. From 04ee970cc11719f1391207995674d096c48d5afc Mon Sep 17 00:00:00 2001 From: Jarka Kadlecova Date: Wed, 16 Aug 2017 09:41:52 +0000 Subject: [PATCH 15/32] Don't create event in Merge Request Create Service --- app/services/merge_requests/create_service.rb | 1 - ...170815060945_remove_duplicate_mr_events.rb | 26 +++++++++++++++++++ db/schema.rb | 2 +- .../remove_duplicate_mr_events_spec.rb | 26 +++++++++++++++++++ .../merge_requests/create_service_spec.rb | 10 +++++++ 5 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 db/post_migrate/20170815060945_remove_duplicate_mr_events.rb create mode 100644 spec/migrations/remove_duplicate_mr_events_spec.rb diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb index fa0c0b7175c..194413bf321 100644 --- a/app/services/merge_requests/create_service.rb +++ b/app/services/merge_requests/create_service.rb @@ -25,7 +25,6 @@ module MergeRequests end def after_create(issuable) - event_service.open_mr(issuable, current_user) todo_service.new_merge_request(issuable, current_user) issuable.cache_merge_request_closes_issues!(current_user) update_merge_requests_head_pipeline(issuable) diff --git a/db/post_migrate/20170815060945_remove_duplicate_mr_events.rb b/db/post_migrate/20170815060945_remove_duplicate_mr_events.rb new file mode 100644 index 00000000000..6132b553177 --- /dev/null +++ b/db/post_migrate/20170815060945_remove_duplicate_mr_events.rb @@ -0,0 +1,26 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RemoveDuplicateMrEvents < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + DOWNTIME = false + + class Event < ActiveRecord::Base + self.table_name = 'events' + end + + def up + base_condition = "action = 1 AND target_type = 'MergeRequest' AND created_at > '2017-08-13'" + Event.select('target_id, count(*)') + .where(base_condition) + .group('target_id').having('count(*) > 1').each do |event| + duplicates = Event.where("#{base_condition} AND target_id = #{event.target_id}").pluck(:id) + duplicates.shift + + Event.where(id: duplicates).delete_all + end + end + + def down + end +end diff --git a/db/schema.rb b/db/schema.rb index 3206e106552..2ea6ae29dc7 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: 20170809161910) do +ActiveRecord::Schema.define(version: 20170815060945) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" diff --git a/spec/migrations/remove_duplicate_mr_events_spec.rb b/spec/migrations/remove_duplicate_mr_events_spec.rb new file mode 100644 index 00000000000..e393374028f --- /dev/null +++ b/spec/migrations/remove_duplicate_mr_events_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20170815060945_remove_duplicate_mr_events.rb') + +describe RemoveDuplicateMrEvents, truncate: true do + let(:migration) { described_class.new } + + describe '#up' do + let(:user) { create(:user) } + let(:merge_requests) { create_list(:merge_request, 2) } + let(:issue) { create(:issue) } + let!(:events) do + [ + create(:event, :created, author: user, target: merge_requests.first), + create(:event, :created, author: user, target: merge_requests.first), + create(:event, :updated, author: user, target: merge_requests.first), + create(:event, :created, author: user, target: merge_requests.second), + create(:event, :created, author: user, target: issue), + create(:event, :created, author: user, target: issue) + ] + end + + it 'removes duplicated merge request create records' do + expect { migration.up }.to change { Event.count }.from(6).to(5) + end + end +end diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb index 8fef480274d..a1f3bec42cc 100644 --- a/spec/services/merge_requests/create_service_spec.rb +++ b/spec/services/merge_requests/create_service_spec.rb @@ -48,6 +48,16 @@ describe MergeRequests::CreateService do expect(Todo.where(attributes).count).to be_zero end + it 'creates exactly 1 create MR event' do + attributes = { + action: Event::CREATED, + target_id: @merge_request.id, + target_type: @merge_request.class.name + } + + expect(Event.where(attributes).count).to eq(1) + end + context 'when merge request is assigned to someone' do let(:opts) do { From 5a26948d774aecb4bcf8d7f0b67efb5b9502dca2 Mon Sep 17 00:00:00 2001 From: Andrew Newdigate Date: Wed, 16 Aug 2017 10:28:37 +0000 Subject: [PATCH 16/32] Update Git version for source installs to match Omnibus --- .gitlab-ci.yml | 4 ++-- doc/install/installation.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4fcf51fb86e..df7244d5a2e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ -image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.7-phantomjs-2.1-node-7.1-postgresql-9.6" +image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.13-phantomjs-2.1-node-7.1-postgresql-9.6" .default-cache: &default-cache key: "ruby-233-with-yarn" @@ -522,7 +522,7 @@ karma: <<: *dedicated-runner <<: *except-docs <<: *pull-cache - image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.7-chrome-59.0-node-7.1-postgresql-9.6" + image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.13-chrome-59.0-node-7.1-postgresql-9.6" stage: test variables: BABEL_ENV: "coverage" diff --git a/doc/install/installation.md b/doc/install/installation.md index b14cb2d44c4..66eb7675896 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -80,7 +80,7 @@ Make sure you have the right version of Git installed # Install Git sudo apt-get install -y git-core - # Make sure Git is version 2.8.4 or higher + # Make sure Git is version 2.13.0 or higher git --version Is the system packaged Git too old? Remove it and compile from source. From 0da7eba850eb475d26b404849890dca68ca21968 Mon Sep 17 00:00:00 2001 From: Tim Zallmann Date: Wed, 16 Aug 2017 12:43:45 +0200 Subject: [PATCH 17/32] Set Description Image to max-width 100% --- app/assets/stylesheets/pages/issuable.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 87eaf27663f..1dac38f2b6d 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -81,6 +81,7 @@ border: 1px solid $white-normal; padding: 5px; max-height: calc(100vh - 100px); + max-width: 100%; } .emoji-block { From bd90dfab87f20769f637938806a4bcf0eecf92a2 Mon Sep 17 00:00:00 2001 From: Jarka Kadlecova Date: Wed, 16 Aug 2017 09:27:32 +0200 Subject: [PATCH 18/32] Fix edit milestone path from group milestones list --- .../shared/milestones/_milestone.html.haml | 2 +- app/views/shared/milestones/_top.html.haml | 2 +- spec/features/groups/milestone_spec.rb | 29 +++++++++++++++++-- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml index 6f6a036b13f..6a85f7d0564 100644 --- a/app/views/shared/milestones/_milestone.html.haml +++ b/app/views/shared/milestones/_milestone.html.haml @@ -32,7 +32,7 @@ .col-sm-6.milestone-actions - if can?(current_user, :admin_milestones, @group) - if milestone.is_group_milestone? - = link_to edit_group_milestone_path(@group, milestone.id), class: "btn btn-xs btn-grouped" do + = link_to edit_group_milestone_path(@group, milestone), class: "btn btn-xs btn-grouped" do Edit \ - if milestone.closed? diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml index b93837e3087..3014300fbe7 100644 --- a/app/views/shared/milestones/_top.html.haml +++ b/app/views/shared/milestones/_top.html.haml @@ -23,7 +23,7 @@ .pull-right - if can?(current_user, :admin_milestones, group) - if milestone.is_group_milestone? - = link_to edit_group_milestone_path(group, milestone.iid), class: "btn btn btn-grouped" do + = link_to edit_group_milestone_path(group, milestone), class: "btn btn btn-grouped" do Edit - if milestone.active? = link_to 'Close Milestone', group_milestone_route(milestone, {state_event: :close }), method: :put, class: "btn btn-grouped btn-close" diff --git a/spec/features/groups/milestone_spec.rb b/spec/features/groups/milestone_spec.rb index 32b3e13c624..56144d17d4f 100644 --- a/spec/features/groups/milestone_spec.rb +++ b/spec/features/groups/milestone_spec.rb @@ -35,12 +35,12 @@ feature 'Group milestones', :js do context 'milestones list' do let!(:other_project) { create(:project_empty_repo, group: group) } - let!(:active_group_milestone) { create(:milestone, group: group, state: 'active') } let!(:active_project_milestone1) { create(:milestone, project: project, state: 'active', title: 'v1.0') } let!(:active_project_milestone2) { create(:milestone, project: other_project, state: 'active', title: 'v1.0') } - let!(:closed_group_milestone) { create(:milestone, group: group, state: 'closed') } let!(:closed_project_milestone1) { create(:milestone, project: project, state: 'closed', title: 'v2.0') } let!(:closed_project_milestone2) { create(:milestone, project: other_project, state: 'closed', title: 'v2.0') } + let!(:active_group_milestone) { create(:milestone, group: group, state: 'active') } + let!(:closed_group_milestone) { create(:milestone, group: group, state: 'closed') } before do visit group_milestones_path(group) @@ -58,5 +58,30 @@ feature 'Group milestones', :js do expect(page).to have_selector("#milestone_#{active_group_milestone.id}", count: 1) expect(page).to have_selector("#milestone_#{legacy_milestone.milestones.first.id}", count: 1) end + + it 'updates milestone' do + page.within(".milestones #milestone_#{active_group_milestone.id}") do + click_link('Edit') + end + + page.within('.milestone-form') do + fill_in 'milestone_title', with: 'new title' + click_button('Update milestone') + end + + expect(find('#content-body h2')).to have_content('new title') + end + + it 'shows milestone detail and supports its edit' do + page.within(".milestones #milestone_#{active_group_milestone.id}") do + click_link(active_group_milestone.title) + end + + page.within('.detail-page-header') do + click_link('Edit') + end + + expect(page).to have_selector('.milestone-form') + end end end From b84d6efa267bec6bf1e28781f08e565f1e38fca7 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 16 Aug 2017 12:11:35 +0000 Subject: [PATCH 19/32] Change find to within for detecting bad search specs --- .../filtered_search/dropdown_assignee_spec.rb | 12 +- .../filtered_search/dropdown_author_spec.rb | 10 +- .../filtered_search/dropdown_label_spec.rb | 18 +- .../dropdown_milestone_spec.rb | 20 +- .../filtered_search/filter_issues_spec.rb | 192 ++++++++++-------- .../issues/filtered_search/search_bar_spec.rb | 2 +- .../filtered_search/visual_tokens_spec.rb | 4 +- .../filter_by_milestone_spec.rb | 2 +- .../filter_merge_requests_spec.rb | 58 +++--- spec/features/search_spec.rb | 12 +- spec/support/filtered_search_helpers.rb | 26 ++- 11 files changed, 209 insertions(+), 147 deletions(-) diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb index a69bd8a09b7..2cc027aac9e 100644 --- a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb @@ -134,8 +134,10 @@ describe 'Dropdown assignee', :js do it 'fills in the assignee username when the assignee has not been filtered' do click_assignee(user_jacob.name) + wait_for_requests + expect(page).to have_css(js_dropdown_assignee, visible: false) - expect_tokens([{ name: 'assignee', value: "@#{user_jacob.username}" }]) + expect_tokens([assignee_token(user_jacob.name)]) expect_filtered_search_input_empty end @@ -143,8 +145,10 @@ describe 'Dropdown assignee', :js do filtered_search.send_keys('roo') click_assignee(user.name) + wait_for_requests + expect(page).to have_css(js_dropdown_assignee, visible: false) - expect_tokens([{ name: 'assignee', value: "@#{user.username}" }]) + expect_tokens([assignee_token(user.name)]) expect_filtered_search_input_empty end @@ -152,7 +156,7 @@ describe 'Dropdown assignee', :js do find('#js-dropdown-assignee .filter-dropdown-item', text: 'No Assignee').click expect(page).to have_css(js_dropdown_assignee, visible: false) - expect_tokens([{ name: 'assignee', value: 'none' }]) + expect_tokens([assignee_token('none')]) expect_filtered_search_input_empty end end @@ -171,7 +175,7 @@ describe 'Dropdown assignee', :js do find('#js-dropdown-assignee .filter-dropdown-item', text: user.username).click expect(page).to have_css(js_dropdown_assignee, visible: false) - expect_tokens([{ name: 'assignee', value: user.username }]) + expect_tokens([assignee_token(user.username)]) expect_filtered_search_input_empty end end diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb index 4bbf18e1dbe..975dc035f2d 100644 --- a/spec/features/issues/filtered_search/dropdown_author_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb @@ -121,16 +121,20 @@ describe 'Dropdown author', js: true do it 'fills in the author username when the author has not been filtered' do click_author(user_jacob.name) + wait_for_requests + expect(page).to have_css(js_dropdown_author, visible: false) - expect_tokens([{ name: 'author', value: "@#{user_jacob.username}" }]) + expect_tokens([author_token(user_jacob.name)]) expect_filtered_search_input_empty end it 'fills in the author username when the author has been filtered' do click_author(user.name) + wait_for_requests + expect(page).to have_css(js_dropdown_author, visible: false) - expect_tokens([{ name: 'author', value: "@#{user.username}" }]) + expect_tokens([author_token(user.name)]) expect_filtered_search_input_empty end end @@ -149,7 +153,7 @@ describe 'Dropdown author', js: true do find('#js-dropdown-author .filter-dropdown-item', text: user.username).click expect(page).to have_css(js_dropdown_author, visible: false) - expect_tokens([{ name: 'author', value: user.username }]) + expect_tokens([author_token(user.username)]) expect_filtered_search_input_empty end end diff --git a/spec/features/issues/filtered_search/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb index 67eb0ef0119..e84b07ec2ef 100644 --- a/spec/features/issues/filtered_search/dropdown_label_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb @@ -47,7 +47,7 @@ describe 'Dropdown label', js: true do filtered_search.native.send_keys(:down, :down, :enter) - expect_tokens([{ name: 'label', value: "~#{bug_label.title}" }]) + expect_tokens([label_token(bug_label.title)]) expect_filtered_search_input_empty end end @@ -178,7 +178,7 @@ describe 'Dropdown label', js: true do click_label(bug_label.title) expect(page).not_to have_css(js_dropdown_label) - expect_tokens([{ name: 'label', value: "~#{bug_label.title}" }]) + expect_tokens([label_token(bug_label.title)]) expect_filtered_search_input_empty end @@ -187,7 +187,7 @@ describe 'Dropdown label', js: true do click_label(bug_label.title) expect(page).not_to have_css(js_dropdown_label) - expect_tokens([{ name: 'label', value: "~#{bug_label.title}" }]) + expect_tokens([label_token(bug_label.title)]) expect_filtered_search_input_empty end @@ -195,7 +195,7 @@ describe 'Dropdown label', js: true do click_label(two_words_label.title) expect(page).not_to have_css(js_dropdown_label) - expect_tokens([{ name: 'label', value: "\"#{two_words_label.title}\"" }]) + expect_tokens([label_token("\"#{two_words_label.title}\"")]) expect_filtered_search_input_empty end @@ -203,7 +203,7 @@ describe 'Dropdown label', js: true do click_label(long_label.title) expect(page).not_to have_css(js_dropdown_label) - expect_tokens([{ name: 'label', value: "\"#{long_label.title}\"" }]) + expect_tokens([label_token("\"#{long_label.title}\"")]) expect_filtered_search_input_empty end @@ -211,7 +211,7 @@ describe 'Dropdown label', js: true do click_label(wont_fix_label.title) expect(page).not_to have_css(js_dropdown_label) - expect_tokens([{ name: 'label', value: "~'#{wont_fix_label.title}'" }]) + expect_tokens([label_token("'#{wont_fix_label.title}'")]) expect_filtered_search_input_empty end @@ -219,7 +219,7 @@ describe 'Dropdown label', js: true do click_label(uppercase_label.title) expect(page).not_to have_css(js_dropdown_label) - expect_tokens([{ name: 'label', value: "~#{uppercase_label.title}" }]) + expect_tokens([label_token(uppercase_label.title)]) expect_filtered_search_input_empty end @@ -227,7 +227,7 @@ describe 'Dropdown label', js: true do click_label(special_label.title) expect(page).not_to have_css(js_dropdown_label) - expect_tokens([{ name: 'label', value: "~#{special_label.title}" }]) + expect_tokens([label_token(special_label.title)]) expect_filtered_search_input_empty end @@ -235,7 +235,7 @@ describe 'Dropdown label', js: true do find("#{js_dropdown_label} .filter-dropdown-item", text: 'No Label').click expect(page).not_to have_css(js_dropdown_label) - expect_tokens([{ name: 'label', value: 'none' }]) + expect_tokens([label_token('none', false)]) expect_filtered_search_input_empty end end diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb index 456eb05f241..5f99921ae2e 100644 --- a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb @@ -134,7 +134,7 @@ describe 'Dropdown milestone', :js do click_milestone(milestone.title) expect(page).to have_css(js_dropdown_milestone, visible: false) - expect_tokens([{ name: 'milestone', value: "%#{milestone.title}" }]) + expect_tokens([milestone_token(milestone.title)]) expect_filtered_search_input_empty end @@ -143,7 +143,7 @@ describe 'Dropdown milestone', :js do click_milestone(milestone.title) expect(page).to have_css(js_dropdown_milestone, visible: false) - expect_tokens([{ name: 'milestone', value: "%#{milestone.title}" }]) + expect_tokens([milestone_token(milestone.title)]) expect_filtered_search_input_empty end @@ -151,7 +151,7 @@ describe 'Dropdown milestone', :js do click_milestone(two_words_milestone.title) expect(page).to have_css(js_dropdown_milestone, visible: false) - expect_tokens([{ name: 'milestone', value: "%\"#{two_words_milestone.title}\"" }]) + expect_tokens([milestone_token("\"#{two_words_milestone.title}\"")]) expect_filtered_search_input_empty end @@ -159,7 +159,7 @@ describe 'Dropdown milestone', :js do click_milestone(long_milestone.title) expect(page).to have_css(js_dropdown_milestone, visible: false) - expect_tokens([{ name: 'milestone', value: "%\"#{long_milestone.title}\"" }]) + expect_tokens([milestone_token("\"#{long_milestone.title}\"")]) expect_filtered_search_input_empty end @@ -167,7 +167,7 @@ describe 'Dropdown milestone', :js do click_milestone(wont_fix_milestone.title) expect(page).to have_css(js_dropdown_milestone, visible: false) - expect_tokens([{ name: 'milestone', value: "%'#{wont_fix_milestone.title}'" }]) + expect_tokens([milestone_token("'#{wont_fix_milestone.title}'")]) expect_filtered_search_input_empty end @@ -175,7 +175,7 @@ describe 'Dropdown milestone', :js do click_milestone(uppercase_milestone.title) expect(page).to have_css(js_dropdown_milestone, visible: false) - expect_tokens([{ name: 'milestone', value: "%#{uppercase_milestone.title}" }]) + expect_tokens([milestone_token(uppercase_milestone.title)]) expect_filtered_search_input_empty end @@ -183,7 +183,7 @@ describe 'Dropdown milestone', :js do click_milestone(special_milestone.title) expect(page).to have_css(js_dropdown_milestone, visible: false) - expect_tokens([{ name: 'milestone', value: "%#{special_milestone.title}" }]) + expect_tokens([milestone_token(special_milestone.title)]) expect_filtered_search_input_empty end @@ -191,7 +191,7 @@ describe 'Dropdown milestone', :js do click_static_milestone('No Milestone') expect(page).to have_css(js_dropdown_milestone, visible: false) - expect_tokens([{ name: 'milestone', value: 'none' }]) + expect_tokens([milestone_token('none', false)]) expect_filtered_search_input_empty end @@ -199,7 +199,7 @@ describe 'Dropdown milestone', :js do click_static_milestone('Upcoming') expect(page).to have_css(js_dropdown_milestone, visible: false) - expect_tokens([{ name: 'milestone', value: 'upcoming' }]) + expect_tokens([milestone_token('upcoming', false)]) expect_filtered_search_input_empty end @@ -207,7 +207,7 @@ describe 'Dropdown milestone', :js do click_static_milestone('Started') expect(page).to have_css(js_dropdown_milestone, visible: false) - expect_tokens([{ name: 'milestone', value: 'started' }]) + expect_tokens([milestone_token('started', false)]) expect_filtered_search_input_empty end end diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb index cd2cbf4bfe7..2070043d842 100644 --- a/spec/features/issues/filtered_search/filter_issues_spec.rb +++ b/spec/features/issues/filtered_search/filter_issues_spec.rb @@ -97,7 +97,9 @@ describe 'Filter issues', js: true do it 'filters issues by searched author' do input_filtered_search("author:@#{user.username}") - expect_tokens([{ name: 'author', value: user.username }]) + wait_for_requests + + expect_tokens([author_token(user.name)]) expect_issues_list_count(5) expect_filtered_search_input_empty end @@ -117,7 +119,9 @@ describe 'Filter issues', js: true do it 'filters issues by searched author and text' do input_filtered_search("author:@#{user.username} #{search_term}") - expect_tokens([{ name: 'author', value: user.username }]) + wait_for_requests + + expect_tokens([author_token(user.name)]) expect_issues_list_count(3) expect_filtered_search_input(search_term) end @@ -125,10 +129,9 @@ describe 'Filter issues', js: true do it 'filters issues by searched author, assignee and text' do input_filtered_search("author:@#{user.username} assignee:@#{user.username} #{search_term}") - expect_tokens([ - { name: 'author', value: user.username }, - { name: 'assignee', value: user.username } - ]) + wait_for_requests + + expect_tokens([author_token(user.name), assignee_token(user.name)]) expect_issues_list_count(3) expect_filtered_search_input(search_term) end @@ -136,10 +139,12 @@ describe 'Filter issues', js: true do it 'filters issues by searched author, assignee, label, and text' do input_filtered_search("author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} #{search_term}") + wait_for_requests + expect_tokens([ - { name: 'author', value: user.username }, - { name: 'assignee', value: user.username }, - { name: 'label', value: caps_sensitive_label.title } + author_token(user.name), + assignee_token(user.name), + label_token(caps_sensitive_label.title) ]) expect_issues_list_count(1) expect_filtered_search_input(search_term) @@ -148,11 +153,13 @@ describe 'Filter issues', js: true do it 'filters issues by searched author, assignee, label, milestone and text' do input_filtered_search("author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} #{search_term}") + wait_for_requests + expect_tokens([ - { name: 'author', value: user.username }, - { name: 'assignee', value: user.username }, - { name: 'label', value: caps_sensitive_label.title }, - { name: 'milestone', value: milestone.title } + author_token(user.name), + assignee_token(user.name), + label_token(caps_sensitive_label.title), + milestone_token(milestone.title) ]) expect_issues_list_count(1) expect_filtered_search_input(search_term) @@ -169,7 +176,9 @@ describe 'Filter issues', js: true do it 'filters issues by searched assignee' do input_filtered_search("assignee:@#{user.username}") - expect_tokens([{ name: 'assignee', value: user.username }]) + wait_for_requests + + expect_tokens([assignee_token(user.name)]) expect_issues_list_count(5) expect_filtered_search_input_empty end @@ -177,7 +186,7 @@ describe 'Filter issues', js: true do it 'filters issues by no assignee' do input_filtered_search('assignee:none') - expect_tokens([{ name: 'assignee', value: 'none' }]) + expect_tokens([assignee_token('none')]) expect_issues_list_count(8, 1) expect_filtered_search_input_empty end @@ -197,7 +206,9 @@ describe 'Filter issues', js: true do it 'filters issues by searched assignee and text' do input_filtered_search("assignee:@#{user.username} #{search_term}") - expect_tokens([{ name: 'assignee', value: user.username }]) + wait_for_requests + + expect_tokens([assignee_token(user.name)]) expect_issues_list_count(2) expect_filtered_search_input(search_term) end @@ -205,10 +216,9 @@ describe 'Filter issues', js: true do it 'filters issues by searched assignee, author and text' do input_filtered_search("assignee:@#{user.username} author:@#{user.username} #{search_term}") - expect_tokens([ - { name: 'assignee', value: user.username }, - { name: 'author', value: user.username } - ]) + wait_for_requests + + expect_tokens([assignee_token(user.name), author_token(user.name)]) expect_issues_list_count(2) expect_filtered_search_input(search_term) end @@ -216,10 +226,12 @@ describe 'Filter issues', js: true do it 'filters issues by searched assignee, author, label, text' do input_filtered_search("assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} #{search_term}") + wait_for_requests + expect_tokens([ - { name: 'assignee', value: user.username }, - { name: 'author', value: user.username }, - { name: 'label', value: caps_sensitive_label.title } + assignee_token(user.name), + author_token(user.name), + label_token(caps_sensitive_label.title) ]) expect_issues_list_count(1) expect_filtered_search_input(search_term) @@ -229,10 +241,10 @@ describe 'Filter issues', js: true do input_filtered_search("assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} #{search_term}") expect_tokens([ - { name: 'assignee', value: user.username }, - { name: 'author', value: user.username }, - { name: 'label', value: caps_sensitive_label.title }, - { name: 'milestone', value: milestone.title } + assignee_token(user.name), + author_token(user.name), + label_token(caps_sensitive_label.title), + milestone_token(milestone.title) ]) expect_issues_list_count(1) expect_filtered_search_input(search_term) @@ -253,7 +265,7 @@ describe 'Filter issues', js: true do it 'filters issues by searched label' do input_filtered_search("label:~#{bug_label.title}") - expect_tokens([{ name: 'label', value: bug_label.title }]) + expect_tokens([label_token(bug_label.title)]) expect_issues_list_count(2) expect_filtered_search_input_empty end @@ -261,7 +273,7 @@ describe 'Filter issues', js: true do it 'filters issues by no label' do input_filtered_search('label:none') - expect_tokens([{ name: 'label', value: 'none' }]) + expect_tokens([label_token('none', false)]) expect_issues_list_count(9, 1) expect_filtered_search_input_empty end @@ -274,8 +286,8 @@ describe 'Filter issues', js: true do input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title}") expect_tokens([ - { name: 'label', value: bug_label.title }, - { name: 'label', value: caps_sensitive_label.title } + label_token(bug_label.title), + label_token(caps_sensitive_label.title) ]) expect_issues_list_count(1) expect_filtered_search_input_empty @@ -287,7 +299,8 @@ describe 'Filter issues', js: true do special_issue.labels << special_label input_filtered_search("label:~#{special_label.title}") - expect_tokens([{ name: 'label', value: special_label.title }]) + + expect_tokens([label_token(special_label.title)]) expect_issues_list_count(1) expect_filtered_search_input_empty end @@ -297,7 +310,7 @@ describe 'Filter issues', js: true do input_filtered_search("label:~#{new_label.title}") - expect_tokens([{ name: 'label', value: new_label.title }]) + expect_tokens([label_token(new_label.title)]) expect_no_issues_list() expect_filtered_search_input_empty end @@ -311,25 +324,27 @@ describe 'Filter issues', js: true do input_filtered_search("label:~'#{special_multiple_label.title}'") - # filtered search defaults quotations to double quotes - expect_tokens([{ name: 'label', value: "\"#{special_multiple_label.title}\"" }]) + # Check for search results (which makes sure that the page has changed) expect_issues_list_count(1) + # filtered search defaults quotations to double quotes + expect_tokens([label_token("\"#{special_multiple_label.title}\"")]) + expect_filtered_search_input_empty end it 'single quotes' do input_filtered_search("label:~'#{multiple_words_label.title}'") - expect_tokens([{ name: 'label', value: "\"#{multiple_words_label.title}\"" }]) expect_issues_list_count(1) + expect_tokens([label_token("\"#{multiple_words_label.title}\"")]) expect_filtered_search_input_empty end it 'double quotes' do input_filtered_search("label:~\"#{multiple_words_label.title}\"") - expect_tokens([{ name: 'label', value: "\"#{multiple_words_label.title}\"" }]) + expect_tokens([label_token("\"#{multiple_words_label.title}\"")]) expect_issues_list_count(1) expect_filtered_search_input_empty end @@ -341,7 +356,7 @@ describe 'Filter issues', js: true do input_filtered_search("label:~'#{double_quotes_label.title}'") - expect_tokens([{ name: 'label', value: "'#{double_quotes_label.title}'" }]) + expect_tokens([label_token("'#{double_quotes_label.title}'")]) expect_issues_list_count(1) expect_filtered_search_input_empty end @@ -353,7 +368,7 @@ describe 'Filter issues', js: true do input_filtered_search("label:~\"#{single_quotes_label.title}\"") - expect_tokens([{ name: 'label', value: "\"#{single_quotes_label.title}\"" }]) + expect_tokens([label_token("\"#{single_quotes_label.title}\"")]) expect_issues_list_count(1) expect_filtered_search_input_empty end @@ -363,7 +378,7 @@ describe 'Filter issues', js: true do it 'filters issues by searched label and text' do input_filtered_search("label:~#{caps_sensitive_label.title} #{search_term}") - expect_tokens([{ name: 'label', value: caps_sensitive_label.title }]) + expect_tokens([label_token(caps_sensitive_label.title)]) expect_issues_list_count(1) expect_filtered_search_input(search_term) end @@ -371,10 +386,9 @@ describe 'Filter issues', js: true do it 'filters issues by searched label, author and text' do input_filtered_search("label:~#{caps_sensitive_label.title} author:@#{user.username} #{search_term}") - expect_tokens([ - { name: 'label', value: caps_sensitive_label.title }, - { name: 'author', value: user.username } - ]) + wait_for_requests + + expect_tokens([label_token(caps_sensitive_label.title), author_token(user.name)]) expect_issues_list_count(1) expect_filtered_search_input(search_term) end @@ -382,10 +396,12 @@ describe 'Filter issues', js: true do it 'filters issues by searched label, author, assignee and text' do input_filtered_search("label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} #{search_term}") + wait_for_requests + expect_tokens([ - { name: 'label', value: caps_sensitive_label.title }, - { name: 'author', value: user.username }, - { name: 'assignee', value: user.username } + label_token(caps_sensitive_label.title), + author_token(user.name), + assignee_token(user.name) ]) expect_issues_list_count(1) expect_filtered_search_input(search_term) @@ -395,10 +411,10 @@ describe 'Filter issues', js: true do input_filtered_search("label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} #{search_term}") expect_tokens([ - { name: 'label', value: caps_sensitive_label.title }, - { name: 'author', value: user.username }, - { name: 'assignee', value: user.username }, - { name: 'milestone', value: milestone.title } + label_token(caps_sensitive_label.title), + author_token(user.name), + assignee_token(user.name), + milestone_token(milestone.title) ]) expect_issues_list_count(1) expect_filtered_search_input(search_term) @@ -410,8 +426,8 @@ describe 'Filter issues', js: true do input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} #{search_term}") expect_tokens([ - { name: 'label', value: bug_label.title }, - { name: 'label', value: caps_sensitive_label.title } + label_token(bug_label.title), + label_token(caps_sensitive_label.title) ]) expect_issues_list_count(1) expect_filtered_search_input(search_term) @@ -420,10 +436,12 @@ describe 'Filter issues', js: true do it 'filters issues by searched label, label2, author and text' do input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} #{search_term}") + wait_for_requests + expect_tokens([ - { name: 'label', value: bug_label.title }, - { name: 'label', value: caps_sensitive_label.title }, - { name: 'author', value: user.username } + label_token(bug_label.title), + label_token(caps_sensitive_label.title), + author_token(user.name) ]) expect_issues_list_count(1) expect_filtered_search_input(search_term) @@ -432,11 +450,13 @@ describe 'Filter issues', js: true do it 'filters issues by searched label, label2, author, assignee and text' do input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} #{search_term}") + wait_for_requests + expect_tokens([ - { name: 'label', value: bug_label.title }, - { name: 'label', value: caps_sensitive_label.title }, - { name: 'author', value: user.username }, - { name: 'assignee', value: user.username } + label_token(bug_label.title), + label_token(caps_sensitive_label.title), + author_token(user.name), + assignee_token(user.name) ]) expect_issues_list_count(1) expect_filtered_search_input(search_term) @@ -445,12 +465,14 @@ describe 'Filter issues', js: true do it 'filters issues by searched label, label2, author, assignee, milestone and text' do input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} #{search_term}") + wait_for_requests + expect_tokens([ - { name: 'label', value: bug_label.title }, - { name: 'label', value: caps_sensitive_label.title }, - { name: 'author', value: user.username }, - { name: 'assignee', value: user.username }, - { name: 'milestone', value: milestone.title } + label_token(bug_label.title), + label_token(caps_sensitive_label.title), + author_token(user.name), + assignee_token(user.name), + milestone_token(milestone.title) ]) expect_issues_list_count(1) expect_filtered_search_input(search_term) @@ -467,7 +489,7 @@ describe 'Filter issues', js: true do end it 'displays in search bar' do - expect_tokens([{ name: 'label', value: "\"#{multiple_words_label.title}\"" }]) + expect_tokens([label_token("\"#{multiple_words_label.title}\"")]) expect_filtered_search_input_empty end end @@ -484,7 +506,7 @@ describe 'Filter issues', js: true do it 'filters issues by searched milestone' do input_filtered_search("milestone:%#{milestone.title}") - expect_tokens([{ name: 'milestone', value: milestone.title }]) + expect_tokens([milestone_token(milestone.title)]) expect_issues_list_count(5) expect_filtered_search_input_empty end @@ -492,7 +514,7 @@ describe 'Filter issues', js: true do it 'filters issues by no milestone' do input_filtered_search("milestone:none") - expect_tokens([{ name: 'milestone', value: 'none' }]) + expect_tokens([milestone_token('none', false)]) expect_issues_list_count(7, 1) expect_filtered_search_input_empty end @@ -500,7 +522,7 @@ describe 'Filter issues', js: true do it 'filters issues by upcoming milestones' do input_filtered_search("milestone:upcoming") - expect_tokens([{ name: 'milestone', value: 'upcoming' }]) + expect_tokens([milestone_token('upcoming', false)]) expect_issues_list_count(1) expect_filtered_search_input_empty end @@ -508,7 +530,7 @@ describe 'Filter issues', js: true do it 'filters issues by started milestones' do input_filtered_search("milestone:started") - expect_tokens([{ name: 'milestone', value: 'started' }]) + expect_tokens([milestone_token('started', false)]) expect_issues_list_count(5) expect_filtered_search_input_empty end @@ -527,7 +549,7 @@ describe 'Filter issues', js: true do input_filtered_search("milestone:%#{special_milestone.title}") - expect_tokens([{ name: 'milestone', value: special_milestone.title }]) + expect_tokens([milestone_token(special_milestone.title)]) expect_issues_list_count(1) expect_filtered_search_input_empty end @@ -537,7 +559,7 @@ describe 'Filter issues', js: true do input_filtered_search("milestone:%#{new_milestone.title}") - expect_tokens([{ name: 'milestone', value: new_milestone.title }]) + expect_tokens([milestone_token(new_milestone.title)]) expect_no_issues_list() expect_filtered_search_input_empty end @@ -549,7 +571,7 @@ describe 'Filter issues', js: true do it 'filters issues by searched milestone and text' do input_filtered_search("milestone:%#{milestone.title} #{search_term}") - expect_tokens([{ name: 'milestone', value: milestone.title }]) + expect_tokens([milestone_token(milestone.title)]) expect_issues_list_count(2) expect_filtered_search_input(search_term) end @@ -557,9 +579,11 @@ describe 'Filter issues', js: true do it 'filters issues by searched milestone, author and text' do input_filtered_search("milestone:%#{milestone.title} author:@#{user.username} #{search_term}") + wait_for_requests + expect_tokens([ - { name: 'milestone', value: milestone.title }, - { name: 'author', value: user.username } + milestone_token(milestone.title), + author_token(user.name) ]) expect_issues_list_count(2) expect_filtered_search_input(search_term) @@ -568,10 +592,12 @@ describe 'Filter issues', js: true do it 'filters issues by searched milestone, author, assignee and text' do input_filtered_search("milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} #{search_term}") + wait_for_requests + expect_tokens([ - { name: 'milestone', value: milestone.title }, - { name: 'author', value: user.username }, - { name: 'assignee', value: user.username } + milestone_token(milestone.title), + author_token(user.name), + assignee_token(user.name) ]) expect_issues_list_count(2) expect_filtered_search_input(search_term) @@ -580,11 +606,13 @@ describe 'Filter issues', js: true do it 'filters issues by searched milestone, author, assignee, label and text' do input_filtered_search("milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} #{search_term}") + wait_for_requests + expect_tokens([ - { name: 'milestone', value: milestone.title }, - { name: 'author', value: user.username }, - { name: 'assignee', value: user.username }, - { name: 'label', value: bug_label.title } + milestone_token(milestone.title), + author_token(user.name), + assignee_token(user.name), + label_token(bug_label.title) ]) expect_issues_list_count(2) expect_filtered_search_input(search_term) diff --git a/spec/features/issues/filtered_search/search_bar_spec.rb b/spec/features/issues/filtered_search/search_bar_spec.rb index aa9d0d842de..a432d031337 100644 --- a/spec/features/issues/filtered_search/search_bar_spec.rb +++ b/spec/features/issues/filtered_search/search_bar_spec.rb @@ -32,7 +32,7 @@ describe 'Search bar', js: true do it 'selects item' do filtered_search.native.send_keys(:down, :down, :enter) - expect_tokens([{ name: 'author' }]) + expect_tokens([author_token]) expect_filtered_search_input_empty end end diff --git a/spec/features/issues/filtered_search/visual_tokens_spec.rb b/spec/features/issues/filtered_search/visual_tokens_spec.rb index 52efe944b69..14a555fde10 100644 --- a/spec/features/issues/filtered_search/visual_tokens_spec.rb +++ b/spec/features/issues/filtered_search/visual_tokens_spec.rb @@ -346,8 +346,8 @@ describe 'Visual tokens', js: true do it 'tokenizes the search term to complete visual token' do expect_tokens([ - { name: 'author', value: '@root' }, - { name: 'assignee', value: 'none' } + author_token(user.name), + assignee_token('none') ]) 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 521fcabc881..166c02a7a7f 100644 --- a/spec/features/merge_requests/filter_by_milestone_spec.rb +++ b/spec/features/merge_requests/filter_by_milestone_spec.rb @@ -25,7 +25,7 @@ feature 'Merge Request filtering by Milestone' do visit_merge_requests(project) input_filtered_search('milestone:none') - expect_tokens([{ name: 'milestone', value: 'none' }]) + expect_tokens([milestone_token('none', false)]) expect_filtered_search_input_empty expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) diff --git a/spec/features/merge_requests/filter_merge_requests_spec.rb b/spec/features/merge_requests/filter_merge_requests_spec.rb index 3686131fee4..b51ae0890e4 100644 --- a/spec/features/merge_requests/filter_merge_requests_spec.rb +++ b/spec/features/merge_requests/filter_merge_requests_spec.rb @@ -24,7 +24,9 @@ describe 'Filter merge requests' do let(:search_query) { "assignee:@#{user.username}" } def expect_assignee_visual_tokens - expect_tokens([{ name: 'assignee', value: "@#{user.username}" }]) + wait_for_requests + + expect_tokens([assignee_token(user.name)]) expect_filtered_search_input_empty end @@ -57,7 +59,7 @@ describe 'Filter merge requests' do let(:search_query) { "milestone:%\"#{milestone.title}\"" } def expect_milestone_visual_tokens - expect_tokens([{ name: 'milestone', value: "%\"#{milestone.title}\"" }]) + expect_tokens([milestone_token("\"#{milestone.title}\"")]) expect_filtered_search_input_empty end @@ -91,7 +93,7 @@ describe 'Filter merge requests' do input_filtered_search('label:none') expect_mr_list_count(1) - expect_tokens([{ name: 'label', value: 'none' }]) + expect_tokens([label_token('none', false)]) expect_filtered_search_input_empty end @@ -99,7 +101,7 @@ describe 'Filter merge requests' do input_filtered_search("label:~#{label.title}") expect_mr_list_count(0) - expect_tokens([{ name: 'label', value: "~#{label.title}" }]) + expect_tokens([label_token(label.title)]) expect_filtered_search_input_empty end @@ -107,10 +109,7 @@ describe 'Filter merge requests' do input_filtered_search("label:~\"#{wontfix.title}\" label:~#{label.title}") expect_mr_list_count(0) - expect_tokens([ - { name: 'label', value: "~\"#{wontfix.title}\"" }, - { name: 'label', value: "~#{label.title}" } - ]) + expect_tokens([label_token("\"#{wontfix.title}\""), label_token(label.title)]) expect_filtered_search_input_empty end @@ -118,16 +117,13 @@ describe 'Filter merge requests' do input_filtered_search("label:~\"#{wontfix.title}\"") expect_mr_list_count(0) - expect_tokens([{ name: 'label', value: "~\"#{wontfix.title}\"" }]) + expect_tokens([label_token("\"#{wontfix.title}\"")]) expect_filtered_search_input_empty input_filtered_search_keys("label:~#{label.title}") expect_mr_list_count(0) - expect_tokens([ - { name: 'label', value: "~\"#{wontfix.title}\"" }, - { name: 'label', value: "~#{label.title}" } - ]) + expect_tokens([label_token("\"#{wontfix.title}\""), label_token(label.title)]) expect_filtered_search_input_empty end end @@ -143,10 +139,9 @@ describe 'Filter merge requests' do context 'assignee and label', js: true do def expect_assignee_label_visual_tokens - expect_tokens([ - { name: 'assignee', value: "@#{user.username}" }, - { name: 'label', value: "~#{label.title}" } - ]) + wait_for_requests + + expect_tokens([assignee_token(user.name), label_token(label.title)]) expect_filtered_search_input_empty end @@ -214,7 +209,7 @@ describe 'Filter merge requests' do input_filtered_search_keys(' label:~bug') expect_mr_list_count(1) - expect_tokens([{ name: 'label', value: '~bug' }]) + expect_tokens([label_token('bug')]) expect_filtered_search_input('Bug') end @@ -227,7 +222,7 @@ describe 'Filter merge requests' do input_filtered_search_keys(' milestone:%8') expect_mr_list_count(1) - expect_tokens([{ name: 'milestone', value: '%8' }]) + expect_tokens([milestone_token('8')]) expect_filtered_search_input('Bug') end @@ -240,7 +235,10 @@ describe 'Filter merge requests' do input_filtered_search_keys(" assignee:@#{user.username}") expect_mr_list_count(1) - expect_tokens([{ name: 'assignee', value: "@#{user.username}" }]) + + wait_for_requests + + expect_tokens([assignee_token(user.name)]) expect_filtered_search_input('Bug') end @@ -252,8 +250,10 @@ describe 'Filter merge requests' do input_filtered_search_keys(" author:@#{user.username}") + wait_for_requests + expect_mr_list_count(1) - expect_tokens([{ name: 'author', value: "@#{user.username}" }]) + expect_tokens([author_token(user.name)]) expect_filtered_search_input('Bug') end end @@ -293,7 +293,9 @@ describe 'Filter merge requests' do it 'filter by current user' do visit project_merge_requests_path(project, assignee_id: user.id) - expect_tokens([{ name: 'assignee', value: "@#{user.username}" }]) + wait_for_requests + + expect_tokens([assignee_token(user.name)]) expect_filtered_search_input_empty end @@ -303,7 +305,9 @@ describe 'Filter merge requests' do visit project_merge_requests_path(project, assignee_id: new_user.id) - expect_tokens([{ name: 'assignee', value: "@#{new_user.username}" }]) + wait_for_requests + + expect_tokens([assignee_token(new_user.name)]) expect_filtered_search_input_empty end end @@ -312,7 +316,9 @@ describe 'Filter merge requests' do it 'filter by current user' do visit project_merge_requests_path(project, author_id: user.id) - expect_tokens([{ name: 'author', value: "@#{user.username}" }]) + wait_for_requests + + expect_tokens([author_token(user.name)]) expect_filtered_search_input_empty end @@ -322,7 +328,9 @@ describe 'Filter merge requests' do visit project_merge_requests_path(project, author_id: new_user.id) - expect_tokens([{ name: 'author', value: "@#{new_user.username}" }]) + wait_for_requests + + expect_tokens([author_token(new_user.name)]) expect_filtered_search_input_empty end end diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb index 9b49fc2225d..6742d77937f 100644 --- a/spec/features/search_spec.rb +++ b/spec/features/search_spec.rb @@ -195,37 +195,33 @@ describe "Search" 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 expect(page).to have_selector('.filtered-search') - expect_tokens([{ name: 'assignee', value: "@#{user.username}" }]) + expect_tokens([assignee_token(user.name)]) expect_filtered_search_input_empty end it 'takes user to her issues page when issues authored is clicked' do find('.dropdown-menu').click_link "Issues I've created" - sleep 2 expect(page).to have_selector('.filtered-search') - expect_tokens([{ name: 'author', value: "@#{user.username}" }]) + expect_tokens([author_token(user.name)]) expect_filtered_search_input_empty end 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 expect(page).to have_selector('.merge-requests-holder') - expect_tokens([{ name: 'assignee', value: "@#{user.username}" }]) + expect_tokens([assignee_token(user.name)]) expect_filtered_search_input_empty end 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 expect(page).to have_selector('.merge-requests-holder') - expect_tokens([{ name: 'author', value: "@#{user.username}" }]) + expect_tokens([author_token(user.name)]) expect_filtered_search_input_empty end end diff --git a/spec/support/filtered_search_helpers.rb b/spec/support/filtered_search_helpers.rb index d21c4324d9e..99b8b6b7ea4 100644 --- a/spec/support/filtered_search_helpers.rb +++ b/spec/support/filtered_search_helpers.rb @@ -54,8 +54,8 @@ module FilteredSearchHelpers # Iterates through each visual token inside # .tokens-container to make sure the correct names and values are rendered def expect_tokens(tokens) - page.find '.filtered-search-box .tokens-container' do - page.all(:css, '.tokens-container li').each_with_index do |el, index| + page.within '.filtered-search-box .tokens-container' do + page.all(:css, '.tokens-container li .selectable').each_with_index do |el, index| token_name = tokens[index][:name] token_value = tokens[index][:value] @@ -67,6 +67,28 @@ module FilteredSearchHelpers end end + def create_token(token_name, token_value = nil, symbol = nil) + { name: token_name, value: "#{symbol}#{token_value}" } + end + + def author_token(author_name = nil) + create_token('Author', author_name) + end + + def assignee_token(assignee_name = nil) + create_token('Assignee', assignee_name) + end + + def milestone_token(milestone_name = nil, has_symbol = true) + symbol = has_symbol ? '%' : nil + create_token('Milestone', milestone_name, symbol) + end + + def label_token(label_name = nil, has_symbol = true) + symbol = has_symbol ? '~' : nil + create_token('Label', label_name, symbol) + end + def default_placeholder 'Search or filter results...' end From ee603a0089520ae22a97d9f5f5d7d083c2fe24ce Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Sun, 13 Aug 2017 14:52:44 +0200 Subject: [PATCH 20/32] Allow a `failure_wait_time` of 0 for storage access This allows testing every storage attempt after a failure. Which could be useful for tests --- config/initializers/6_validations.rb | 4 +- .../git/storage/circuit_breaker_spec.rb | 59 +++++++++++++++---- spec/support/stub_configuration.rb | 5 +- 3 files changed, 55 insertions(+), 13 deletions(-) diff --git a/config/initializers/6_validations.rb b/config/initializers/6_validations.rb index 92ce4dd03cd..f8e67ce04c9 100644 --- a/config/initializers/6_validations.rb +++ b/config/initializers/6_validations.rb @@ -37,12 +37,12 @@ def validate_storages_config storage_validation_error("#{name} is not a valid storage, because it has no `path` key. Refer to gitlab.yml.example for an updated example") end - %w(failure_count_threshold failure_wait_time failure_reset_time storage_timeout).each do |setting| + %w(failure_count_threshold failure_reset_time storage_timeout).each do |setting| # Falling back to the defaults is fine! next if repository_storage[setting].nil? unless repository_storage[setting].to_f > 0 - storage_validation_error("#{setting}, for storage `#{name}` needs to be greater than 0") + storage_validation_error("`#{setting}` for storage `#{name}` needs to be greater than 0") end end end diff --git a/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb b/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb index 9d1763b96ad..c86353abb7c 100644 --- a/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb +++ b/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb @@ -1,9 +1,30 @@ require 'spec_helper' describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: true, broken_storage: true do - let(:circuit_breaker) { described_class.new('default') } + let(:storage_name) { 'default' } + let(:circuit_breaker) { described_class.new(storage_name) } let(:hostname) { Gitlab::Environment.hostname } - let(:cache_key) { "storage_accessible:default:#{hostname}" } + let(:cache_key) { "storage_accessible:#{storage_name}:#{hostname}" } + + before do + # Override test-settings for the circuitbreaker with something more realistic + # for these specs. + stub_storage_settings('default' => { + 'path' => TestEnv.repos_path, + 'failure_count_threshold' => 10, + 'failure_wait_time' => 30, + 'failure_reset_time' => 1800, + 'storage_timeout' => 5 + }, + 'broken' => { + 'path' => 'tmp/tests/non-existent-repositories', + 'failure_count_threshold' => 10, + 'failure_wait_time' => 30, + 'failure_reset_time' => 1800, + 'storage_timeout' => 5 + } + ) + end def value_from_redis(name) Gitlab::Git::Storage.redis.with do |redis| @@ -96,14 +117,14 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: end describe '#circuit_broken?' do - it 'is closed when there is no last failure' do + it 'is working when there is no last failure' do set_in_redis(:last_failure, nil) set_in_redis(:failure_count, 0) expect(circuit_breaker.circuit_broken?).to be_falsey end - it 'is open when there was a recent failure' do + it 'is broken when there was a recent failure' do Timecop.freeze do set_in_redis(:last_failure, 1.second.ago.to_f) set_in_redis(:failure_count, 1) @@ -112,16 +133,34 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: end end - it 'is open when there are to many failures' do + it 'is broken when there are too many failures' do set_in_redis(:last_failure, 1.day.ago.to_f) set_in_redis(:failure_count, 200) expect(circuit_breaker.circuit_broken?).to be_truthy end + + context 'the `failure_wait_time` is set to 0' do + before do + stub_storage_settings('default' => { + 'failure_wait_time' => 0, + 'path' => TestEnv.repos_path + }) + end + + it 'is working even when there is a recent failure' do + Timecop.freeze do + set_in_redis(:last_failure, 0.seconds.ago.to_f) + set_in_redis(:failure_count, 1) + + expect(circuit_breaker.circuit_broken?).to be_falsey + end + end + end end describe "storage_available?" do - context 'when the storage is available' do + context 'the storage is available' do it 'tracks that the storage was accessible an raises the error' do expect(circuit_breaker).to receive(:track_storage_accessible) @@ -136,8 +175,8 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: end end - context 'when storage is not available' do - let(:circuit_breaker) { described_class.new('broken') } + context 'storage is not available' do + let(:storage_name) { 'broken' } it 'tracks that the storage was inaccessible' do expect(circuit_breaker).to receive(:track_storage_inaccessible) @@ -158,8 +197,8 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: end end - context 'when the storage is not available' do - let(:circuit_breaker) { described_class.new('broken') } + context 'the storage is not available' do + let(:storage_name) { 'broken' } it 'raises an error' do expect(circuit_breaker).to receive(:track_storage_inaccessible) diff --git a/spec/support/stub_configuration.rb b/spec/support/stub_configuration.rb index 37c89d37aa0..45c10e78789 100644 --- a/spec/support/stub_configuration.rb +++ b/spec/support/stub_configuration.rb @@ -39,14 +39,17 @@ module StubConfiguration end def stub_storage_settings(messages) + # Default storage is always required + messages['default'] ||= Gitlab.config.repositories.storages.default messages.each do |storage_name, storage_settings| + storage_settings['path'] ||= TestEnv.repos_path storage_settings['failure_count_threshold'] ||= 10 storage_settings['failure_wait_time'] ||= 30 storage_settings['failure_reset_time'] ||= 1800 storage_settings['storage_timeout'] ||= 5 end - allow(Gitlab.config.repositories).to receive(:storages).and_return(messages) + allow(Gitlab.config.repositories).to receive(:storages).and_return(Settingslogic.new(messages)) end private From 93d56eb2a5763cb5f1ac89610bb2e1dc7f77a04a Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Sun, 13 Aug 2017 14:53:49 +0200 Subject: [PATCH 21/32] Use better higher threshold settings for storage access in tests `failure_count_threshold`: We should never need this, but we don't want to block access in tests because of this. `failure_wait_time`: Setting it to 0 now allows each storage attempt `storage_timeout`: Try a bit longer to access storage on CI in case the slow machines take a bit longer to spin up the process to perfom the check --- config/gitlab.yml.example | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index e73db08fcac..25285525846 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -649,6 +649,9 @@ test: default: path: tmp/tests/repositories/ gitaly_address: unix:tmp/tests/gitaly/gitaly.socket + failure_count_threshold: 999999 + failure_wait_time: 0 + storage_timeout: 30 broken: path: tmp/tests/non-existent-repositories gitaly_address: unix:tmp/tests/gitaly/gitaly.socket From aa8592eb872413a83341e49d01edb8a01c4f8ae3 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Wed, 16 Aug 2017 10:05:44 +0200 Subject: [PATCH 22/32] Add reserved names to docs --- doc/gitlab-basics/create-project.md | 5 ++ doc/user/group/index.md | 7 +- doc/user/group/subgroups/index.md | 7 +- doc/user/reserved_names.md | 109 ++++++++++++++++++++++++++++ 4 files changed, 122 insertions(+), 6 deletions(-) create mode 100644 doc/user/reserved_names.md diff --git a/doc/gitlab-basics/create-project.md b/doc/gitlab-basics/create-project.md index 2513f4b420a..763ebc70ed0 100644 --- a/doc/gitlab-basics/create-project.md +++ b/doc/gitlab-basics/create-project.md @@ -1,5 +1,9 @@ # How to create a project in GitLab +>**Notes:** +- For a list of words that are not allowed to be used as project names see the + [reserved names][reserved]. + 1. In your dashboard, click the green **New project** button or use the plus icon in the upper right corner of the navigation bar. @@ -26,3 +30,4 @@ 1. Click **Create project**. [import it]: ../workflow/importing/README.md +[reserved]: ../user/reserved_names.md diff --git a/doc/user/group/index.md b/doc/user/group/index.md index ceec8b74373..9e168e830e5 100644 --- a/doc/user/group/index.md +++ b/doc/user/group/index.md @@ -57,6 +57,10 @@ By doing so: ## Create a new group +> **Notes:** +- For a list of words that are not allowed to be used as group names see the + [reserved names][reserved]. + You can create a group in GitLab from: 1. The Groups page: expand the left menu, click **Groups**, and click the green button **New group**: @@ -213,4 +217,5 @@ for the group (GitLab admins only, available in [GitLab Enterprise Edition Start - **Pipelines quota**: keep track of the [pipeline quota](../admin_area/settings/continuous_integration.md) for the group [permissions]: ../permissions.md#permissions -[ee]: https://about.gitlab.com/products/ \ No newline at end of file +[ee]: https://about.gitlab.com/products/ +[reserved]: ../reserved_names.md diff --git a/doc/user/group/subgroups/index.md b/doc/user/group/subgroups/index.md index 5724dcfab48..d2478aea4bd 100644 --- a/doc/user/group/subgroups/index.md +++ b/doc/user/group/subgroups/index.md @@ -83,10 +83,7 @@ structure. - You need to be an Owner of a group in order to be able to create a subgroup. For more information check the [permissions table][permissions]. - For a list of words that are not allowed to be used as group names see the - [`path_regex.rb` file][reserved] under the `TOP_LEVEL_ROUTES`, `PROJECT_WILDCARD_ROUTES` and `GROUP_ROUTES` lists: - - `TOP_LEVEL_ROUTES`: are names that are reserved as usernames or top level groups - - `PROJECT_WILDCARD_ROUTES`: are names that are reserved for child groups or projects. - - `GROUP_ROUTES`: are names that are reserved for all groups or projects. + [reserved names][reserved]. To create a subgroup: @@ -175,5 +172,5 @@ Here's a list of what you can't do with subgroups: [ce-2772]: https://gitlab.com/gitlab-org/gitlab-ce/issues/2772 [permissions]: ../../permissions.md#group -[reserved]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/path_regex.rb +[reserved]: ../../reserved_names.md [issue]: https://gitlab.com/gitlab-org/gitlab-ce/issues/30472#note_27747600 diff --git a/doc/user/reserved_names.md b/doc/user/reserved_names.md new file mode 100644 index 00000000000..50ec99be48b --- /dev/null +++ b/doc/user/reserved_names.md @@ -0,0 +1,109 @@ +# Reserved project and group names + +Not all project & group names are allowed because they would conflict with +existing routes used by GitLab. + +For a list of words that are not allowed to be used as group or project names, see the +[`path_regex.rb` file][reserved] under the `TOP_LEVEL_ROUTES`, `PROJECT_WILDCARD_ROUTES` and `GROUP_ROUTES` lists: +- `TOP_LEVEL_ROUTES`: are names that are reserved as usernames or top level groups +- `PROJECT_WILDCARD_ROUTES`: are names that are reserved for child groups or projects. +- `GROUP_ROUTES`: are names that are reserved for all groups or projects. + +## Reserved project names + +It is currently not possible to create a project with the following names: + +- - +- badges +- blame +- blob +- builds +- commits +- create +- create_dir +- edit +- environments/folders +- files +- find_file +- gitlab-lfs/objects +- info/lfs/objects +- new +- preview +- raw +- refs +- tree +- update +- wikis + +## Reserved group names + +Currently the following names are reserved as top level groups: + +- 503.html +- - +- .well-known +- 404.html +- 422.html +- 500.html +- 502.html +- abuse_reports +- admin +- api +- apple-touch-icon-precomposed.png +- apple-touch-icon.png +- files +- assets +- autocomplete +- ci +- dashboard +- deploy.html +- explore +- favicon.ico +- groups +- header_logo_dark.png +- header_logo_light.png +- health_check +- help +- import +- invites +- jwt +- koding +- notification_settings +- oauth +- profile +- projects +- public +- robots.txt +- s +- search +- sent_notifications +- slash-command-logo.png +- snippets +- u +- unicorn_test +- unsubscribes +- uploads +- users + +These group names are unavailable as subgroup names: + +- - +- activity +- analytics +- audit_events +- avatar +- edit +- group_members +- hooks +- issues +- labels +- ldap +- ldap_group_links +- merge_requests +- milestones +- notification_setting +- pipeline_quota +- projects +- subgroups + +[reserved]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/path_regex.rb From b54a1e030d3a0ab904bf3c278a2cfaea6ff0b3dc Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Wed, 16 Aug 2017 14:17:19 +0000 Subject: [PATCH 23/32] Make sort by dropdown style consistent --- app/assets/stylesheets/framework/nav.scss | 2 ++ app/views/help/ui.html.haml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index d386ac5ba9c..071f20fc457 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -161,6 +161,8 @@ } .nav-controls { + @include new-style-dropdown; + display: inline-block; float: right; text-align: right; diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index f18c3a74120..445f0dffbcc 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -189,7 +189,7 @@ = icon('chevron-down') %ul.dropdown-menu %li - %a Sort by date + = link_to 'Sort by date', '#' = link_to 'New issue', '#', class: 'btn btn-new btn-inverted' From 90a07deb7995014688c67d54250dca6cb81dc556 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 16 Aug 2017 16:25:00 +0200 Subject: [PATCH 24/32] Use charlock_holmes 0.7.5 --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index a484cefb9a4..d10269d7aac 100644 --- a/Gemfile +++ b/Gemfile @@ -232,7 +232,7 @@ gem 'ace-rails-ap', '~> 4.1.0' gem 'mousetrap-rails', '~> 1.4.6' # Detect and convert string character encoding -gem 'charlock_holmes', '~> 0.7.3' +gem 'charlock_holmes', '~> 0.7.5' # Faster JSON gem 'oj', '~> 2.17.4' diff --git a/Gemfile.lock b/Gemfile.lock index a93caba2393..f7ad7bcbc6e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -117,7 +117,7 @@ GEM activesupport (>= 4.0.0) mime-types (>= 1.16) cause (0.1) - charlock_holmes (0.7.3) + charlock_holmes (0.7.5) chronic (0.10.2) chronic_duration (0.10.6) numerizer (~> 0.1.1) @@ -986,7 +986,7 @@ DEPENDENCIES capybara (~> 2.6.2) capybara-screenshot (~> 1.0.0) carrierwave (~> 1.1) - charlock_holmes (~> 0.7.3) + charlock_holmes (~> 0.7.5) chronic (~> 0.10.2) chronic_duration (~> 0.10.6) concurrent-ruby (~> 1.0.5) From 862da3cfed8db462589d0271e144f21f408cf73b Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 10 Aug 2017 17:53:20 +0200 Subject: [PATCH 25/32] Add more database development related docs --- doc/development/README.md | 2 + .../database_merge_request_checklist.md | 15 +++ doc/development/ordering_table_columns.md | 127 ++++++++++++++++++ doc/development/sql.md | 26 ++++ 4 files changed, 170 insertions(+) create mode 100644 doc/development/database_merge_request_checklist.md create mode 100644 doc/development/ordering_table_columns.md diff --git a/doc/development/README.md b/doc/development/README.md index 58993c52dcd..36367f0a60d 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -46,6 +46,7 @@ ## Databases +- [Merge Request Checklist](database_merge_request_checklist.md) - [What requires downtime?](what_requires_downtime.md) - [Adding database indexes](adding_database_indexes.md) - [Post Deployment Migrations](post_deployment_migrations.md) @@ -56,6 +57,7 @@ - [Background Migrations](background_migrations.md) - [Storing SHA1 Hashes As Binary](sha1_as_binary.md) - [Iterating Tables In Batches](iterating_tables_in_batches.md) +- [Ordering Table Columns](ordering_table_columns.md) ## i18n diff --git a/doc/development/database_merge_request_checklist.md b/doc/development/database_merge_request_checklist.md new file mode 100644 index 00000000000..75c395b61ef --- /dev/null +++ b/doc/development/database_merge_request_checklist.md @@ -0,0 +1,15 @@ +# Merge Request Checklist + +When creating a merge request that performs database related changes (schema +changes, adjusting queries to optimise performance, etc) you should use the +merge request template called "Database Changes". This template contains a +checklist of steps to follow to make sure the changes are up to snuff. + +To use the checklist, create a new merge request and click on the "Choose a +template" dropdown, then click "Database Changes". + +An example of this checklist can be found at +https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12463. + +The source code of the checklist can be found in at +https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab/merge_request_templates/Database%20Changes.md diff --git a/doc/development/ordering_table_columns.md b/doc/development/ordering_table_columns.md new file mode 100644 index 00000000000..249e70c7b0e --- /dev/null +++ b/doc/development/ordering_table_columns.md @@ -0,0 +1,127 @@ +# Ordering Table Columns + +Similar to C structures the space of a table is influenced by the order of +columns. This is because the size of columns is aligned depending on the type of +the column. Take the following column order for example: + +* id (integer, 4 bytes) +* name (text, variable) +* user_id (integer, 4 bytes) + +Integers are aligned to the word size. This means that on a 64 bit platform the +actual size of each column would be: 8 bytes, variable, 8 bytes. This means that +each row will require at least 16 bytes for the two integers, and a variable +amount for the text field. If a table has a few rows this is not an issue, but +once you start storing millions of rows you can save space by using a different +order. For the above example a more ideal column order would be the following: + +* id (integer, 4 bytes) +* user_id (integer, 4 bytes) +* name (text, variable) + +In this setup the `id` and `user_id` columns can be packed together, which means +we only need 8 bytes to store _both_ of them. This in turn each row will require +8 bytes less of space. + +For GitLab we require that columns of new tables are ordered based to use the +least amount of space. An easy way of doing this is to order them based on the +type size in descending order with variable sizes (string and text columns for +example) at the end. + +## Type Sizes + +While the PostgreSQL docuemntation +(https://www.postgresql.org/docs/current/static/datatype.html) contains plenty +of information we will list the sizes of common types here so it's easier to +look them up. Here "word" refers to the word size, which is 4 bytes for a 32 +bits platform and 8 bytes for a 64 bits platform. + +| Type | Size | Aligned To | +|:-----------------|:-------------------------------------|:-----------| +| smallint | 2 bytes | 1 word | +| integer | 4 bytes | 1 word | +| bigint | 8 bytes | 8 bytes | +| real | 4 bytes | 1 word | +| double precision | 8 bytes | 8 bytes | +| boolean | 1 byte | not needed | +| text / string | variable, 1 byte plus the data | 1 word | +| bytea | variable, 1 or 4 bytes plus the data | 1 word | +| timestamp | 8 bytes | 8 bytes | +| timestamptz | 8 bytes | 8 bytes | +| date | 4 bytes | 1 word | + +A "variable" size means the actual size depends on the value being stored. If +PostgreSQL determines this can be embedded directly into a row it may do so, but +for very large values it will store the data externally and store a pointer (of +1 word in size) in the column. Because of this variable sized columns should +always be at the end of a table. + +## Real Example + +Let's use the "events" table as an example, which currently has the following +layout: + +| Column | Type | Size | +|:------------|:----------------------------|:---------| +| id | integer | 4 bytes | +| target_type | character varying | variable | +| target_id | integer | 4 bytes | +| title | character varying | variable | +| data | text | variable | +| project_id | integer | 4 bytes | +| created_at | timestamp without time zone | 8 bytes | +| updated_at | timestamp without time zone | 8 bytes | +| action | integer | 4 bytes | +| author_id | integer | 4 bytes | + +After adding padding to align the columns this would translate to columns being +divided into fixed size chunks as follows: + +| Chunk Size | Columns | +|:-----------|:------------------| +| 8 bytes | id | +| variable | target_type | +| 8 bytes | target_id | +| variable | title | +| variable | data | +| 8 bytes | project_id | +| 8 bytes | created_at | +| 8 bytes | updated_at | +| 8 bytes | action, author_id | + +This means that excluding the variable sized data we need at least 48 bytes per +row. + +We can optimise this by using the following column order instead: + +| Column | Type | Size | +|:------------|:----------------------------|:---------| +| created_at | timestamp without time zone | 8 bytes | +| updated_at | timestamp without time zone | 8 bytes | +| id | integer | 4 bytes | +| target_id | integer | 4 bytes | +| project_id | integer | 4 bytes | +| action | integer | 4 bytes | +| author_id | integer | 4 bytes | +| target_type | character varying | variable | +| title | character varying | variable | +| data | text | variable | + +This would produce the following chunks: + +| Chunk Size | Columns | +|:-----------|:-------------------| +| 8 bytes | created_at | +| 8 bytes | updated_at | +| 8 bytes | id, target_id | +| 8 bytes | project_id, action | +| 8 bytes | author_id | +| variable | target_type | +| variable | title | +| variable | data | + +Here we only need 40 bytes per row excluding the variable sized data. 8 bytes +being saved may not sound like much, but for tables as large as the "events" +table it does begin to matter. For example, when storing 80 000 000 rows this +translates to a space saving of at least 610 MB: all by just changing the order +of a few columns. diff --git a/doc/development/sql.md b/doc/development/sql.md index 23fd7604957..974b1d99dff 100644 --- a/doc/development/sql.md +++ b/doc/development/sql.md @@ -216,4 +216,30 @@ exact same results. This also means there's no need to add an index on `created_at` to ensure consistent performance as `id` is already indexed by default. +## Use WHERE EXISTS instead of WHERE IN + +While `WHERE IN` and `WHERE EXISTS` can be used to produce the same data it is +recommended to use `WHERE EXISTS` whenever possible. While in many cases +PostgreSQL can optimise `WHERE IN` quite well there are also many cases where +`WHERE EXISTS` will perform (much) better. + +In Rails you have to use this by creating SQL fragments: + +```ruby +Project.where('EXISTS (?)', User.select(1).where('projects.creator_id = users.id AND users.foo = X')) +``` + +This would then produce a query along the lines of the following: + +```sql +SELECT * +FROM projects +WHERE EXISTS ( + SELECT 1 + FROM users + WHERE projects.creator_id = users.id + AND users.foo = X +) +``` + [gin-index]: http://www.postgresql.org/docs/current/static/gin.html From 39c5be7a1307e26a31f86f726df3c2f9ccd0c7d8 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Fri, 11 Aug 2017 14:06:43 +0200 Subject: [PATCH 26/32] State that comma separated data is serialised data Comma separated values really are a form of serialised data so we should clarify that we shouldn't store such data in the DB. --- doc/development/serializing_data.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/development/serializing_data.md b/doc/development/serializing_data.md index 2b56f48bc44..37332c20147 100644 --- a/doc/development/serializing_data.md +++ b/doc/development/serializing_data.md @@ -1,7 +1,8 @@ # Serializing Data **Summary:** don't store serialized data in the database, use separate columns -and/or tables instead. +and/or tables instead. This includes storing of comma separated values as a +string. Rails makes it possible to store serialized data in JSON, YAML or other formats. Such a field can be defined as follows: From 6735e1dc9a3ae2e55c837f4ecea4bc3c6972a671 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Fri, 11 Aug 2017 14:12:37 +0200 Subject: [PATCH 27/32] Document how to handle different DB (versions) --- doc/development/README.md | 1 + .../verifying_database_capabilities.md | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 doc/development/verifying_database_capabilities.md diff --git a/doc/development/README.md b/doc/development/README.md index 36367f0a60d..50b064ee960 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -58,6 +58,7 @@ - [Storing SHA1 Hashes As Binary](sha1_as_binary.md) - [Iterating Tables In Batches](iterating_tables_in_batches.md) - [Ordering Table Columns](ordering_table_columns.md) +- [Verifying Database Capabilities](verifying_database_capabilities.md) ## i18n diff --git a/doc/development/verifying_database_capabilities.md b/doc/development/verifying_database_capabilities.md new file mode 100644 index 00000000000..cc6d62957e3 --- /dev/null +++ b/doc/development/verifying_database_capabilities.md @@ -0,0 +1,26 @@ +# Verifying Database Capabilities + +Sometimes certain bits of code may only work on a certain database and/or +version. While we try to avoid such code as much as possible sometimes it is +necessary to add database (version) specific behaviour. + +To facilitate this we have the following methods that you can use: + +* `Gitlab::Database.postgresql?`: returns `true` if PostgreSQL is being used +* `Gitlab::Database.mysql?`: returns `true` if MySQL is being used +* `Gitlab::Database.version`: returns the PostgreSQL version number as a string + in the format `X.Y.Z`. This method does not work for MySQL + +This allows you to write code such as: + +```ruby +if Gitlab::Database.postgresql? + if Gitlab::Database.version.to_f >= 9.6 + run_really_fast_query + else + run_fast_query + end +else + run_query +end +``` From a4a8cae7e1d4f5c72ddc0fce18d8530bf0e6c911 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Fri, 11 Aug 2017 14:16:08 +0200 Subject: [PATCH 28/32] Document not using database hash indexes --- doc/development/README.md | 1 + doc/development/hash_indexes.md | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 doc/development/hash_indexes.md diff --git a/doc/development/README.md b/doc/development/README.md index 50b064ee960..dd150421b65 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -59,6 +59,7 @@ - [Iterating Tables In Batches](iterating_tables_in_batches.md) - [Ordering Table Columns](ordering_table_columns.md) - [Verifying Database Capabilities](verifying_database_capabilities.md) +- [Hash Indexes](hash_indexes.md) ## i18n diff --git a/doc/development/hash_indexes.md b/doc/development/hash_indexes.md new file mode 100644 index 00000000000..e6c1b3590b1 --- /dev/null +++ b/doc/development/hash_indexes.md @@ -0,0 +1,20 @@ +# Hash Indexes + +Both PostgreSQL and MySQL support hash indexes besides the regular btree +indexes. Hash indexes however are to be avoided at all costs. While they may +_sometimes_ provide better performance the cost of rehashing can be very high. +More importantly: at least until PostgreSQL 10.0 hash indexes are not +WAL-logged, meaning they are not replicated to any replicas. From the PostgreSQL +documentation: + +> Hash index operations are not presently WAL-logged, so hash indexes might need +> to be rebuilt with REINDEX after a database crash if there were unwritten +> changes. Also, changes to hash indexes are not replicated over streaming or +> file-based replication after the initial base backup, so they give wrong +> answers to queries that subsequently use them. For these reasons, hash index +> use is presently discouraged. + +RuboCop is configured to register an offence when it detects the use of a hash +index. + +Instead of using hash indexes you should use regular btree indexes. From 138f07ada72e27e4ffa31ef303d74183168bfdaa Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 16 Aug 2017 15:56:12 +0100 Subject: [PATCH 29/32] Fix commit list request appending 40 to offset Closes #36569 --- app/assets/javascripts/commits.js | 2 +- changelogs/unreleased/commits-list-page-limit.yml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/commits-list-page-limit.yml diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js index 2b0bf49cf92..047544b1762 100644 --- a/app/assets/javascripts/commits.js +++ b/app/assets/javascripts/commits.js @@ -17,7 +17,7 @@ window.CommitsList = (function() { } }); - Pager.init(limit, false, false, this.processCommits); + Pager.init(parseInt(limit, 10), false, false, this.processCommits); this.content = $("#commits-list"); this.searchField = $("#commits-search"); diff --git a/changelogs/unreleased/commits-list-page-limit.yml b/changelogs/unreleased/commits-list-page-limit.yml new file mode 100644 index 00000000000..2fd54c5960a --- /dev/null +++ b/changelogs/unreleased/commits-list-page-limit.yml @@ -0,0 +1,5 @@ +--- +title: Fix commit list not loading the correct page when scrolling +merge_request: +author: +type: fixed From e9f85bf17973d7baaee43c3562473b00885de410 Mon Sep 17 00:00:00 2001 From: Regis Date: Wed, 16 Aug 2017 09:55:17 -0600 Subject: [PATCH 30/32] change to 'Not confidential' --- .../components/confidential/confidential_issue_sidebar.vue | 2 +- spec/javascripts/sidebar/confidential_issue_sidebar_spec.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue index cfacba09fad..8e7abdbffef 100644 --- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue @@ -71,7 +71,7 @@ export default { />

- This issue is not confidential + Not confidential
diff --git a/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js b/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js index 2e16adffb5b..88a33caf2e3 100644 --- a/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js +++ b/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js @@ -41,7 +41,7 @@ describe('Confidential Issue Sidebar Block', () => { ).toBe(true); expect( - vm2.$el.innerHTML.includes('This issue is not confidential'), + vm2.$el.innerHTML.includes('Not confidential'), ).toBe(true); }); From ba7251fefd92b0ecb6365cfe55510e24c5343ac6 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 15 Aug 2017 13:22:55 +0200 Subject: [PATCH 31/32] Only create commit GPG signature when necessary --- app/models/commit.rb | 2 +- app/models/gpg_signature.rb | 4 ++ app/services/git_push_service.rb | 15 +++- app/workers/create_gpg_signature_worker.rb | 8 +-- .../active_record_array_type_casting.rb | 20 ++++++ lib/gitlab/git/commit.rb | 19 ++--- lib/gitlab/gpg/commit.rb | 38 ++++++---- .../gpg/invalid_gpg_signature_updater.rb | 4 +- spec/lib/gitlab/gpg/commit_spec.rb | 71 ++++++++++--------- .../gpg/invalid_gpg_signature_updater_spec.rb | 23 +++--- spec/services/git_push_service_spec.rb | 34 ++++++++- .../create_gpg_signature_worker_spec.rb | 26 +++---- 12 files changed, 160 insertions(+), 104 deletions(-) create mode 100644 config/initializers/active_record_array_type_casting.rb diff --git a/app/models/commit.rb b/app/models/commit.rb index 638fddc5d3d..be9a56c190c 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -383,6 +383,6 @@ class Commit end def gpg_commit - @gpg_commit ||= Gitlab::Gpg::Commit.new(self) + @gpg_commit ||= Gitlab::Gpg::Commit.for_commit(self) end end diff --git a/app/models/gpg_signature.rb b/app/models/gpg_signature.rb index 1ac0e123ff1..50fb35c77ec 100644 --- a/app/models/gpg_signature.rb +++ b/app/models/gpg_signature.rb @@ -18,4 +18,8 @@ class GpgSignature < ActiveRecord::Base def commit project.commit(commit_sha) end + + def gpg_commit + Gitlab::Gpg::Commit.new(project, commit_sha) + end end diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index ada2b64a3a6..e81a56672e2 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -90,8 +90,19 @@ class GitPushService < BaseService end def update_signatures - @push_commits.each do |commit| - CreateGpgSignatureWorker.perform_async(commit.sha, @project.id) + commit_shas = @push_commits.last(PROCESS_COMMIT_LIMIT).map(&:sha) + + return if commit_shas.empty? + + shas_with_cached_signatures = GpgSignature.where(commit_sha: commit_shas).pluck(:commit_sha) + commit_shas -= shas_with_cached_signatures + + return if commit_shas.empty? + + commit_shas = Gitlab::Git::Commit.shas_with_signatures(project.repository, commit_shas) + + commit_shas.each do |sha| + CreateGpgSignatureWorker.perform_async(sha, project.id) end end diff --git a/app/workers/create_gpg_signature_worker.rb b/app/workers/create_gpg_signature_worker.rb index 4f47717ff69..f34dff2d656 100644 --- a/app/workers/create_gpg_signature_worker.rb +++ b/app/workers/create_gpg_signature_worker.rb @@ -4,13 +4,9 @@ class CreateGpgSignatureWorker def perform(commit_sha, project_id) project = Project.find_by(id: project_id) - return unless project - commit = project.commit(commit_sha) - - return unless commit - - commit.signature + # This calculates and caches the signature in the database + Gitlab::Gpg::Commit.new(project, commit_sha).signature end end diff --git a/config/initializers/active_record_array_type_casting.rb b/config/initializers/active_record_array_type_casting.rb new file mode 100644 index 00000000000..d94d592add6 --- /dev/null +++ b/config/initializers/active_record_array_type_casting.rb @@ -0,0 +1,20 @@ +module ActiveRecord + class PredicateBuilder + class ArrayHandler + module TypeCasting + def call(attribute, value) + # This is necessary because by default ActiveRecord does not respect + # custom type definitions (like our `ShaAttribute`) when providing an + # array in `where`, like in `where(commit_sha: [sha1, sha2, sha3])`. + model = attribute.relation&.engine + type = model.user_provided_columns[attribute.name] if model + value = value.map { |value| type.type_cast_for_database(value) } if type + + super(attribute, value) + end + end + + prepend TypeCasting + end + end +end diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb index fd4dfdb09a2..a499bbc6266 100644 --- a/lib/gitlab/git/commit.rb +++ b/lib/gitlab/git/commit.rb @@ -210,6 +210,16 @@ module Gitlab @rugged_sort_types.fetch(sort_type, Rugged::SORT_NONE) end + + def shas_with_signatures(repository, shas) + shas.select do |sha| + begin + Rugged::Commit.extract_signature(repository.rugged, sha) + rescue Rugged::OdbError + false + end + end + end end def initialize(repository, raw_commit, head = nil) @@ -335,15 +345,6 @@ module Gitlab parent_ids.map { |oid| self.class.find(@repository, oid) }.compact end - # Get the gpg signature of this commit. - # - # Ex. - # commit.signature(repo) - # - def signature(repo) - Rugged::Commit.extract_signature(repo.rugged, sha) - end - def stats Gitlab::Git::CommitStats.new(self) end diff --git a/lib/gitlab/gpg/commit.rb b/lib/gitlab/gpg/commit.rb index 55428b85207..606c7576f70 100644 --- a/lib/gitlab/gpg/commit.rb +++ b/lib/gitlab/gpg/commit.rb @@ -1,12 +1,20 @@ module Gitlab module Gpg class Commit - attr_reader :commit + def self.for_commit(commit) + new(commit.project, commit.sha) + end - def initialize(commit) - @commit = commit + def initialize(project, sha) + @project = project + @sha = sha - @signature_text, @signed_text = commit.raw.signature(commit.project.repository) + @signature_text, @signed_text = + begin + Rugged::Commit.extract_signature(project.repository.rugged, sha) + rescue Rugged::OdbError + nil + end end def has_signature? @@ -16,18 +24,20 @@ module Gitlab def signature return unless has_signature? - cached_signature = GpgSignature.find_by(commit_sha: commit.sha) - return cached_signature if cached_signature.present? + return @signature if @signature - using_keychain do |gpg_key| - create_cached_signature!(gpg_key) - end + cached_signature = GpgSignature.find_by(commit_sha: @sha) + return @signature = cached_signature if cached_signature.present? + + @signature = create_cached_signature! end def update_signature!(cached_signature) using_keychain do |gpg_key| cached_signature.update_attributes!(attributes(gpg_key)) end + + @signature = cached_signature end private @@ -55,16 +65,18 @@ module Gitlab end end - def create_cached_signature!(gpg_key) - GpgSignature.create!(attributes(gpg_key)) + def create_cached_signature! + using_keychain do |gpg_key| + GpgSignature.create!(attributes(gpg_key)) + end end def attributes(gpg_key) user_infos = user_infos(gpg_key) { - commit_sha: commit.sha, - project: commit.project, + commit_sha: @sha, + project: @project, gpg_key: gpg_key, gpg_key_primary_keyid: gpg_key&.primary_keyid || verified_signature.fingerprint, gpg_key_user_name: user_infos[:name], diff --git a/lib/gitlab/gpg/invalid_gpg_signature_updater.rb b/lib/gitlab/gpg/invalid_gpg_signature_updater.rb index 3bb491120ba..a525ee7a9ee 100644 --- a/lib/gitlab/gpg/invalid_gpg_signature_updater.rb +++ b/lib/gitlab/gpg/invalid_gpg_signature_updater.rb @@ -10,9 +10,7 @@ module Gitlab .select(:id, :commit_sha, :project_id) .where('gpg_key_id IS NULL OR valid_signature = ?', false) .where(gpg_key_primary_keyid: @gpg_key.primary_keyid) - .find_each do |gpg_signature| - Gitlab::Gpg::Commit.new(gpg_signature.commit).update_signature!(gpg_signature) - end + .find_each { |sig| sig.gpg_commit.update_signature!(sig) } end end end diff --git a/spec/lib/gitlab/gpg/commit_spec.rb b/spec/lib/gitlab/gpg/commit_spec.rb index ddb8dd9f0f4..e521fcc6dc1 100644 --- a/spec/lib/gitlab/gpg/commit_spec.rb +++ b/spec/lib/gitlab/gpg/commit_spec.rb @@ -1,13 +1,13 @@ require 'rails_helper' -RSpec.describe Gitlab::Gpg::Commit do +describe Gitlab::Gpg::Commit do describe '#signature' do let!(:project) { create :project, :repository, path: 'sample-project' } let!(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' } - context 'unisgned commit' do + context 'unsigned commit' do it 'returns nil' do - expect(described_class.new(project.commit).signature).to be_nil + expect(described_class.new(project, commit_sha).signature).to be_nil end end @@ -16,18 +16,19 @@ RSpec.describe Gitlab::Gpg::Commit do create :gpg_key, key: GpgHelpers::User1.public_key, user: create(:user, email: GpgHelpers::User1.emails.first) end - let!(:commit) do - raw_commit = double(:raw_commit, signature: [ - GpgHelpers::User1.signed_commit_signature, - GpgHelpers::User1.signed_commit_base_data - ], sha: commit_sha) - allow(raw_commit).to receive :save! - - create :commit, git_commit: raw_commit, project: project + before do + allow(Rugged::Commit).to receive(:extract_signature) + .with(Rugged::Repository, commit_sha) + .and_return( + [ + GpgHelpers::User1.signed_commit_signature, + GpgHelpers::User1.signed_commit_base_data + ] + ) end it 'returns a valid signature' do - expect(described_class.new(commit).signature).to have_attributes( + expect(described_class.new(project, commit_sha).signature).to have_attributes( commit_sha: commit_sha, project: project, gpg_key: gpg_key, @@ -39,7 +40,7 @@ RSpec.describe Gitlab::Gpg::Commit do end it 'returns the cached signature on second call' do - gpg_commit = described_class.new(commit) + gpg_commit = described_class.new(project, commit_sha) expect(gpg_commit).to receive(:using_keychain).and_call_original gpg_commit.signature @@ -53,18 +54,19 @@ RSpec.describe Gitlab::Gpg::Commit do context 'known but unverified public key' do let!(:gpg_key) { create :gpg_key, key: GpgHelpers::User1.public_key } - let!(:commit) do - raw_commit = double(:raw_commit, signature: [ - GpgHelpers::User1.signed_commit_signature, - GpgHelpers::User1.signed_commit_base_data - ], sha: commit_sha) - allow(raw_commit).to receive :save! - - create :commit, git_commit: raw_commit, project: project + before do + allow(Rugged::Commit).to receive(:extract_signature) + .with(Rugged::Repository, commit_sha) + .and_return( + [ + GpgHelpers::User1.signed_commit_signature, + GpgHelpers::User1.signed_commit_base_data + ] + ) end it 'returns an invalid signature' do - expect(described_class.new(commit).signature).to have_attributes( + expect(described_class.new(project, commit_sha).signature).to have_attributes( commit_sha: commit_sha, project: project, gpg_key: gpg_key, @@ -76,7 +78,7 @@ RSpec.describe Gitlab::Gpg::Commit do end it 'returns the cached signature on second call' do - gpg_commit = described_class.new(commit) + gpg_commit = described_class.new(project, commit_sha) expect(gpg_commit).to receive(:using_keychain).and_call_original gpg_commit.signature @@ -88,20 +90,19 @@ RSpec.describe Gitlab::Gpg::Commit do end context 'unknown public key' do - let!(:commit) do - raw_commit = double(:raw_commit, signature: [ - GpgHelpers::User1.signed_commit_signature, - GpgHelpers::User1.signed_commit_base_data - ], sha: commit_sha) - allow(raw_commit).to receive :save! - - create :commit, - git_commit: raw_commit, - project: project + before do + allow(Rugged::Commit).to receive(:extract_signature) + .with(Rugged::Repository, commit_sha) + .and_return( + [ + GpgHelpers::User1.signed_commit_signature, + GpgHelpers::User1.signed_commit_base_data + ] + ) end it 'returns an invalid signature' do - expect(described_class.new(commit).signature).to have_attributes( + expect(described_class.new(project, commit_sha).signature).to have_attributes( commit_sha: commit_sha, project: project, gpg_key: nil, @@ -113,7 +114,7 @@ RSpec.describe Gitlab::Gpg::Commit do end it 'returns the cached signature on second call' do - gpg_commit = described_class.new(commit) + gpg_commit = described_class.new(project, commit_sha) expect(gpg_commit).to receive(:using_keychain).and_call_original gpg_commit.signature diff --git a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb index c4e04ee46a2..4de4419de27 100644 --- a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb +++ b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb @@ -4,23 +4,16 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do describe '#run' do let!(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' } let!(:project) { create :project, :repository, path: 'sample-project' } - let!(:raw_commit) do - raw_commit = double(:raw_commit, signature: [ - GpgHelpers::User1.signed_commit_signature, - GpgHelpers::User1.signed_commit_base_data - ], sha: commit_sha) - - allow(raw_commit).to receive :save! - - raw_commit - end - - let!(:commit) do - create :commit, git_commit: raw_commit, project: project - end before do - allow_any_instance_of(Project).to receive(:commit).and_return(commit) + allow(Rugged::Commit).to receive(:extract_signature) + .with(Rugged::Repository, commit_sha) + .and_return( + [ + GpgHelpers::User1.signed_commit_signature, + GpgHelpers::User1.signed_commit_base_data + ] + ) end context 'gpg signature did have an associated gpg key which was removed later' do diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index 8485605b398..e3c1bdce300 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -688,10 +688,38 @@ describe GitPushService, services: true do ) end - it 'calls CreateGpgSignatureWorker.perform_async for each commit' do - expect(CreateGpgSignatureWorker).to receive(:perform_async).with(sample_commit.id, project.id) + context 'when the commit has a signature' do + context 'when the signature is already cached' do + before do + create(:gpg_signature, commit_sha: sample_commit.id) + end - execute_service(project, user, oldrev, newrev, ref) + it 'does not queue a CreateGpgSignatureWorker' do + expect(CreateGpgSignatureWorker).not_to receive(:perform_async).with(sample_commit.id, project.id) + + execute_service(project, user, oldrev, newrev, ref) + end + end + + context 'when the signature is not yet cached' do + it 'queues a CreateGpgSignatureWorker' do + expect(CreateGpgSignatureWorker).to receive(:perform_async).with(sample_commit.id, project.id) + + execute_service(project, user, oldrev, newrev, ref) + end + end + end + + context 'when the commit does not have a signature' do + before do + allow(Gitlab::Git::Commit).to receive(:shas_with_signatures).with(project.repository, [sample_commit.id]).and_return([]) + end + + it 'does not queue a CreateGpgSignatureWorker' do + expect(CreateGpgSignatureWorker).not_to receive(:perform_async).with(sample_commit.id, project.id) + + execute_service(project, user, oldrev, newrev, ref) + end end end diff --git a/spec/workers/create_gpg_signature_worker_spec.rb b/spec/workers/create_gpg_signature_worker_spec.rb index c6a17d77d73..54978baca88 100644 --- a/spec/workers/create_gpg_signature_worker_spec.rb +++ b/spec/workers/create_gpg_signature_worker_spec.rb @@ -1,34 +1,26 @@ require 'spec_helper' describe CreateGpgSignatureWorker do + let(:project) { create(:project, :repository) } + context 'when GpgKey is found' do - it 'calls Commit#signature' do - commit_sha = '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' - project = create :project - commit = instance_double(Commit) + let(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' } - allow(Project).to receive(:find_by).with(id: project.id).and_return(project) - allow(project).to receive(:commit).with(commit_sha).and_return(commit) + it 'calls Gitlab::Gpg::Commit#signature' do + expect(Gitlab::Gpg::Commit).to receive(:new).with(project, commit_sha).and_call_original - expect(commit).to receive(:signature) + expect_any_instance_of(Gitlab::Gpg::Commit).to receive(:signature) described_class.new.perform(commit_sha, project.id) end end context 'when Commit is not found' do - let(:nonexisting_commit_sha) { 'bogus' } - let(:project) { create :project } + let(:nonexisting_commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a34' } it 'does not raise errors' do expect { described_class.new.perform(nonexisting_commit_sha, project.id) }.not_to raise_error end - - it 'does not call Commit#signature' do - expect_any_instance_of(Commit).not_to receive(:signature) - - described_class.new.perform(nonexisting_commit_sha, project.id) - end end context 'when Project is not found' do @@ -38,8 +30,8 @@ describe CreateGpgSignatureWorker do expect { described_class.new.perform(anything, nonexisting_project_id) }.not_to raise_error end - it 'does not call Commit#signature' do - expect_any_instance_of(Commit).not_to receive(:signature) + it 'does not call Gitlab::Gpg::Commit#signature' do + expect_any_instance_of(Gitlab::Gpg::Commit).not_to receive(:signature) described_class.new.perform(anything, nonexisting_project_id) end From 49df4118760a489ab91cc01ac01542cf1ffeba49 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Wed, 16 Aug 2017 17:03:58 +0000 Subject: [PATCH 32/32] Repo editor with flex layout --- .../javascripts/repo/components/repo.vue | 21 ++++++++------ app/assets/stylesheets/pages/repo.scss | 29 +++++++++++++------ 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/app/assets/javascripts/repo/components/repo.vue b/app/assets/javascripts/repo/components/repo.vue index 3d5e01c8ec0..d6c864cb976 100644 --- a/app/assets/javascripts/repo/components/repo.vue +++ b/app/assets/javascripts/repo/components/repo.vue @@ -43,15 +43,18 @@ export default {