From feb61d56e7ce9ab2cd994486bbad9887c3c023f5 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Fri, 13 Nov 2020 18:09:11 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .rubocop_manual_todo.yml | 3 - .../application_settings/general/index.js | 20 +- .../pages/profiles/preferences/show/index.js | 3 + .../components/integration_view.vue | 81 ++++++ .../components/profile_preferences.vue | 56 ++++ .../preferences/profile_preferences_bundle.js | 23 ++ .../releases/components/app_edit_new.vue | 1 - .../components/tag_field_existing.vue | 20 +- .../releases/stores/modules/detail/state.js | 2 - .../components/edit_area.vue | 8 +- .../static_site_editor/image_repository.js | 4 +- .../services/renderers/render_image.js | 18 +- .../components/integrations_help_text.vue | 35 +++ .../modals/add_image/add_image_modal.vue | 21 +- .../rich_content_editor.vue | 3 +- .../services/editor_service.js | 23 +- app/controllers/concerns/notes_actions.rb | 6 +- .../merge_requests/diffs_controller.rb | 12 +- .../projects/static_site_editor_controller.rb | 3 - .../concerns/caching_array_resolver.rb | 128 +++++++++ .../resolvers/concerns/resolves_snippets.rb | 2 +- .../resolvers/projects/snippets_resolver.rb | 1 + app/graphql/resolvers/snippets_resolver.rb | 1 + .../resolvers/users/snippets_resolver.rb | 1 + app/helpers/diff_helper.rb | 14 + app/helpers/gitpod_helper.rb | 5 +- app/helpers/preferences_helper.rb | 4 +- app/helpers/releases_helper.rb | 1 - app/helpers/sourcegraph_helper.rb | 26 +- app/serializers/diff_file_entity.rb | 16 +- app/serializers/diffs_entity.rb | 6 +- app/serializers/paginated_diff_entity.rb | 8 +- app/services/users/approve_service.rb | 7 + .../application_settings/_gitpod.html.haml | 2 +- app/views/devise/registrations/new.html.haml | 6 +- .../profiles/preferences/_gitpod.html.haml | 9 - .../preferences/_integrations.html.haml | 18 -- .../preferences/_sourcegraph.html.haml | 10 - app/views/profiles/preferences/show.html.haml | 269 +++++++++--------- app/workers/post_receive.rb | 2 +- ...784-notes-etag-only-for-empty-response.yml | 5 + .../218529-display-uploaded-images.yml | 5 + .../281697-fj-fix-snippet-resolvers.yml | 5 + .../nfriend-update-tag-name-placeholder.yml | 5 + .../sh-enable-sidekiq-arg-logging-default.yml | 5 + .../display_merge_conflicts_in_diff.yml | 2 +- config/initializers/sidekiq.rb | 7 +- doc/administration/audit_events.md | 1 + doc/administration/logs.md | 4 +- doc/administration/troubleshooting/sidekiq.md | 29 +- .../graphql/reference/gitlab_schema.graphql | 45 ++- doc/api/graphql/reference/gitlab_schema.json | 119 ++++---- doc/api/graphql/reference/index.md | 11 +- doc/api/projects.md | 32 +++ doc/ci/cloud_deployment/index.md | 11 +- .../product_analytics/usage_ping.md | 17 ++ doc/development/sidekiq_style_guide.md | 4 +- ...oval_settings_compliance_project_v13_5.png | Bin 33148 -> 0 bytes .../img/scope_mr_approval_settings_v13_5.png | Bin 55092 -> 0 bytes .../admin_area/merge_requests_approvals.md | 41 +-- .../compliance/compliance_dashboard/index.md | 1 + doc/user/gitlab_com/index.md | 2 +- doc/user/project/index.md | 8 + doc/user/project/static_site_editor/index.md | 31 +- lib/gitlab/conflict/file.rb | 47 ++- lib/gitlab/diff/line.rb | 4 +- lib/gitlab/etag_caching/middleware.rb | 19 +- lib/gitlab/graphql/present/instrumentation.rb | 11 +- .../metrics/requests_rack_middleware.rb | 10 +- lib/gitlab/sidekiq_logging/logs_jobs.rb | 2 +- .../usage_data_counters/designs_counter.rb | 38 +-- .../usage_data_counters/web_ide_counter.rb | 31 +- locale/gitlab.pot | 63 ++-- .../merge_requests/diffs_controller_spec.rb | 1 + .../projects/notes_controller_spec.rb | 19 ++ .../integration_view_spec.js.snap | 67 +++++ .../profile_preferences_spec.js.snap | 51 ++++ .../components/integration_view_spec.js | 124 ++++++++ .../components/profile_preferences_spec.js | 57 ++++ .../frontend/profile/preferences/mock_data.js | 18 ++ .../releases/components/app_edit_new_spec.js | 1 - .../components/tag_field_exsting_spec.js | 21 +- .../stores/modules/detail/actions_spec.js | 1 - .../stores/modules/detail/mutations_spec.js | 1 - .../services/renderers/render_image_spec.js | 52 +++- .../integration_help_text_spec.js.snap | 27 ++ .../components/integration_help_text_spec.js | 57 ++++ .../editor_service_spec.js | 21 +- .../modals/add_image/add_image_modal_spec.js | 5 +- .../rich_content_editor_spec.js | 2 +- .../concerns/caching_array_resolver_spec.rb | 208 ++++++++++++++ spec/helpers/releases_helper_spec.rb | 2 - spec/helpers/sourcegraph_helper_spec.rb | 37 +-- spec/lib/gitlab/conflict/file_spec.rb | 45 +++ .../gitlab/etag_caching/middleware_spec.rb | 33 +++ .../metrics/requests_rack_middleware_spec.rb | 2 +- .../sidekiq_logging/structured_logger_spec.rb | 20 +- spec/requests/projects/noteable_notes_spec.rb | 40 +++ spec/serializers/diff_file_entity_spec.rb | 11 + spec/serializers/diffs_entity_spec.rb | 50 +++- .../serializers/paginated_diff_entity_spec.rb | 46 +++ spec/spec_helper.rb | 2 +- spec/support/helpers/graphql_helpers.rb | 13 +- .../preferences/show.html.haml_spec.rb | 57 ---- 104 files changed, 1938 insertions(+), 646 deletions(-) create mode 100644 app/assets/javascripts/pages/profiles/preferences/show/index.js create mode 100644 app/assets/javascripts/profile/preferences/components/integration_view.vue create mode 100644 app/assets/javascripts/profile/preferences/components/profile_preferences.vue create mode 100644 app/assets/javascripts/profile/preferences/profile_preferences_bundle.js create mode 100644 app/assets/javascripts/vue_shared/components/integrations_help_text.vue create mode 100644 app/graphql/resolvers/concerns/caching_array_resolver.rb delete mode 100644 app/views/profiles/preferences/_gitpod.html.haml delete mode 100644 app/views/profiles/preferences/_integrations.html.haml delete mode 100644 app/views/profiles/preferences/_sourcegraph.html.haml create mode 100644 changelogs/unreleased/209784-notes-etag-only-for-empty-response.yml create mode 100644 changelogs/unreleased/218529-display-uploaded-images.yml create mode 100644 changelogs/unreleased/281697-fj-fix-snippet-resolvers.yml create mode 100644 changelogs/unreleased/nfriend-update-tag-name-placeholder.yml create mode 100644 changelogs/unreleased/sh-enable-sidekiq-arg-logging-default.yml delete mode 100644 doc/user/admin_area/img/mr_approval_settings_compliance_project_v13_5.png delete mode 100644 doc/user/admin_area/img/scope_mr_approval_settings_v13_5.png create mode 100644 spec/frontend/profile/preferences/components/__snapshots__/integration_view_spec.js.snap create mode 100644 spec/frontend/profile/preferences/components/__snapshots__/profile_preferences_spec.js.snap create mode 100644 spec/frontend/profile/preferences/components/integration_view_spec.js create mode 100644 spec/frontend/profile/preferences/components/profile_preferences_spec.js create mode 100644 spec/frontend/profile/preferences/mock_data.js create mode 100644 spec/frontend/vue_shared/components/__snapshots__/integration_help_text_spec.js.snap create mode 100644 spec/frontend/vue_shared/components/integration_help_text_spec.js create mode 100644 spec/graphql/resolvers/concerns/caching_array_resolver_spec.rb create mode 100644 spec/requests/projects/noteable_notes_spec.rb diff --git a/.rubocop_manual_todo.yml b/.rubocop_manual_todo.yml index c53b3b059d7..56a7b19eb3b 100644 --- a/.rubocop_manual_todo.yml +++ b/.rubocop_manual_todo.yml @@ -55,10 +55,7 @@ Graphql/ResolverType: - 'app/graphql/resolvers/merge_requests_resolver.rb' - 'app/graphql/resolvers/project_merge_requests_resolver.rb' - 'app/graphql/resolvers/project_pipelines_resolver.rb' - - 'app/graphql/resolvers/projects/snippets_resolver.rb' - - 'app/graphql/resolvers/snippets_resolver.rb' - 'app/graphql/resolvers/users/group_count_resolver.rb' - - 'app/graphql/resolvers/users/snippets_resolver.rb' - 'ee/app/graphql/resolvers/ci/jobs_resolver.rb' - 'ee/app/graphql/resolvers/geo/merge_request_diff_registries_resolver.rb' - 'ee/app/graphql/resolvers/geo/package_file_registries_resolver.rb' diff --git a/app/assets/javascripts/pages/admin/application_settings/general/index.js b/app/assets/javascripts/pages/admin/application_settings/general/index.js index 8183e81fb02..af1595398a8 100644 --- a/app/assets/javascripts/pages/admin/application_settings/general/index.js +++ b/app/assets/javascripts/pages/admin/application_settings/general/index.js @@ -1,3 +1,21 @@ +import Vue from 'vue'; import initUserInternalRegexPlaceholder from '../account_and_limits'; +import IntegrationHelpText from '~/vue_shared/components/integrations_help_text.vue'; -document.addEventListener('DOMContentLoaded', initUserInternalRegexPlaceholder()); +document.addEventListener('DOMContentLoaded', () => { + initUserInternalRegexPlaceholder(); + + const gitpodSettingEl = document.querySelector('#js-gitpod-settings-help-text'); + if (!gitpodSettingEl) { + return; + } + + // eslint-disable-next-line no-new + new Vue({ + el: gitpodSettingEl, + name: 'GitpodSettings', + components: { + IntegrationHelpText, + }, + }); +}); diff --git a/app/assets/javascripts/pages/profiles/preferences/show/index.js b/app/assets/javascripts/pages/profiles/preferences/show/index.js new file mode 100644 index 00000000000..d489ed80f46 --- /dev/null +++ b/app/assets/javascripts/pages/profiles/preferences/show/index.js @@ -0,0 +1,3 @@ +import initProfilePreferences from '~/profile/preferences/profile_preferences_bundle'; + +document.addEventListener('DOMContentLoaded', initProfilePreferences); diff --git a/app/assets/javascripts/profile/preferences/components/integration_view.vue b/app/assets/javascripts/profile/preferences/components/integration_view.vue new file mode 100644 index 00000000000..c2952629a5d --- /dev/null +++ b/app/assets/javascripts/profile/preferences/components/integration_view.vue @@ -0,0 +1,81 @@ + + + diff --git a/app/assets/javascripts/profile/preferences/components/profile_preferences.vue b/app/assets/javascripts/profile/preferences/components/profile_preferences.vue new file mode 100644 index 00000000000..8b2006b7c5b --- /dev/null +++ b/app/assets/javascripts/profile/preferences/components/profile_preferences.vue @@ -0,0 +1,56 @@ + + + diff --git a/app/assets/javascripts/profile/preferences/profile_preferences_bundle.js b/app/assets/javascripts/profile/preferences/profile_preferences_bundle.js new file mode 100644 index 00000000000..bcca3140717 --- /dev/null +++ b/app/assets/javascripts/profile/preferences/profile_preferences_bundle.js @@ -0,0 +1,23 @@ +import Vue from 'vue'; +import ProfilePreferences from './components/profile_preferences.vue'; + +export default () => { + const el = document.querySelector('#js-profile-preferences-app'); + const shouldParse = ['integrationViews', 'userFields']; + + const provide = Object.keys(el.dataset).reduce((memo, key) => { + let value = el.dataset[key]; + if (shouldParse.includes(key)) { + value = JSON.parse(value); + } + + return { ...memo, [key]: value }; + }, {}); + + return new Vue({ + el, + name: 'ProfilePreferencesApp', + provide, + render: createElement => createElement(ProfilePreferences), + }); +}; diff --git a/app/assets/javascripts/releases/components/app_edit_new.vue b/app/assets/javascripts/releases/components/app_edit_new.vue index e0705489738..8d1bc44cba0 100644 --- a/app/assets/javascripts/releases/components/app_edit_new.vue +++ b/app/assets/javascripts/releases/components/app_edit_new.vue @@ -29,7 +29,6 @@ export default { 'markdownDocsPath', 'markdownPreviewPath', 'releasesPagePath', - 'updateReleaseApiDocsPath', 'release', 'newMilestonePath', 'manageMilestonesPath', diff --git a/app/assets/javascripts/releases/components/tag_field_existing.vue b/app/assets/javascripts/releases/components/tag_field_existing.vue index b84e713df26..046885fe2f6 100644 --- a/app/assets/javascripts/releases/components/tag_field_existing.vue +++ b/app/assets/javascripts/releases/components/tag_field_existing.vue @@ -1,14 +1,14 @@ + + diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal.vue b/app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal.vue index 558d361615b..82060d2e4ad 100644 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal.vue +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal.vue @@ -2,7 +2,6 @@ import { GlModal, GlFormGroup, GlFormInput, GlTabs, GlTab } from '@gitlab/ui'; import { isSafeURL, joinPaths } from '~/lib/utils/url_utility'; import { __ } from '~/locale'; -import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { IMAGE_TABS } from '../../constants'; import UploadImageTab from './upload_image_tab.vue'; @@ -15,7 +14,6 @@ export default { GlTabs, GlTab, }, - mixins: [glFeatureFlagMixin()], props: { imageRoot: { type: String, @@ -34,10 +32,10 @@ export default { }, modalTitle: __('Image details'), okTitle: __('Insert image'), - urlTabTitle: __('By URL'), + urlTabTitle: __('Link to an image'), urlLabel: __('Image URL'), descriptionLabel: __('Description'), - uploadTabTitle: __('Upload file'), + uploadTabTitle: __('Upload an image'), computed: { altText() { return this.description; @@ -54,7 +52,7 @@ export default { this.$refs.modal.show(); }, onOk(event) { - if (this.glFeatures.sseImageUploads && this.tabIndex === IMAGE_TABS.UPLOAD_TAB) { + if (this.tabIndex === IMAGE_TABS.UPLOAD_TAB) { this.submitFile(event); return; } @@ -108,7 +106,7 @@ export default { :ok-title="$options.okTitle" @ok="onOk" > - + @@ -128,17 +126,6 @@ export default { - - - - diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue b/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue index dbd05685b55..9eacf74bba8 100644 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue @@ -114,10 +114,9 @@ export default { if (file) { this.$emit('uploadImage', { file, imageUrl }); - // TODO - ensure that the actual repo URL for the image is used in Markdown mode } - addImage(this.editorInstance, image); + addImage(this.editorInstance, image, file); }, onOpenInsertVideoModal() { this.$refs.insertVideoModal.show(); diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/editor_service.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/editor_service.js index 8b3fbcabcfa..463e64b4936 100644 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/editor_service.js +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/editor_service.js @@ -34,6 +34,20 @@ const buildVideoIframe = src => { return wrapper; }; +const buildImg = (alt, originalSrc, file) => { + const img = document.createElement('img'); + const src = file ? URL.createObjectURL(file) : originalSrc; + const attributes = { alt, src }; + + if (file) { + img.dataset.originalSrc = originalSrc; + } + + Object.assign(img, attributes); + + return img; +}; + export const generateToolbarItem = config => { const { icon, classes, event, command, tooltip, isDivider } = config; @@ -59,7 +73,14 @@ export const addCustomEventListener = (editorApi, event, handler) => { export const removeCustomEventListener = (editorApi, event, handler) => editorApi.eventManager.removeEventHandler(event, handler); -export const addImage = ({ editor }, image) => editor.exec('AddImage', image); +export const addImage = ({ editor }, { altText, imageUrl }, file) => { + if (editor.isWysiwygMode()) { + const img = buildImg(altText, imageUrl, file); + editor.getSquire().insertElement(img); + } else { + editor.insertText(`![${altText}](${imageUrl})`); + } +}; export const insertVideo = ({ editor }, url) => { const videoIframe = buildVideoIframe(url); diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb index 7a5b470f366..bfa7a30bc65 100644 --- a/app/controllers/concerns/notes_actions.rb +++ b/app/controllers/concerns/notes_actions.rb @@ -31,6 +31,10 @@ module NotesActions # We know there's more data, so tell the frontend to poll again after 1ms set_polling_interval_header(interval: 1) if meta[:more] + # Only present an ETag for the empty response to ensure pagination works + # as expected + ::Gitlab::EtagCaching::Middleware.skip!(response) if notes.present? + render json: meta.merge(notes: notes) end @@ -115,7 +119,7 @@ module NotesActions end def gather_some_notes - paginator = Gitlab::UpdatedNotesPaginator.new( + paginator = ::Gitlab::UpdatedNotesPaginator.new( notes_finder.execute.inc_relations_for_view, last_fetched_at: last_fetched_at ) diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb index 17c566f853b..15e16687c02 100644 --- a/app/controllers/projects/merge_requests/diffs_controller.rb +++ b/app/controllers/projects/merge_requests/diffs_controller.rb @@ -34,6 +34,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic environment: environment, merge_request: @merge_request, diff_view: diff_view, + merge_ref_head_diff: render_merge_ref_head_diff?, pagination_data: diffs.pagination_data } @@ -67,7 +68,10 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic render: ->(partial, locals) { view_to_html_string(partial, locals) } } - options = additional_attributes.merge(diff_view: Feature.enabled?(:unified_diff_lines, @merge_request.project, default_enabled: true) ? "inline" : diff_view) + options = additional_attributes.merge( + diff_view: Feature.enabled?(:unified_diff_lines, @merge_request.project, default_enabled: true) ? "inline" : diff_view, + merge_ref_head_diff: render_merge_ref_head_diff? + ) if @merge_request.project.context_commits_enabled? options[:context_commits] = @merge_request.recent_context_commits @@ -116,7 +120,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic end end - if Gitlab::Utils.to_boolean(params[:diff_head]) && @merge_request.diffable_merge_ref? + if render_merge_ref_head_diff? return CompareService.new(@project, @merge_request.merge_ref_head.sha) .execute(@project, @merge_request.target_branch) end @@ -158,6 +162,10 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic @notes = prepare_notes_for_rendering(@grouped_diff_discussions.values.flatten.flat_map(&:notes), @merge_request) end + def render_merge_ref_head_diff? + Gitlab::Utils.to_boolean(params[:diff_head]) && @merge_request.diffable_merge_ref? + end + def note_positions @note_positions ||= Gitlab::Diff::PositionCollection.new(renderable_notes.map(&:position)) end diff --git a/app/controllers/projects/static_site_editor_controller.rb b/app/controllers/projects/static_site_editor_controller.rb index 9fb5976e11a..5c3d9b60877 100644 --- a/app/controllers/projects/static_site_editor_controller.rb +++ b/app/controllers/projects/static_site_editor_controller.rb @@ -16,9 +16,6 @@ class Projects::StaticSiteEditorController < Projects::ApplicationController prepend_before_action :authenticate_user!, only: [:show] before_action :assign_ref_and_path, only: [:show] before_action :authorize_edit_tree!, only: [:show] - before_action do - push_frontend_feature_flag(:sse_image_uploads) - end feature_category :static_site_editor diff --git a/app/graphql/resolvers/concerns/caching_array_resolver.rb b/app/graphql/resolvers/concerns/caching_array_resolver.rb new file mode 100644 index 00000000000..4f2c8b98928 --- /dev/null +++ b/app/graphql/resolvers/concerns/caching_array_resolver.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +# Concern that will eliminate N+1 queries for size-constrained +# collections of items. +# +# **note**: The resolver will never load more items than +# `@field.max_page_size` if defined, falling back to +# `context.schema.default_max_page_size`. +# +# provided that: +# +# - the query can be uniquely determined by the object and the arguments +# - the model class includes FromUnion +# - the model class defines a scalar primary key +# +# This comes at the cost of returning arrays, not relations, so we don't get +# any keyset pagination goodness. Consequently, this is only suitable for small-ish +# result sets, as the full result set will be loaded into memory. +# +# To enforce this, the resolver limits the size of result sets to +# `@field.max_page_size || context.schema.default_max_page_size`. +# +# **important**: If the cardinality of your collection is likely to be greater than 100, +# then you will want to pass `max_page_size:` as part of the field definition +# or (ideally) as part of the resolver `field_options`. +# +# How to implement: +# -------------------- +# +# Each including class operates on two generic parameters, A and R: +# - A is any Object that can be used as a Hash key. Instances of A +# are returned by `query_input` and then passed to `query_for`. +# - R is any subclass of ApplicationRecord that includes FromUnion. +# R must have a single scalar primary_key +# +# Classes must implement: +# - #model_class -> Class[R]. (Must respond to :primary_key, and :from_union) +# - #query_input(**kwargs) -> A (Must be hashable) +# - #query_for(A) -> ActiveRecord::Relation[R] +# +# Note the relationship between query_input and query_for, one of which +# consumes the input of the other +# (i.e. `resolve(**args).sync == query_for(query_input(**args)).to_a`). +# +# Classes may implement: +# - #item_found(A, R) (return value is ignored) +# - max_union_size Integer (the maximum number of queries to run in any one union) +module CachingArrayResolver + MAX_UNION_SIZE = 50 + + def resolve(**args) + key = query_input(**args) + + BatchLoader::GraphQL.for(key).batch(**batch) do |keys, loader| + if keys.size == 1 + # We can avoid the union entirely. + k = keys.first + limit(query_for(k)).each { |item| found(loader, k, item) } + else + queries = keys.map { |key| query_for(key) } + + queries.in_groups_of(max_union_size, false).each do |group| + by_id = model_class + .from_union(tag(group), remove_duplicates: false) + .group_by { |r| r[primary_key] } + + by_id.values.each do |item_group| + item = item_group.first + item_group.map(&:union_member_idx).each do |i| + found(loader, keys[i], item) + end + end + end + end + end + end + + # Override this to intercept the items once they are found + def item_found(query_input, item) + end + + def max_union_size + MAX_UNION_SIZE + end + + private + + def primary_key + @primary_key ||= (model_class.primary_key || raise("No primary key for #{model_class}")) + end + + def batch + { key: self.class, default_value: [] } + end + + def found(loader, key, value) + loader.call(key) do |vs| + item_found(key, value) + vs << value + end + end + + # Tag each row returned from each query with a the index of which query in + # the union it comes from. This lets us map the results back to the cache key. + def tag(queries) + queries.each_with_index.map do |q, i| + limit(q.select(all_fields, member_idx(i))) + end + end + + def limit(query) + query.limit(query_limit) # rubocop: disable CodeReuse/ActiveRecord + end + + def all_fields + model_class.arel_table[Arel.star] + end + + # rubocop: disable Graphql/Descriptions (false positive!) + def query_limit + field&.max_page_size.presence || context.schema.default_max_page_size + end + # rubocop: enable Graphql/Descriptions + + def member_idx(idx) + ::Arel::Nodes::SqlLiteral.new(idx.to_s).as('union_member_idx') + end +end diff --git a/app/graphql/resolvers/concerns/resolves_snippets.rb b/app/graphql/resolvers/concerns/resolves_snippets.rb index ea1a22f80a6..790ff4f774f 100644 --- a/app/graphql/resolvers/concerns/resolves_snippets.rb +++ b/app/graphql/resolvers/concerns/resolves_snippets.rb @@ -4,7 +4,7 @@ module ResolvesSnippets extend ActiveSupport::Concern included do - type Types::SnippetType, null: false + type Types::SnippetType.connection_type, null: false argument :ids, [::Types::GlobalIDType[::Snippet]], required: false, diff --git a/app/graphql/resolvers/projects/snippets_resolver.rb b/app/graphql/resolvers/projects/snippets_resolver.rb index 22895a24054..796884b5170 100644 --- a/app/graphql/resolvers/projects/snippets_resolver.rb +++ b/app/graphql/resolvers/projects/snippets_resolver.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true +# rubocop:disable Graphql/ResolverType module Resolvers module Projects diff --git a/app/graphql/resolvers/snippets_resolver.rb b/app/graphql/resolvers/snippets_resolver.rb index 652fbbe8593..993e4d0f19c 100644 --- a/app/graphql/resolvers/snippets_resolver.rb +++ b/app/graphql/resolvers/snippets_resolver.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true +# rubocop:disable Graphql/ResolverType module Resolvers class SnippetsResolver < BaseResolver diff --git a/app/graphql/resolvers/users/snippets_resolver.rb b/app/graphql/resolvers/users/snippets_resolver.rb index d757640b5ff..f322cb0de1c 100644 --- a/app/graphql/resolvers/users/snippets_resolver.rb +++ b/app/graphql/resolvers/users/snippets_resolver.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true +# rubocop:disable Graphql/ResolverType module Resolvers module Users diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index a9361c55958..69a2efebb1f 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -252,4 +252,18 @@ module DiffHelper "...#{path[-(max - 3)..-1]}" end + + def code_navigation_path(diffs) + Gitlab::CodeNavigationPath.new(merge_request.project, diffs.diff_refs&.head_sha) + end + + def conflicts + return unless options[:merge_ref_head_diff] + + conflicts_service = MergeRequests::Conflicts::ListService.new(merge_request) # rubocop:disable CodeReuse/ServiceClass + + return unless conflicts_service.can_be_resolved_in_ui? + + conflicts_service.conflicts.files.index_by(&:our_path) + end end diff --git a/app/helpers/gitpod_helper.rb b/app/helpers/gitpod_helper.rb index 7edf7dc218d..875a44c51bb 100644 --- a/app/helpers/gitpod_helper.rb +++ b/app/helpers/gitpod_helper.rb @@ -2,9 +2,6 @@ module GitpodHelper def gitpod_enable_description - link_start = ''.html_safe - link_end = "#{sprite_icon('external-link', size: 12, css_class: 'ml-1 vertical-align-center')}".html_safe - - s_('Enable %{link_start}Gitpod%{link_end} integration to launch a development environment in your browser directly from GitLab.').html_safe % { link_start: link_start, link_end: link_end } + s_('Enable %{linkStart}Gitpod%{linkEnd} integration to launch a development environment in your browser directly from GitLab.') end end diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index 9bf819febb0..5310aef5bad 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -82,8 +82,8 @@ module PreferencesHelper def integration_views [].tap do |views| - views << 'gitpod' if Gitlab::Gitpod.feature_and_settings_enabled? - views << 'sourcegraph' if Gitlab::Sourcegraph.feature_available? && Gitlab::CurrentSettings.sourcegraph_enabled + views << { name: 'gitpod', message: gitpod_enable_description, message_url: 'https://gitpod.io/', help_link: help_page_path('integration/gitpod.md') } if Gitlab::Gitpod.feature_and_settings_enabled? + views << { name: 'sourcegraph', message: sourcegraph_url_message, message_url: Gitlab::CurrentSettings.sourcegraph_url, help_link: help_page_path('user/profile/preferences.md', anchor: 'sourcegraph') } if Gitlab::Sourcegraph.feature_available? && Gitlab::CurrentSettings.sourcegraph_enabled end end diff --git a/app/helpers/releases_helper.rb b/app/helpers/releases_helper.rb index 72441226ef7..d9851564585 100644 --- a/app/helpers/releases_helper.rb +++ b/app/helpers/releases_helper.rb @@ -65,7 +65,6 @@ module ReleasesHelper project_path: @project.full_path, markdown_preview_path: preview_markdown_path(@project), markdown_docs_path: help_page_path('user/markdown'), - update_release_api_docs_path: help_page_path('api/releases/index.md', anchor: 'update-a-release'), release_assets_docs_path: help_page(anchor: 'release-assets'), manage_milestones_path: project_milestones_path(@project), new_milestone_path: new_project_milestone_path(@project) diff --git a/app/helpers/sourcegraph_helper.rb b/app/helpers/sourcegraph_helper.rb index cc5a5c77e9a..25d7b209b45 100644 --- a/app/helpers/sourcegraph_helper.rb +++ b/app/helpers/sourcegraph_helper.rb @@ -2,26 +2,22 @@ module SourcegraphHelper def sourcegraph_url_message - link_start = ''.html_safe % { url: Gitlab::CurrentSettings.sourcegraph_url } - link_end = "#{sprite_icon('external-link', size: 12, css_class: 'ml-1 vertical-align-center')}".html_safe - message = if Gitlab::CurrentSettings.sourcegraph_url_is_com? - s_('SourcegraphPreferences|Uses %{link_start}Sourcegraph.com%{link_end}.').html_safe + s_('SourcegraphPreferences|Uses %{linkStart}Sourcegraph.com%{linkEnd}.').html_safe else - s_('SourcegraphPreferences|Uses a custom %{link_start}Sourcegraph instance%{link_end}.').html_safe + s_('SourcegraphPreferences|Uses a custom %{linkStart}Sourcegraph instance%{linkEnd}.').html_safe end - message % { link_start: link_start, link_end: link_end } - end + experimental_message = + if Gitlab::Sourcegraph.feature_conditional? + s_("SourcegraphPreferences|This feature is experimental and currently limited to certain projects.") + elsif Gitlab::CurrentSettings.sourcegraph_public_only + s_("SourcegraphPreferences|This feature is experimental and limited to public projects.") + else + s_("SourcegraphPreferences|This feature is experimental.") + end - def sourcegraph_experimental_message - if Gitlab::Sourcegraph.feature_conditional? - s_("SourcegraphPreferences|This feature is experimental and currently limited to certain projects.") - elsif Gitlab::CurrentSettings.sourcegraph_public_only - s_("SourcegraphPreferences|This feature is experimental and limited to public projects.") - else - s_("SourcegraphPreferences|This feature is experimental.") - end + "#{message} #{experimental_message}" end end diff --git a/app/serializers/diff_file_entity.rb b/app/serializers/diff_file_entity.rb index e3fefbb46b6..9865af1e116 100644 --- a/app/serializers/diff_file_entity.rb +++ b/app/serializers/diff_file_entity.rb @@ -3,6 +3,7 @@ class DiffFileEntity < DiffFileBaseEntity include CommitsHelper include IconsHelper + include Gitlab::Utils::StrongMemoize expose :added_lines expose :removed_lines @@ -54,11 +55,16 @@ class DiffFileEntity < DiffFileBaseEntity # Used for inline diffs expose :highlighted_diff_lines, using: DiffLineEntity, if: -> (diff_file, options) { inline_diff_view?(options, diff_file) && diff_file.text? } do |diff_file| - diff_file.diff_lines_for_serializer + file = conflict_file(options, diff_file) || diff_file + file.diff_lines_for_serializer end expose :is_fully_expanded do |diff_file| - diff_file.fully_expanded? + if conflict_file(options, diff_file) + false + else + diff_file.fully_expanded? + end end # Used for parallel diffs @@ -79,4 +85,10 @@ class DiffFileEntity < DiffFileBaseEntity # If nothing is present, inline will be the default. options.fetch(:diff_view, :inline).to_sym == :inline end + + def conflict_file(options, diff_file) + strong_memoize(:conflict_file) do + options[:conflicts] && options[:conflicts][diff_file.new_path] + end + end end diff --git a/app/serializers/diffs_entity.rb b/app/serializers/diffs_entity.rb index 0b4f21c55f4..f573bbe8385 100644 --- a/app/serializers/diffs_entity.rb +++ b/app/serializers/diffs_entity.rb @@ -71,7 +71,7 @@ class DiffsEntity < Grape::Entity submodule_links = Gitlab::SubmoduleLinks.new(merge_request.project.repository) DiffFileEntity.represent(diffs.diff_files, - options.merge(submodule_links: submodule_links, code_navigation_path: code_navigation_path(diffs))) + options.merge(submodule_links: submodule_links, code_navigation_path: code_navigation_path(diffs), conflicts: conflicts)) end expose :merge_request_diffs, using: MergeRequestDiffEntity, if: -> (_, options) { options[:merge_request_diffs]&.any? } do |diffs| @@ -88,10 +88,6 @@ class DiffsEntity < Grape::Entity private - def code_navigation_path(diffs) - Gitlab::CodeNavigationPath.new(merge_request.project, diffs.diff_refs&.head_sha) - end - def commit_ids @commit_ids ||= merge_request.recent_commits.map(&:id) end diff --git a/app/serializers/paginated_diff_entity.rb b/app/serializers/paginated_diff_entity.rb index 1d72f970c49..fe59686278c 100644 --- a/app/serializers/paginated_diff_entity.rb +++ b/app/serializers/paginated_diff_entity.rb @@ -7,6 +7,7 @@ # class PaginatedDiffEntity < Grape::Entity include RequestAwareEntity + include DiffHelper expose :diff_files do |diffs, options| submodule_links = Gitlab::SubmoduleLinks.new(merge_request.project.repository) @@ -15,7 +16,8 @@ class PaginatedDiffEntity < Grape::Entity diffs.diff_files, options.merge( submodule_links: submodule_links, - code_navigation_path: code_navigation_path(diffs) + code_navigation_path: code_navigation_path(diffs), + conflicts: conflicts ) ) end @@ -41,10 +43,6 @@ class PaginatedDiffEntity < Grape::Entity private - def code_navigation_path(diffs) - Gitlab::CodeNavigationPath.new(merge_request.project, diffs.diff_refs&.head_sha) - end - %i[current_page next_page total_pages].each do |method| define_method method do pagination_data[method] diff --git a/app/services/users/approve_service.rb b/app/services/users/approve_service.rb index 3b48c913c77..27668e9430e 100644 --- a/app/services/users/approve_service.rb +++ b/app/services/users/approve_service.rb @@ -17,6 +17,7 @@ module Users user.accept_pending_invitations! if user.active_for_authentication? DeviseMailer.user_admin_approval(user).deliver_later + after_approve_hook(user) success else error(user.errors.full_messages.uniq.join('. ')) @@ -27,6 +28,10 @@ module Users attr_reader :current_user + def after_approve_hook(user) + # overridden by EE module + end + def allowed? can?(current_user, :approve_user) end @@ -36,3 +41,5 @@ module Users end end end + +Users::ApproveService.prepend_if_ee('EE::Users::ApproveService') diff --git a/app/views/admin/application_settings/_gitpod.html.haml b/app/views/admin/application_settings/_gitpod.html.haml index f0a1fd5e763..3a6451f4283 100644 --- a/app/views/admin/application_settings/_gitpod.html.haml +++ b/app/views/admin/application_settings/_gitpod.html.haml @@ -8,7 +8,7 @@ %button.btn.btn-default.js-settings-toggle{ type: 'button' } = expanded ? _('Collapse') : _('Expand') %p - = gitpod_enable_description + %integration-help-text{ "id" => "js-gitpod-settings-help-text", "message" => gitpod_enable_description, "message-url" => "https://gitpod.io/" } = link_to sprite_icon('question-o'), help_page_path('integration/gitpod.md'), target: '_blank', class: 'has-tooltip', title: _('More information') diff --git a/app/views/devise/registrations/new.html.haml b/app/views/devise/registrations/new.html.haml index 6782e1576b5..bb6be2af5d5 100644 --- a/app/views/devise/registrations/new.html.haml +++ b/app/views/devise/registrations/new.html.haml @@ -2,5 +2,9 @@ - add_page_specific_style 'page_bundles/signup' .signup-page - = render 'devise/shared/signup_box', url: registration_path(resource_name), button_text: _('Register'), show_omniauth_providers: omniauth_enabled? && button_based_providers_enabled? + = render 'devise/shared/signup_box', + url: registration_path(resource_name), + button_text: _('Register'), + show_omniauth_providers: omniauth_enabled? && button_based_providers_enabled?, + suggestion_path: nil = render 'devise/shared/sign_in_link' diff --git a/app/views/profiles/preferences/_gitpod.html.haml b/app/views/profiles/preferences/_gitpod.html.haml deleted file mode 100644 index 589c3a27c18..00000000000 --- a/app/views/profiles/preferences/_gitpod.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -%label.label-bold#gitpod - = s_('Gitpod') -= link_to sprite_icon('question-o'), help_page_path('integration/gitpod.md'), target: '_blank', class: 'has-tooltip', title: _('More information') -.form-group.form-check - = f.check_box :gitpod_enabled, class: 'form-check-input' - = f.label :gitpod_enabled, class: 'form-check-label' do - = s_('Gitpod|Enable Gitpod integration').html_safe - .form-text.text-muted - = gitpod_enable_description diff --git a/app/views/profiles/preferences/_integrations.html.haml b/app/views/profiles/preferences/_integrations.html.haml deleted file mode 100644 index 037fe5df263..00000000000 --- a/app/views/profiles/preferences/_integrations.html.haml +++ /dev/null @@ -1,18 +0,0 @@ -- views = integration_views -- return unless views.any? - -.col-sm-12 - %hr - -.col-lg-4.profile-settings-sidebar#integrations - %h4.gl-mt-0 - = s_('Preferences|Integrations') - %p - = s_('Preferences|Customize integrations with third party services.') - = succeed '.' do - = link_to _('Learn more'), help_page_path('user/profile/preferences.md', anchor: 'integrations'), target: '_blank' - -.col-lg-8 - - views.each do |view| - = render view, f: f - diff --git a/app/views/profiles/preferences/_sourcegraph.html.haml b/app/views/profiles/preferences/_sourcegraph.html.haml deleted file mode 100644 index fdd0be22664..00000000000 --- a/app/views/profiles/preferences/_sourcegraph.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -%label.label-bold - = s_('Preferences|Sourcegraph') -= link_to sprite_icon('question-o'), help_page_path('user/profile/preferences.md', anchor: 'sourcegraph'), target: '_blank', class: 'has-tooltip', title: _('More information') -.form-group.form-check - = f.check_box :sourcegraph_enabled, class: 'form-check-input' - = f.label :sourcegraph_enabled, class: 'form-check-label' do - = s_('Preferences|Enable integrated code intelligence on code views').html_safe - .form-text.text-muted - = sourcegraph_url_message - = sourcegraph_experimental_message diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index b8d7e1af005..ca5972f1b46 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -1,146 +1,151 @@ - page_title _('Preferences') - @content_class = "limit-container-width" unless fluid_layout +- user_fields = { gitpod_enabled: @user.gitpod_enabled, sourcegraph_enabled: @user.sourcegraph_enabled } +- user_theme_id = Gitlab::Themes.for_user(@user).id +- data_attributes = { integration_views: integration_views.to_json, user_fields: user_fields.to_json } - Gitlab::Themes.each do |theme| = stylesheet_link_tag "themes/#{theme.css_filename}" if theme.css_filename -= form_for @user, url: profile_preferences_path, remote: true, method: :put, html: { class: 'row gl-mt-3 js-preferences-form' } do |f| - .col-lg-4.application-theme#navigation-theme - %h4.gl-mt-0 - = s_('Preferences|Navigation theme') - %p - = s_('Preferences|Customize the appearance of the application header and navigation sidebar.') - .col-lg-8.application-theme - .row - - Gitlab::Themes.each do |theme| - %label.col-6.col-sm-4.col-md-3.gl-mb-5.gl-text-center - .preview{ class: theme.css_class } - = f.radio_button :theme_id, theme.id, checked: Gitlab::Themes.for_user(@user).id == theme.id - = theme.name += form_for @user, url: profile_preferences_path, remote: true, method: :put do |f| + .row.gl-mt-3.js-preferences-form + .col-lg-4.application-theme#navigation-theme + %h4.gl-mt-0 + = s_('Preferences|Navigation theme') + %p + = s_('Preferences|Customize the appearance of the application header and navigation sidebar.') + .col-lg-8.application-theme + .row + - Gitlab::Themes.each do |theme| + %label.col-6.col-sm-4.col-md-3.gl-mb-5.gl-text-center + .preview{ class: theme.css_class } + = f.radio_button :theme_id, theme.id, checked: user_theme_id == theme.id + = theme.name - .col-sm-12 - %hr - - .col-lg-4.profile-settings-sidebar#syntax-highlighting-theme - %h4.gl-mt-0 - = s_('Preferences|Syntax highlighting theme') - %p - = s_('Preferences|This setting allows you to customize the appearance of the syntax.') - = succeed '.' do - = link_to _('Learn more'), help_page_path('user/profile/preferences', anchor: 'syntax-highlighting-theme'), target: '_blank' - .col-lg-8.syntax-theme - - Gitlab::ColorSchemes.each do |scheme| - = label_tag do - .preview= image_tag "#{scheme.css_class}-scheme-preview.png" - = f.radio_button :color_scheme_id, scheme.id - = scheme.name - - .col-sm-12 - %hr - - .col-lg-4.profile-settings-sidebar#behavior - %h4.gl-mt-0 - = s_('Preferences|Behavior') - %p - = s_('Preferences|This setting allows you to customize the behavior of the system layout and default views.') - = succeed '.' do - = link_to _('Learn more'), help_page_path('user/profile/preferences', anchor: 'behavior'), target: '_blank' - .col-lg-8 - .form-group - = f.label :layout, class: 'label-bold' do - = s_('Preferences|Layout width') - = f.select :layout, layout_choices, {}, class: 'select2' - .form-text.text-muted - = s_('Preferences|Choose between fixed (max. 1280px) and fluid (%{percentage}) application layout.').html_safe % { percentage: '100%' } - .form-group - = f.label :dashboard, class: 'label-bold' do - = s_('Preferences|Homepage content') - = f.select :dashboard, dashboard_choices, {}, class: 'select2' - .form-text.text-muted - = s_('Preferences|Choose what content you want to see on your homepage.') - - = render_if_exists 'profiles/preferences/group_overview_selector', f: f # EE-specific - - .form-group - = f.label :project_view, class: 'label-bold' do - = s_('Preferences|Project overview content') - = f.select :project_view, project_view_choices, {}, class: 'select2' - .form-text.text-muted - = s_('Preferences|Choose what content you want to see on a project’s overview page.') - .form-group.form-check - = f.check_box :render_whitespace_in_code, class: 'form-check-input' - = f.label :render_whitespace_in_code, class: 'form-check-label' do - = s_('Preferences|Render whitespace characters in the Web IDE') - .form-group.form-check - = f.check_box :show_whitespace_in_diffs, class: 'form-check-input' - = f.label :show_whitespace_in_diffs, class: 'form-check-label' do - = s_('Preferences|Show whitespace changes in diffs') - - if Feature.enabled?(:view_diffs_file_by_file, default_enabled: true) - .form-group.form-check - = f.check_box :view_diffs_file_by_file, class: 'form-check-input' - = f.label :view_diffs_file_by_file, class: 'form-check-label' do - = s_("Preferences|Show one file at a time on merge request's Changes tab") - .form-text.text-muted - = s_("Preferences|Instead of all the files changed, show only one file at a time. To switch between files, use the file browser.") - .form-group - = f.label :tab_width, s_('Preferences|Tab width'), class: 'label-bold' - = f.number_field :tab_width, - class: 'form-control', - min: Gitlab::TabWidth::MIN, - max: Gitlab::TabWidth::MAX, - required: true - .form-text.text-muted - = s_('Preferences|Must be a number between %{min} and %{max}') % { min: Gitlab::TabWidth::MIN, max: Gitlab::TabWidth::MAX } - - .col-sm-12 - %hr - - .col-lg-4.profile-settings-sidebar#localization - %h4.gl-mt-0 - = _('Localization') - %p - = _('Customize language and region related settings.') - = succeed '.' do - = link_to _('Learn more'), help_page_path('user/profile/preferences', anchor: 'localization'), target: '_blank' - .col-lg-8 - .form-group - = f.label :preferred_language, class: 'label-bold' do - = _('Language') - = f.select :preferred_language, language_choices, {}, class: 'select2' - .form-text.text-muted - = s_('Preferences|This feature is experimental and translations are not complete yet') - .form-group - = f.label :first_day_of_week, class: 'label-bold' do - = _('First day of the week') - = f.select :first_day_of_week, first_day_of_week_choices_with_default, {}, class: 'select2' - - if Feature.enabled?(:user_time_settings) .col-sm-12 %hr - .col-lg-4.profile-settings-sidebar - %h4.gl-mt-0= s_('Preferences|Time preferences') - %p= s_('Preferences|These settings will update how dates and times are displayed for you.') + + .col-lg-4.profile-settings-sidebar#syntax-highlighting-theme + %h4.gl-mt-0 + = s_('Preferences|Syntax highlighting theme') + %p + = s_('Preferences|This setting allows you to customize the appearance of the syntax.') + = succeed '.' do + = link_to _('Learn more'), help_page_path('user/profile/preferences', anchor: 'syntax-highlighting-theme'), target: '_blank' + .col-lg-8.syntax-theme + - Gitlab::ColorSchemes.each do |scheme| + = label_tag do + .preview= image_tag "#{scheme.css_class}-scheme-preview.png" + = f.radio_button :color_scheme_id, scheme.id + = scheme.name + + .col-sm-12 + %hr + + .col-lg-4.profile-settings-sidebar#behavior + %h4.gl-mt-0 + = s_('Preferences|Behavior') + %p + = s_('Preferences|This setting allows you to customize the behavior of the system layout and default views.') + = succeed '.' do + = link_to _('Learn more'), help_page_path('user/profile/preferences', anchor: 'behavior'), target: '_blank' .col-lg-8 .form-group - %h5= s_('Preferences|Time format') - .checkbox-icon-inline-wrapper - - time_format_label = capture do - = s_('Preferences|Display time in 24-hour format') - = f.check_box :time_format_in_24h - = f.label :time_format_in_24h do - = time_format_label - %h5= s_('Preferences|Time display') - .checkbox-icon-inline-wrapper - - time_display_label = capture do - = s_('Preferences|Use relative times') - = f.check_box :time_display_relative - = f.label :time_display_relative do - = time_display_label + = f.label :layout, class: 'label-bold' do + = s_('Preferences|Layout width') + = f.select :layout, layout_choices, {}, class: 'select2' + .form-text.text-muted + = s_('Preferences|Choose between fixed (max. 1280px) and fluid (%{percentage}) application layout.').html_safe % { percentage: '100%' } + .form-group + = f.label :dashboard, class: 'label-bold' do + = s_('Preferences|Homepage content') + = f.select :dashboard, dashboard_choices, {}, class: 'select2' + .form-text.text-muted + = s_('Preferences|Choose what content you want to see on your homepage.') + + = render_if_exists 'profiles/preferences/group_overview_selector', f: f # EE-specific + + .form-group + = f.label :project_view, class: 'label-bold' do + = s_('Preferences|Project overview content') + = f.select :project_view, project_view_choices, {}, class: 'select2' + .form-text.text-muted + = s_('Preferences|Choose what content you want to see on a project’s overview page.') + .form-group.form-check + = f.check_box :render_whitespace_in_code, class: 'form-check-input' + = f.label :render_whitespace_in_code, class: 'form-check-label' do + = s_('Preferences|Render whitespace characters in the Web IDE') + .form-group.form-check + = f.check_box :show_whitespace_in_diffs, class: 'form-check-input' + = f.label :show_whitespace_in_diffs, class: 'form-check-label' do + = s_('Preferences|Show whitespace changes in diffs') + - if Feature.enabled?(:view_diffs_file_by_file, default_enabled: true) + .form-group.form-check + = f.check_box :view_diffs_file_by_file, class: 'form-check-input' + = f.label :view_diffs_file_by_file, class: 'form-check-label' do + = s_("Preferences|Show one file at a time on merge request's Changes tab") .form-text.text-muted - = s_('Preferences|For example: 30 mins ago.') + = s_("Preferences|Instead of all the files changed, show only one file at a time. To switch between files, use the file browser.") + .form-group + = f.label :tab_width, s_('Preferences|Tab width'), class: 'label-bold' + = f.number_field :tab_width, + class: 'form-control', + min: Gitlab::TabWidth::MIN, + max: Gitlab::TabWidth::MAX, + required: true + .form-text.text-muted + = s_('Preferences|Must be a number between %{min} and %{max}') % { min: Gitlab::TabWidth::MIN, max: Gitlab::TabWidth::MAX } - = render 'integrations', f: f + .col-sm-12 + %hr - .col-lg-4.profile-settings-sidebar - .col-lg-8 - .form-group - = f.submit _('Save changes'), class: 'gl-button btn btn-success' + .col-lg-4.profile-settings-sidebar#localization + %h4.gl-mt-0 + = _('Localization') + %p + = _('Customize language and region related settings.') + = succeed '.' do + = link_to _('Learn more'), help_page_path('user/profile/preferences', anchor: 'localization'), target: '_blank' + .col-lg-8 + .form-group + = f.label :preferred_language, class: 'label-bold' do + = _('Language') + = f.select :preferred_language, language_choices, {}, class: 'select2' + .form-text.text-muted + = s_('Preferences|This feature is experimental and translations are not complete yet') + .form-group + = f.label :first_day_of_week, class: 'label-bold' do + = _('First day of the week') + = f.select :first_day_of_week, first_day_of_week_choices_with_default, {}, class: 'select2' + - if Feature.enabled?(:user_time_settings) + .col-sm-12 + %hr + .col-lg-4.profile-settings-sidebar + %h4.gl-mt-0= s_('Preferences|Time preferences') + %p= s_('Preferences|These settings will update how dates and times are displayed for you.') + .col-lg-8 + .form-group + %h5= s_('Preferences|Time format') + .checkbox-icon-inline-wrapper + - time_format_label = capture do + = s_('Preferences|Display time in 24-hour format') + = f.check_box :time_format_in_24h + = f.label :time_format_in_24h do + = time_format_label + %h5= s_('Preferences|Time display') + .checkbox-icon-inline-wrapper + - time_display_label = capture do + = s_('Preferences|Use relative times') + = f.check_box :time_display_relative + = f.label :time_display_relative do + = time_display_label + .form-text.text-muted + = s_('Preferences|For example: 30 mins ago.') + + #js-profile-preferences-app{ data: data_attributes, user_fields: user_fields.to_json } + + .row.gl-mt-3.js-preferences-form + .col-lg-4.profile-settings-sidebar + .col-lg-8 + .form-group + = f.submit _('Save changes'), class: 'gl-button btn btn-success' diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb index 0b224b88e4d..9fe7dd31e68 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -20,7 +20,7 @@ class PostReceive # rubocop:disable Scalability/IdempotentWorker changes = Base64.decode64(changes) unless changes.include?(' ') # Use Sidekiq.logger so arguments can be correlated with execution # time and thread ID's. - Sidekiq.logger.info "changes: #{changes.inspect}" if ENV['SIDEKIQ_LOG_ARGUMENTS'] + Sidekiq.logger.info "changes: #{changes.inspect}" if SidekiqLogArguments.enabled? post_received = Gitlab::GitPostReceive.new(container, identifier, changes, push_options) if repo_type.wiki? diff --git a/changelogs/unreleased/209784-notes-etag-only-for-empty-response.yml b/changelogs/unreleased/209784-notes-etag-only-for-empty-response.yml new file mode 100644 index 00000000000..19e3948a592 --- /dev/null +++ b/changelogs/unreleased/209784-notes-etag-only-for-empty-response.yml @@ -0,0 +1,5 @@ +--- +title: Only set an ETag for the notes endpoint after all notes have been sent +merge_request: 46810 +author: +type: performance diff --git a/changelogs/unreleased/218529-display-uploaded-images.yml b/changelogs/unreleased/218529-display-uploaded-images.yml new file mode 100644 index 00000000000..15c39519b4c --- /dev/null +++ b/changelogs/unreleased/218529-display-uploaded-images.yml @@ -0,0 +1,5 @@ +--- +title: Enable the ability to upload images via the SSE +merge_request: 36299 +author: +type: added diff --git a/changelogs/unreleased/281697-fj-fix-snippet-resolvers.yml b/changelogs/unreleased/281697-fj-fix-snippet-resolvers.yml new file mode 100644 index 00000000000..50b3a4eb6db --- /dev/null +++ b/changelogs/unreleased/281697-fj-fix-snippet-resolvers.yml @@ -0,0 +1,5 @@ +--- +title: Add type annotation for snippet resolvers +merge_request: 47548 +author: +type: changed diff --git a/changelogs/unreleased/nfriend-update-tag-name-placeholder.yml b/changelogs/unreleased/nfriend-update-tag-name-placeholder.yml new file mode 100644 index 00000000000..c2081edbecf --- /dev/null +++ b/changelogs/unreleased/nfriend-update-tag-name-placeholder.yml @@ -0,0 +1,5 @@ +--- +title: Update the tag name field helper text on the Edit Release page +merge_request: 47234 +author: +type: changed diff --git a/changelogs/unreleased/sh-enable-sidekiq-arg-logging-default.yml b/changelogs/unreleased/sh-enable-sidekiq-arg-logging-default.yml new file mode 100644 index 00000000000..913e890d5ba --- /dev/null +++ b/changelogs/unreleased/sh-enable-sidekiq-arg-logging-default.yml @@ -0,0 +1,5 @@ +--- +title: Enable Sidekiq argument logging by default +merge_request: 44853 +author: +type: changed diff --git a/config/feature_flags/development/display_merge_conflicts_in_diff.yml b/config/feature_flags/development/display_merge_conflicts_in_diff.yml index f23e8ac9969..d460e491480 100644 --- a/config/feature_flags/development/display_merge_conflicts_in_diff.yml +++ b/config/feature_flags/development/display_merge_conflicts_in_diff.yml @@ -1,7 +1,7 @@ --- name: display_merge_conflicts_in_diff introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45008 -rollout_issue_url: +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/277097 milestone: '13.5' type: development group: group::source code diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index 72e2b94fe07..8e3241a2e4c 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -1,4 +1,9 @@ # frozen_string_literal: true +module SidekiqLogArguments + def self.enabled? + Gitlab::Utils.to_boolean(ENV['SIDEKIQ_LOG_ARGUMENTS'], default: true) + end +end def enable_reliable_fetch? return true unless Feature::FlipperFeature.table_exists? @@ -35,7 +40,7 @@ Sidekiq.configure_server do |config| config.server_middleware(&Gitlab::SidekiqMiddleware.server_configurator({ metrics: Settings.monitoring.sidekiq_exporter, - arguments_logger: ENV['SIDEKIQ_LOG_ARGUMENTS'] && !enable_json_logs, + arguments_logger: SidekiqLogArguments.enabled? && !enable_json_logs, memory_killer: enable_sidekiq_memory_killer && use_sidekiq_legacy_memory_killer })) diff --git a/doc/administration/audit_events.md b/doc/administration/audit_events.md index f2a801ff576..ce1fa328b69 100644 --- a/doc/administration/audit_events.md +++ b/doc/administration/audit_events.md @@ -99,6 +99,7 @@ From there, you can see the following actions: - Number of required approvals was updated ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7531) in GitLab 12.9) - Added or removed users and groups from project approval groups ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/213603) in GitLab 13.2) - Project CI/CD variable added, removed, or protected status changed ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/30857) in GitLab 13.4) +- User was approved via Admin Area ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/276250) in GitLab 13.6) Project events can also be accessed via the [Project Audit Events API](../api/audit_events.md#project-audit-events). diff --git a/doc/administration/logs.md b/doc/administration/logs.md index ee7c7247ad3..e5523ba67aa 100644 --- a/doc/administration/logs.md +++ b/doc/administration/logs.md @@ -856,7 +856,9 @@ This file is stored in: - `/var/log/gitlab/gitlab-rails/update_mirror_service_json.log` for Omnibus GitLab installations. - `/home/git/gitlab/log/update_mirror_service_json.log` for installations from source. -This file contains information about any errors that occurred during project mirroring. +This file contains information about LFS errors that occurred during project mirroring. +While we work to move other project mirroring errors into this log, the [general log](#productionlog) +can be used. ```json { diff --git a/doc/administration/troubleshooting/sidekiq.md b/doc/administration/troubleshooting/sidekiq.md index a9a0e6ea2aa..d415aa0d980 100644 --- a/doc/administration/troubleshooting/sidekiq.md +++ b/doc/administration/troubleshooting/sidekiq.md @@ -26,19 +26,11 @@ preventing other threads from continuing. ## Log arguments to Sidekiq jobs -If you want to see what arguments are being passed to Sidekiq jobs you can set -the `SIDEKIQ_LOG_ARGUMENTS` [environment variable](https://docs.gitlab.com/omnibus/settings/environment-variables.html) to `1` (true). - -Example: - -```ruby -gitlab_rails['env'] = {"SIDEKIQ_LOG_ARGUMENTS" => "1"} -``` - -This does not log all job arguments. To avoid logging sensitive -information (for instance, password reset tokens), it logs numeric -arguments for all workers, with overrides for some specific workers -where their arguments are not sensitive. +[In GitLab 13.6 and later](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44853) +some arguments passed to Sidekiq jobs are logged by default. +To avoid logging sensitive information (for instance, password reset tokens), +GitLab logs numeric arguments for all workers, with overrides for some specific +workers where their arguments are not sensitive. Example log output: @@ -53,6 +45,17 @@ arguments logs are limited to a maximum size of 10 kilobytes of text; any arguments after this limit will be discarded and replaced with a single argument containing the string `"..."`. +You can set `SIDEKIQ_LOG_ARGUMENTS` [environment variable](https://docs.gitlab.com/omnibus/settings/environment-variables.html) +to `0` (false) to disable argument logging. + +Example: + +```ruby +gitlab_rails['env'] = {"SIDEKIQ_LOG_ARGUMENTS" => "0"} +``` + +In GitLab 13.5 and earlier, set `SIDEKIQ_LOG_ARGUMENTS` to `1` to start logging arguments passed to Sidekiq. + ## Thread dump Send the Sidekiq process ID the `TTIN` signal and it will output thread diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index bc4023aa52d..459cce39ffc 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -1904,6 +1904,16 @@ input BoardIssueInput { """ epicWildcardId: EpicWildcardId + """ + Filter by iteration title + """ + iterationTitle: String + + """ + Filter by iteration ID wildcard + """ + iterationWildcardId: IterationWildcardId + """ Filter by label name """ @@ -11239,11 +11249,6 @@ enum IssueType { Represents an iteration object """ type Iteration implements TimeboxReportInterface { - """ - Daily scope and completed totals for burnup charts - """ - burnupTimeSeries: [BurnupChartDailyTotals!] - """ Timestamp of iteration creation """ @@ -11371,6 +11376,21 @@ enum IterationState { upcoming } +""" +Iteration ID wildcard values +""" +enum IterationWildcardId { + """ + An iteration is assigned + """ + ANY + + """ + No iteration is assigned + """ + NONE +} + """ Represents untyped JSON """ @@ -13314,11 +13334,6 @@ type MetricsDashboardAnnotationEdge { Represents a milestone """ type Milestone implements TimeboxReportInterface { - """ - Daily scope and completed totals for burnup charts - """ - burnupTimeSeries: [BurnupChartDailyTotals!] - """ Timestamp of milestone creation """ @@ -13883,6 +13898,11 @@ input NegatedBoardIssueInput { """ epicId: EpicID + """ + Filter by iteration title + """ + iterationTitle: String + """ Filter by label name """ @@ -20948,11 +20968,6 @@ type TimeboxReport { } interface TimeboxReportInterface { - """ - Daily scope and completed totals for burnup charts - """ - burnupTimeSeries: [BurnupChartDailyTotals!] - """ Historically accurate report about the timebox """ diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 00b42da7999..3965100cea8 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -5111,6 +5111,16 @@ }, "defaultValue": null }, + { + "name": "iterationTitle", + "description": "Filter by iteration title", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, { "name": "weight", "description": "Filter by weight", @@ -5150,6 +5160,16 @@ "ofType": null }, "defaultValue": null + }, + { + "name": "iterationWildcardId", + "description": "Filter by iteration ID wildcard", + "type": { + "kind": "ENUM", + "name": "IterationWildcardId", + "ofType": null + }, + "defaultValue": null } ], "interfaces": null, @@ -30696,28 +30716,6 @@ "name": "Iteration", "description": "Represents an iteration object", "fields": [ - { - "name": "burnupTimeSeries", - "description": "Daily scope and completed totals for burnup charts", - "args": [ - - ], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "BurnupChartDailyTotals", - "ofType": null - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "createdAt", "description": "Timestamp of iteration creation", @@ -31135,6 +31133,29 @@ ], "possibleTypes": null }, + { + "kind": "ENUM", + "name": "IterationWildcardId", + "description": "Iteration ID wildcard values", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "NONE", + "description": "No iteration is assigned", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ANY", + "description": "An iteration is assigned", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, { "kind": "SCALAR", "name": "JSON", @@ -36633,28 +36654,6 @@ "name": "Milestone", "description": "Represents a milestone", "fields": [ - { - "name": "burnupTimeSeries", - "description": "Daily scope and completed totals for burnup charts", - "args": [ - - ], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "BurnupChartDailyTotals", - "ofType": null - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "createdAt", "description": "Timestamp of milestone creation", @@ -41116,6 +41115,16 @@ }, "defaultValue": null }, + { + "name": "iterationTitle", + "description": "Filter by iteration title", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, { "name": "weight", "description": "Filter by weight", @@ -60920,28 +60929,6 @@ "name": "TimeboxReportInterface", "description": null, "fields": [ - { - "name": "burnupTimeSeries", - "description": "Daily scope and completed totals for burnup charts", - "args": [ - - ], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "BurnupChartDailyTotals", - "ofType": null - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "report", "description": "Historically accurate report about the timebox", diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 001b982abaf..2480deb197b 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -1719,7 +1719,6 @@ Represents an iteration object. | Field | Type | Description | | ----- | ---- | ----------- | -| `burnupTimeSeries` | BurnupChartDailyTotals! => Array | Daily scope and completed totals for burnup charts | | `createdAt` | Time! | Timestamp of iteration creation | | `description` | String | Description of the iteration | | `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` | @@ -2042,7 +2041,6 @@ Represents a milestone. | Field | Type | Description | | ----- | ---- | ----------- | -| `burnupTimeSeries` | BurnupChartDailyTotals! => Array | Daily scope and completed totals for burnup charts | | `createdAt` | Time! | Timestamp of milestone creation | | `description` | String | Description of the milestone | | `dueDate` | Time | Timestamp of the milestone due date | @@ -3994,6 +3992,15 @@ State of a GitLab iteration. | `started` | | | `upcoming` | | +### IterationWildcardId + +Iteration ID wildcard values. + +| Value | Description | +| ----- | ----------- | +| `ANY` | An iteration is assigned | +| `NONE` | No iteration is assigned | + ### ListLimitMetric List limit metric setting. diff --git a/doc/api/projects.md b/doc/api/projects.md index 9c83d4adb11..b14fe33ab1f 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -1927,6 +1927,38 @@ The returned `url` is relative to the project path. The returned `full_path` is the absolute path to the file. In Markdown contexts, the link is expanded when the format in `markdown` is used. +## Upload a project avatar + +Uploads an avatar to the specified project. + +```plaintext +PUT /projects/:id +``` + +| Attribute | Type | Required | Description | +|-----------|----------------|------------------------|-------------| +| `avatar` | string | **{check-circle}** Yes | The file to be uploaded. | +| `id` | integer/string | **{check-circle}** Yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | + +To upload an avatar from your file system, use the `--form` argument. This causes +cURL to post data using the header `Content-Type: multipart/form-data`. The +`file=` parameter must point to an image file on your file system and be +preceded by `@`. For example: + +Example request: + +```shell +curl --request PUT --header "PRIVATE-TOKEN: " --form "avatar=@dk.png" "https://gitlab.example.com/api/v4/projects/5" +``` + +Returned object: + +```json +{ + "avatar_url": "https://gitlab.example.com/uploads/-/system/project/avatar/2/dk.png" +} +``` + ## Share project with group Allow to share project with group. diff --git a/doc/ci/cloud_deployment/index.md b/doc/ci/cloud_deployment/index.md index b1d947a5f08..e8267ec9645 100644 --- a/doc/ci/cloud_deployment/index.md +++ b/doc/ci/cloud_deployment/index.md @@ -284,11 +284,14 @@ When running your project pipeline at this point: #### Custom build job for Auto DevOps -To leverage [Auto DevOps](../../topics/autodevops/index.md) for your project when deploying to -AWS EC2, you must specify a job for the `build` stage. +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216008) in GitLab 13.6. -To do so, you must reference the `Auto-DevOps.gitlab-ci.yml` template and include a job named -`build_artifact` in your `.gitlab-ci.yml` file. For example: +To leverage [Auto DevOps](../../topics/autodevops/index.md) for your project when deploying to +AWS EC2, first you must define [your AWS credentials as environment variables](#run-aws-commands-from-gitlab-cicd). + +Next, define a job for the `build` stage. To do so, you must reference the +`Auto-DevOps.gitlab-ci.yml` template and include a job named `build_artifact` in your +`.gitlab-ci.yml` file. For example: ```yaml # .gitlab-ci.yml diff --git a/doc/development/product_analytics/usage_ping.md b/doc/development/product_analytics/usage_ping.md index 96081ea2f0e..e6ffb052a5a 100644 --- a/doc/development/product_analytics/usage_ping.md +++ b/doc/development/product_analytics/usage_ping.md @@ -447,6 +447,23 @@ Implemented using Redis methods [PFADD](https://redis.io/commands/pfadd) and [PF - `end_date`: end date of the period for which we want to get event data. - `context`: context of the event. Allowed values are `default`, `free`, `bronze`, `silver`, `gold`, `starter`, `premium`, `ultimate`. +1. Testing tracking and getting unique events + +Trigger events in rails console by using `track_event` method + + ```ruby + Gitlab::UsageDataCounters::HLLRedisCounter.track_event(1, 'g_compliance_audit_events') + Gitlab::UsageDataCounters::HLLRedisCounter.track_event(2, 'g_compliance_audit_events') + ``` + +Next, get the unique events for the current week. + + ```ruby + # Get unique events for metric for current_week + Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: 'g_compliance_audit_events', + start_date: Date.current.beginning_of_week, end_date: Date.current.end_of_week) + ``` + Recommendations: - Key should expire in 29 days for daily and 42 days for weekly. diff --git a/doc/development/sidekiq_style_guide.md b/doc/development/sidekiq_style_guide.md index dbd69543f9c..ff4606923b1 100644 --- a/doc/development/sidekiq_style_guide.md +++ b/doc/development/sidekiq_style_guide.md @@ -696,8 +696,8 @@ blocks: ## Arguments logging -When [`SIDEKIQ_LOG_ARGUMENTS`](../administration/troubleshooting/sidekiq.md#log-arguments-to-sidekiq-jobs) -is enabled, Sidekiq job arguments will be logged. +As of GitLab 13.6, Sidekiq job arguments will be logged by default, unless [`SIDEKIQ_LOG_ARGUMENTS`](../administration/troubleshooting/sidekiq.md#log-arguments-to-sidekiq-jobs) +is disabled. By default, the only arguments logged are numeric arguments, because arguments of other types could contain sensitive information. To diff --git a/doc/user/admin_area/img/mr_approval_settings_compliance_project_v13_5.png b/doc/user/admin_area/img/mr_approval_settings_compliance_project_v13_5.png deleted file mode 100644 index 281ccc7aa03edd214e5f312dc6de6daa067dd6c3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33148 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

Zt3&Od9lUo(ZwMWkr#?uyqdZJI7=W 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 z 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 Introduced in [GitLab Premium](https://gitlab.com/gitlab-org/gitlab/-/issues/39060) 12.8. -Merge request approvals rules prevent users overriding certain settings on a project -level. When configured, only administrators can change these settings on a project level -if they are enabled at an instance level. +Merge request approval rules prevent users from overriding certain settings on the project +level. When enabled at the instance level, these settings are no longer editable on the +project level. To enable merge request approval rules for an instance: 1. Navigate to **Admin Area >** **{push-rules}** **Push Rules** and expand **Merge - requests approvals**. +requests approvals**. 1. Set the required rule. 1. Click **Save changes**. -GitLab administrators can later override these settings in a project’s settings. - ## Available rules Merge request approval rules that can be set at an instance level are: - **Prevent approval of merge requests by merge request author**. Prevents project - maintainers from allowing request authors to merge their own merge requests. +maintainers from allowing request authors to merge their own merge requests. - **Prevent approval of merge requests by merge request committers**. Prevents project - maintainers from allowing users to approve merge requests if they have submitted - any commits to the source branch. -- **Can override approvers and approvals required per merge request**. Allows project - maintainers to modify the approvers list in individual merge requests. - -## Scope rules to compliance-labeled projects - -> Introduced in [GitLab Premium](https://gitlab.com/groups/gitlab-org/-/epics/3432) 13.2. - -Merge request approval rules can be further scoped to specific compliance frameworks. - -When the compliance framework label is selected and the project is assigned the compliance -label, the instance-level MR approval settings will take effect and the -[project-level settings](../project/merge_requests/merge_request_approvals.md#adding--editing-a-default-approval-rule) -is locked for modification. - -When the compliance framework label is not selected or the project is not assigned the -compliance label, the project-level MR approval settings will take effect and the users with -Maintainer role and above can modify these. - -| Instance-level | Project-level | -| -------------- | ------------- | -| ![Scope MR approval settings to compliance frameworks](img/scope_mr_approval_settings_v13_5.png) | ![MR approval settings on compliance projects](img/mr_approval_settings_compliance_project_v13_5.png) | +maintainers from allowing users to approve merge requests if they have submitted +any commits to the source branch. +- **Prevent users from modifying merge request approvers list**. Prevents users from +modifying the approvers list in project settings or in individual merge requests. diff --git a/doc/user/compliance/compliance_dashboard/index.md b/doc/user/compliance/compliance_dashboard/index.md index 5c05725d95b..d2fb8dd318a 100644 --- a/doc/user/compliance/compliance_dashboard/index.md +++ b/doc/user/compliance/compliance_dashboard/index.md @@ -68,6 +68,7 @@ project to make sure it complies with the separation of duties described above. The Chain of Custody report allows customers to export a list of merge commits within the group. The data provides a comprehensive view with respect to merge commits. It includes the merge commit SHA, merge request author, merge request ID, merge user, pipeline ID, group name, project name, and merge request approvers. +Depending on the merge strategy, the merge commit SHA can either be a merge commit, squash commit or a diff head commit. To download the Chain of Custody report, navigate to **{shield}** **Security & Compliance > Compliance** on the group's menu and click **List of all merge commits** diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md index 66c8fd9402b..996aae4c780 100644 --- a/doc/user/gitlab_com/index.md +++ b/doc/user/gitlab_com/index.md @@ -432,7 +432,7 @@ and the following environment variables: | `SIDEKIQ_MEMORY_KILLER_CHECK_INTERVAL` | - | `3` | | `SIDEKIQ_MEMORY_KILLER_GRACE_TIME` | - | `900` | | `SIDEKIQ_MEMORY_KILLER_SHUTDOWN_WAIT` | - | `30` | -| `SIDEKIQ_LOG_ARGUMENTS` | `1` | - | +| `SIDEKIQ_LOG_ARGUMENTS` | `1` | `1` | NOTE: **Note:** The `SIDEKIQ_MEMORY_KILLER_MAX_RSS` setting is `16000000` on Sidekiq import diff --git a/doc/user/project/index.md b/doc/user/project/index.md index 976d4d75059..333c72a65b1 100644 --- a/doc/user/project/index.md +++ b/doc/user/project/index.md @@ -240,6 +240,14 @@ For users without permissions to view the project's code: - The wiki homepage is displayed, if any. - The list of issues within the project is displayed. +## GitLab Workflow - VS Code extension + +To avoid switching from the GitLab UI and VS Code while working in GitLab repositories, you can integrate +the [VS Code](https://code.visualstudio.com/) editor with GitLab through the +[GitLab Workflow extension](https://marketplace.visualstudio.com/items?itemName=GitLab.gitlab-workflow). + +To review or contribute to the extension's code, visit [its codebase in GitLab](https://gitlab.com/gitlab-org/gitlab-vscode-extension/). + ## Redirects when changing repository paths When a repository path changes, it is essential to smoothly transition from the diff --git a/doc/user/project/static_site_editor/index.md b/doc/user/project/static_site_editor/index.md index 47b18863e4d..e58667c275c 100644 --- a/doc/user/project/static_site_editor/index.md +++ b/doc/user/project/static_site_editor/index.md @@ -107,13 +107,36 @@ The Static Site Editors supports Markdown files (`.md`, `.md.erb`) for editing t ### Images > - Support for adding images through the WYSIWYG editor [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216640) in GitLab 13.1. +> - Support for uploading images via the WYSIWYG editor [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/218529) in GitLab 13.6. -You can add image files on the WYSIWYG mode by clicking the image icon (**{doc-image}**). -From there, link to a URL, add optional [ALT text](https://moz.com/learn/seo/alt-text), -and you're done. The link can reference images already hosted in your project, an asset hosted +#### Upload an image + +You can upload image files via the WYSIWYG editor directly to the repository to default upload directory +`source/images`. To do so: + +1. Click the image icon (**{doc-image}**). +1. Choose the **Upload file** tab. +1. Click **Choose file** to select a file from your computer. +1. Optional: add a description to the image for SEO and accessibility ([ALT text](https://moz.com/learn/seo/alt-text)). +1. Click **Insert image**. + +The selected file can be any supported image file (`.png`, `.jpg`, `.jpeg`, `.gif`). The editor renders +thumbnail previews so you can verify the correct image is included and there aren't any references to +missing images. + +#### Link to an image + +You can also link to an image if you'd like: + +1. Click the image icon (**{doc-image}**). +1. Choose the **Link to an image** tab. +1. Add the link to the image into the **Image URL** field (use the full path; relative paths are not supported yet). +1. Optional: add a description to the image for SEO and accessibility ([ALT text](https://moz.com/learn/seo/alt-text)). +1. Click **Insert image**. + +The link can reference images already hosted in your project, an asset hosted externally on a content delivery network, or any other external URL. The editor renders thumbnail previews so you can verify the correct image is included and there aren't any references to missing images. -default directory (`source/images/`). ### Videos diff --git a/lib/gitlab/conflict/file.rb b/lib/gitlab/conflict/file.rb index 0ca99506311..4d7590a8e38 100644 --- a/lib/gitlab/conflict/file.rb +++ b/lib/gitlab/conflict/file.rb @@ -9,6 +9,11 @@ module Gitlab CONTEXT_LINES = 3 + CONFLICT_TYPES = { + "old" => "conflict_marker_their", + "new" => "conflict_marker_our" + }.freeze + attr_reader :merge_request # 'raw' holds the Gitlab::Git::Conflict::File that this instance wraps @@ -46,6 +51,34 @@ module Gitlab end end + def diff_lines_for_serializer + # calculate sections and highlight lines before changing types + sections && highlight_lines! + + sections.flat_map do |section| + if section[:conflict] + lines = [] + + initial_type = nil + section[:lines].each do |line| + if line.type != initial_type + lines << create_separator_line(line) + initial_type = line.type + end + + line.type = CONFLICT_TYPES[line.type] + lines << line + end + + lines << create_separator_line(lines.last) + + lines + else + section[:lines] + end + end + end + def sections return @sections if @sections @@ -93,9 +126,15 @@ module Gitlab lines = tail_lines elsif conflict_before - # We're at the end of the file (no conflicts after), so just remove extra - # trailing lines. + # We're at the end of the file (no conflicts after) + number_of_trailing_lines = lines.size + + # Remove extra trailing lines lines = lines.first(CONTEXT_LINES) + + if number_of_trailing_lines > CONTEXT_LINES + lines << create_match_line(lines.last) + end end end @@ -117,6 +156,10 @@ module Gitlab Gitlab::Diff::Line.new('', 'match', line.index, line.old_pos, line.new_pos) end + def create_separator_line(line) + Gitlab::Diff::Line.new('', 'conflict_marker', line.index, nil, nil) + end + # Any line beginning with a letter, an underscore, or a dollar can be used in a # match line header. Only context sections can contain match lines, as match lines # have to exist in both versions of the file. diff --git a/lib/gitlab/diff/line.rb b/lib/gitlab/diff/line.rb index 379fc6af875..af9140215f0 100644 --- a/lib/gitlab/diff/line.rb +++ b/lib/gitlab/diff/line.rb @@ -8,9 +8,9 @@ module Gitlab # SERIALIZE_KEYS = %i(line_code rich_text text type index old_pos new_pos).freeze - attr_reader :line_code, :type, :old_pos, :new_pos + attr_reader :line_code, :old_pos, :new_pos attr_writer :rich_text - attr_accessor :text, :index + attr_accessor :text, :index, :type def initialize(text, type, index, old_pos, new_pos, parent_file: nil, line_code: nil, rich_text: nil) @text, @type, @index = text, type, index diff --git a/lib/gitlab/etag_caching/middleware.rb b/lib/gitlab/etag_caching/middleware.rb index 81cfce6a3db..fc3c05c57b2 100644 --- a/lib/gitlab/etag_caching/middleware.rb +++ b/lib/gitlab/etag_caching/middleware.rb @@ -3,6 +3,14 @@ module Gitlab module EtagCaching class Middleware + SKIP_HEADER_KEY = 'X-Gitlab-Skip-Etag' + + class << self + def skip!(response) + response.set_header(SKIP_HEADER_KEY, '1') + end + end + def initialize(app) @app = app end @@ -22,9 +30,7 @@ module Gitlab else track_cache_miss(if_none_match, cached_value_present, route) - status, headers, body = @app.call(env) - headers['ETag'] = etag - [status, headers, body] + maybe_apply_etag(etag, *@app.call(env)) end end @@ -43,6 +49,13 @@ module Gitlab [weak_etag_format(current_value), cached_value_present] end + def maybe_apply_etag(etag, status, headers, body) + headers['ETag'] = etag unless + Gitlab::Utils.to_boolean(headers.delete(SKIP_HEADER_KEY)) + + [status, headers, body] + end + def weak_etag_format(value) %Q{W/"#{value}"} end diff --git a/lib/gitlab/graphql/present/instrumentation.rb b/lib/gitlab/graphql/present/instrumentation.rb index 941a4f434a1..b8535575da5 100644 --- a/lib/gitlab/graphql/present/instrumentation.rb +++ b/lib/gitlab/graphql/present/instrumentation.rb @@ -4,6 +4,8 @@ module Gitlab module Graphql module Present class Instrumentation + SAFE_CONTEXT_KEYS = %i[current_user].freeze + def instrument(type, field) return field unless field.metadata[:type_class] @@ -22,7 +24,8 @@ module Gitlab next old_resolver.call(presented_type, args, context) end - presenter = presented_in.presenter_class.new(object, **context.to_h) + attrs = safe_context_values(context) + presenter = presented_in.presenter_class.new(object, **attrs) # we have to use the new `authorized_new` method, as `new` is protected wrapped = presented_type.class.authorized_new(presenter, context) @@ -34,6 +37,12 @@ module Gitlab resolve(resolve_with_presenter) end end + + private + + def safe_context_values(context) + context.to_h.slice(*SAFE_CONTEXT_KEYS) + end end end end diff --git a/lib/gitlab/metrics/requests_rack_middleware.rb b/lib/gitlab/metrics/requests_rack_middleware.rb index a541b27508b..23d7eb67312 100644 --- a/lib/gitlab/metrics/requests_rack_middleware.rb +++ b/lib/gitlab/metrics/requests_rack_middleware.rb @@ -32,20 +32,20 @@ module Gitlab end def self.http_requests_total - @http_requests_total ||= ::Gitlab::Metrics.counter(:http_requests_total, 'Request count') + ::Gitlab::Metrics.counter(:http_requests_total, 'Request count') end def self.rack_uncaught_errors_count - @rack_uncaught_errors_count ||= ::Gitlab::Metrics.counter(:rack_uncaught_errors_total, 'Request handling uncaught errors count') + ::Gitlab::Metrics.counter(:rack_uncaught_errors_total, 'Request handling uncaught errors count') end def self.http_request_duration_seconds - @http_request_duration_seconds ||= ::Gitlab::Metrics.histogram(:http_request_duration_seconds, 'Request handling execution time', - {}, [0.05, 0.1, 0.25, 0.5, 0.7, 1, 2.5, 5, 10, 25]) + ::Gitlab::Metrics.histogram(:http_request_duration_seconds, 'Request handling execution time', + {}, [0.05, 0.1, 0.25, 0.5, 0.7, 1, 2.5, 5, 10, 25]) end def self.http_health_requests_total - @http_health_requests_total ||= ::Gitlab::Metrics.counter(:http_health_requests_total, 'Health endpoint request count') + ::Gitlab::Metrics.counter(:http_health_requests_total, 'Health endpoint request count') end def self.initialize_metrics diff --git a/lib/gitlab/sidekiq_logging/logs_jobs.rb b/lib/gitlab/sidekiq_logging/logs_jobs.rb index 326dfdae661..dc81c34c4d0 100644 --- a/lib/gitlab/sidekiq_logging/logs_jobs.rb +++ b/lib/gitlab/sidekiq_logging/logs_jobs.rb @@ -16,7 +16,7 @@ module Gitlab # Add process id params job['pid'] = ::Process.pid - job.delete('args') unless ENV['SIDEKIQ_LOG_ARGUMENTS'] + job.delete('args') unless SidekiqLogArguments.enabled? job end diff --git a/lib/gitlab/usage_data_counters/designs_counter.rb b/lib/gitlab/usage_data_counters/designs_counter.rb index 22188b555d2..07e1963f9fb 100644 --- a/lib/gitlab/usage_data_counters/designs_counter.rb +++ b/lib/gitlab/usage_data_counters/designs_counter.rb @@ -1,42 +1,8 @@ # frozen_string_literal: true module Gitlab::UsageDataCounters - class DesignsCounter - extend Gitlab::UsageDataCounters::RedisCounter - + class DesignsCounter < BaseCounter KNOWN_EVENTS = %w[create update delete].freeze - - UnknownEvent = Class.new(StandardError) - - class << self - # Each event gets a unique Redis key - def redis_key(event) - raise UnknownEvent, event unless KNOWN_EVENTS.include?(event.to_s) - - "USAGE_DESIGN_MANAGEMENT_DESIGNS_#{event}".upcase - end - - def count(event) - increment(redis_key(event)) - end - - def read(event) - total_count(redis_key(event)) - end - - def totals - KNOWN_EVENTS.map { |event| [counter_key(event), read(event)] }.to_h - end - - def fallback_totals - KNOWN_EVENTS.map { |event| [counter_key(event), -1] }.to_h - end - - private - - def counter_key(event) - "design_management_designs_#{event}".to_sym - end - end + PREFIX = 'design_management_designs' end end diff --git a/lib/gitlab/usage_data_counters/web_ide_counter.rb b/lib/gitlab/usage_data_counters/web_ide_counter.rb index 00fcd42a9af..9f2f4ac3971 100644 --- a/lib/gitlab/usage_data_counters/web_ide_counter.rb +++ b/lib/gitlab/usage_data_counters/web_ide_counter.rb @@ -2,54 +2,43 @@ module Gitlab module UsageDataCounters - class WebIdeCounter - extend RedisCounter - KNOWN_EVENTS = %i[commits views merge_requests previews terminals pipelines].freeze + class WebIdeCounter < BaseCounter + KNOWN_EVENTS = %w[commits views merge_requests previews terminals pipelines].freeze PREFIX = 'web_ide' class << self def increment_commits_count - increment(redis_key('commits')) + count('commits') end def increment_merge_requests_count - increment(redis_key('merge_requests')) + count('merge_requests') end def increment_views_count - increment(redis_key('views')) + count('views') end def increment_terminals_count - increment(redis_key('terminals')) + count('terminals') end def increment_pipelines_count - increment(redis_key('pipelines')) + count('pipelines') end def increment_previews_count return unless Gitlab::CurrentSettings.web_ide_clientside_preview_enabled? - increment(redis_key('previews')) - end - - def totals - KNOWN_EVENTS.map { |event| [counter_key(event), total_count(redis_key(event))] }.to_h - end - - def fallback_totals - KNOWN_EVENTS.map { |event| [counter_key(event), -1] }.to_h + count('previews') end private def redis_key(event) - "#{PREFIX}_#{event}_count".upcase - end + require_known_event(event) - def counter_key(event) - "#{PREFIX}_#{event}".to_sym + "#{prefix}_#{event}_count".upcase end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index ee28aef4dd2..e725c6166c7 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -4696,9 +4696,6 @@ msgstr "" msgid "By %{user_name}" msgstr "" -msgid "By URL" -msgstr "" - msgid "By clicking Register, I agree that I have read and accepted the %{company_name} %{linkStart}Terms of Use and Privacy Policy%{linkEnd}" msgstr "" @@ -5053,9 +5050,6 @@ msgstr "" msgid "Changes won't take place until the index is %{link_start}recreated%{link_end}." msgstr "" -msgid "Changing a Release tag is only supported via Releases API. %{linkStart}More information%{linkEnd}" -msgstr "" - msgid "Changing group URL can have unintended side effects." msgstr "" @@ -6987,9 +6981,6 @@ msgstr "" msgid "Compliance framework (optional)" msgstr "" -msgid "Compliance frameworks" -msgstr "" - msgid "ComplianceDashboard|created by:" msgstr "" @@ -10113,7 +10104,7 @@ msgstr "" msgid "Enable" msgstr "" -msgid "Enable %{link_start}Gitpod%{link_end} integration to launch a development environment in your browser directly from GitLab." +msgid "Enable %{linkStart}Gitpod%{linkEnd} integration to launch a development environment in your browser directly from GitLab." msgstr "" msgid "Enable Auto DevOps" @@ -16149,6 +16140,9 @@ msgstr "" msgid "Link title is required" msgstr "" +msgid "Link to an image" +msgstr "" + msgid "Link to go to GitLab pipeline documentation" msgstr "" @@ -20313,18 +20307,12 @@ msgstr "" msgid "Preferences|Choose what content you want to see on your homepage." msgstr "" -msgid "Preferences|Customize integrations with third party services." -msgstr "" - msgid "Preferences|Customize the appearance of the application header and navigation sidebar." msgstr "" msgid "Preferences|Display time in 24-hour format" msgstr "" -msgid "Preferences|Enable integrated code intelligence on code views" -msgstr "" - msgid "Preferences|For example: 30 mins ago." msgstr "" @@ -20334,9 +20322,6 @@ msgstr "" msgid "Preferences|Instead of all the files changed, show only one file at a time. To switch between files, use the file browser." msgstr "" -msgid "Preferences|Integrations" -msgstr "" - msgid "Preferences|Layout width" msgstr "" @@ -20358,9 +20343,6 @@ msgstr "" msgid "Preferences|Show whitespace changes in diffs" msgstr "" -msgid "Preferences|Sourcegraph" -msgstr "" - msgid "Preferences|Syntax highlighting theme" msgstr "" @@ -20415,6 +20397,9 @@ msgstr "" msgid "Prevent users from changing their profile name" msgstr "" +msgid "Prevent users from modifying merge request approvers list" +msgstr "" + msgid "Prevent users from performing write operations on GitLab while performing maintenance." msgstr "" @@ -20544,6 +20529,24 @@ msgstr "" msgid "Profile Settings" msgstr "" +msgid "ProfilePreferences|Customize integrations with third party services." +msgstr "" + +msgid "ProfilePreferences|Enable Gitpod integration" +msgstr "" + +msgid "ProfilePreferences|Enable integrated code intelligence on code views" +msgstr "" + +msgid "ProfilePreferences|Gitpod" +msgstr "" + +msgid "ProfilePreferences|Integrations" +msgstr "" + +msgid "ProfilePreferences|Sourcegraph" +msgstr "" + msgid "ProfileSession|on" msgstr "" @@ -22331,9 +22334,6 @@ msgstr "" msgid "Registry setup" msgstr "" -msgid "Regulate approvals by authors/committers, based on compliance frameworks. Can be changed only at the instance level." -msgstr "" - msgid "Reindexing status" msgstr "" @@ -25593,10 +25593,10 @@ msgstr "" msgid "SourcegraphPreferences|This feature is experimental." msgstr "" -msgid "SourcegraphPreferences|Uses %{link_start}Sourcegraph.com%{link_end}." +msgid "SourcegraphPreferences|Uses %{linkStart}Sourcegraph.com%{linkEnd}." msgstr "" -msgid "SourcegraphPreferences|Uses a custom %{link_start}Sourcegraph instance%{link_end}." +msgid "SourcegraphPreferences|Uses a custom %{linkStart}Sourcegraph instance%{linkEnd}." msgstr "" msgid "Spam Logs" @@ -26765,9 +26765,6 @@ msgstr "" msgid "The X509 Certificate to use when mutual TLS is required to communicate with the external authorization service. If left blank, the server certificate is still validated when accessing over HTTPS." msgstr "" -msgid "The above settings apply to all projects with the selected compliance framework(s)." -msgstr "" - msgid "The application will be used where the client secret can be kept confidential. Native mobile apps and Single Page Apps are considered non-confidential." msgstr "" @@ -27085,6 +27082,9 @@ msgstr "" msgid "The status of the table below only applies to the default branch and is based on the %{linkStart}latest pipeline%{linkEnd}. Once you've enabled a scan for the default branch, any subsequent feature branch you create will include the scan." msgstr "" +msgid "The tag name can't be changed for an existing release." +msgstr "" + msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running." msgstr "" @@ -29180,6 +29180,9 @@ msgstr "" msgid "Upload a private key for your certificate" msgstr "" +msgid "Upload an image" +msgstr "" + msgid "Upload file" msgstr "" diff --git a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb index 5c6af3316d8..bda1f1a3b1c 100644 --- a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb +++ b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb @@ -383,6 +383,7 @@ RSpec.describe Projects::MergeRequests::DiffsController do environment: nil, merge_request: merge_request, diff_view: :inline, + merge_ref_head_diff: nil, pagination_data: { current_page: nil, next_page: nil, diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb index 8c59b2b378f..d76432f71b3 100644 --- a/spec/controllers/projects/notes_controller_spec.rb +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -113,6 +113,8 @@ RSpec.describe Projects::NotesController do end it 'returns the first page of notes' do + expect(Gitlab::EtagCaching::Middleware).to receive(:skip!) + get :index, params: request_params expect(json_response['notes'].count).to eq(page_1.count) @@ -122,6 +124,8 @@ RSpec.describe Projects::NotesController do end it 'returns the second page of notes' do + expect(Gitlab::EtagCaching::Middleware).to receive(:skip!) + request.headers['X-Last-Fetched-At'] = page_1_boundary get :index, params: request_params @@ -133,6 +137,8 @@ RSpec.describe Projects::NotesController do end it 'returns the final page of notes' do + expect(Gitlab::EtagCaching::Middleware).to receive(:skip!) + request.headers['X-Last-Fetched-At'] = page_2_boundary get :index, params: request_params @@ -142,6 +148,19 @@ RSpec.describe Projects::NotesController do expect(json_response['last_fetched_at']).to eq(microseconds(Time.zone.now)) expect(response.headers['Poll-Interval'].to_i).to be > 1 end + + it 'returns an empty page of notes' do + expect(Gitlab::EtagCaching::Middleware).not_to receive(:skip!) + + request.headers['X-Last-Fetched-At'] = microseconds(Time.zone.now) + + get :index, params: request_params + + expect(json_response['notes']).to be_empty + expect(json_response['more']).to be_falsy + expect(json_response['last_fetched_at']).to eq(microseconds(Time.zone.now)) + expect(response.headers['Poll-Interval'].to_i).to be > 1 + end end context 'feature flag disabled' do diff --git a/spec/frontend/profile/preferences/components/__snapshots__/integration_view_spec.js.snap b/spec/frontend/profile/preferences/components/__snapshots__/integration_view_spec.js.snap new file mode 100644 index 00000000000..2fd1fd6a04e --- /dev/null +++ b/spec/frontend/profile/preferences/components/__snapshots__/integration_view_spec.js.snap @@ -0,0 +1,67 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`IntegrationView component should render IntegrationView properly 1`] = ` +

+`; diff --git a/spec/frontend/profile/preferences/components/__snapshots__/profile_preferences_spec.js.snap b/spec/frontend/profile/preferences/components/__snapshots__/profile_preferences_spec.js.snap new file mode 100644 index 00000000000..4df92cf86a5 --- /dev/null +++ b/spec/frontend/profile/preferences/components/__snapshots__/profile_preferences_spec.js.snap @@ -0,0 +1,51 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ProfilePreferences component should render ProfilePreferences properly 1`] = ` +
+
+
+
+ +
+

+ + Integrations + +

+ +

+ + Customize integrations with third party services. + +

+
+ +
+ + +
+
+`; diff --git a/spec/frontend/profile/preferences/components/integration_view_spec.js b/spec/frontend/profile/preferences/components/integration_view_spec.js new file mode 100644 index 00000000000..5d55a089119 --- /dev/null +++ b/spec/frontend/profile/preferences/components/integration_view_spec.js @@ -0,0 +1,124 @@ +import { shallowMount } from '@vue/test-utils'; + +import { GlFormText } from '@gitlab/ui'; +import IntegrationView from '~/profile/preferences/components/integration_view.vue'; +import IntegrationHelpText from '~/vue_shared/components/integrations_help_text.vue'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import { integrationViews, userFields } from '../mock_data'; + +const viewProps = convertObjectPropsToCamelCase(integrationViews[0]); + +describe('IntegrationView component', () => { + let wrapper; + const defaultProps = { + config: { + title: 'Foo', + label: 'Enable foo', + formName: 'foo_enabled', + }, + ...viewProps, + }; + + function createComponent(options = {}) { + const { props = {}, provide = {} } = options; + return shallowMount(IntegrationView, { + provide: { + userFields, + ...provide, + }, + propsData: { + ...defaultProps, + ...props, + }, + }); + } + + function findCheckbox() { + return wrapper.find('[data-testid="profile-preferences-integration-checkbox"]'); + } + function findFormGroup() { + return wrapper.find('[data-testid="profile-preferences-integration-form-group"]'); + } + function findHiddenField() { + return wrapper.find('[data-testid="profile-preferences-integration-hidden-field"]'); + } + function findFormGroupLabel() { + return wrapper.find('[data-testid="profile-preferences-integration-form-group"] label'); + } + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('should render the title correctly', () => { + wrapper = createComponent(); + + expect(wrapper.find('label.label-bold').text()).toBe('Foo'); + }); + + it('should render the form correctly', () => { + wrapper = createComponent(); + + expect(findFormGroup().exists()).toBe(true); + expect(findHiddenField().exists()).toBe(true); + expect(findCheckbox().exists()).toBe(true); + expect(findCheckbox().attributes('id')).toBe('user_foo_enabled'); + expect(findCheckbox().attributes('name')).toBe('user[foo_enabled]'); + }); + + it('should have the checkbox value to be set to 1', () => { + wrapper = createComponent(); + + expect(findCheckbox().attributes('value')).toBe('1'); + }); + + it('should have the hidden value to be set to 0', () => { + wrapper = createComponent(); + + expect(findHiddenField().attributes('value')).toBe('0'); + }); + + it('should set the checkbox value to be true', () => { + wrapper = createComponent(); + + expect(findCheckbox().element.checked).toBe(true); + }); + + it('should set the checkbox value to be false when false is provided', () => { + wrapper = createComponent({ + provide: { + userFields: { + foo_enabled: false, + }, + }, + }); + + expect(findCheckbox().element.checked).toBe(false); + }); + + it('should set the checkbox value to be false when not provided', () => { + wrapper = createComponent({ provide: { userFields: {} } }); + + expect(findCheckbox().element.checked).toBe(false); + }); + + it('should render the help text', () => { + wrapper = createComponent(); + + expect(wrapper.find(GlFormText).exists()).toBe(true); + expect(wrapper.find(IntegrationHelpText).exists()).toBe(true); + }); + + it('should render the label correctly', () => { + wrapper = createComponent(); + + expect(findFormGroupLabel().text()).toBe('Enable foo'); + }); + + it('should render IntegrationView properly', () => { + wrapper = createComponent(); + + expect(wrapper.element).toMatchSnapshot(); + }); +}); diff --git a/spec/frontend/profile/preferences/components/profile_preferences_spec.js b/spec/frontend/profile/preferences/components/profile_preferences_spec.js new file mode 100644 index 00000000000..fcc27d8faaf --- /dev/null +++ b/spec/frontend/profile/preferences/components/profile_preferences_spec.js @@ -0,0 +1,57 @@ +import { shallowMount } from '@vue/test-utils'; + +import ProfilePreferences from '~/profile/preferences/components/profile_preferences.vue'; +import IntegrationView from '~/profile/preferences/components/integration_view.vue'; +import { integrationViews, userFields } from '../mock_data'; + +describe('ProfilePreferences component', () => { + let wrapper; + const defaultProvide = { + integrationViews: [], + userFields, + }; + + function createComponent(options = {}) { + const { props = {}, provide = {} } = options; + return shallowMount(ProfilePreferences, { + provide: { + ...defaultProvide, + ...provide, + }, + propsData: props, + }); + } + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('should not render Integrations section', () => { + wrapper = createComponent(); + const views = wrapper.findAll(IntegrationView); + const divider = wrapper.find('[data-testid="profile-preferences-integrations-rule"]'); + const heading = wrapper.find('[data-testid="profile-preferences-integrations-heading"]'); + + expect(divider.exists()).toBe(false); + expect(heading.exists()).toBe(false); + expect(views).toHaveLength(0); + }); + + it('should render Integration section', () => { + wrapper = createComponent({ provide: { integrationViews } }); + const divider = wrapper.find('[data-testid="profile-preferences-integrations-rule"]'); + const heading = wrapper.find('[data-testid="profile-preferences-integrations-heading"]'); + const views = wrapper.findAll(IntegrationView); + + expect(divider.exists()).toBe(true); + expect(heading.exists()).toBe(true); + expect(views).toHaveLength(integrationViews.length); + }); + + it('should render ProfilePreferences properly', () => { + wrapper = createComponent({ provide: { integrationViews } }); + + expect(wrapper.element).toMatchSnapshot(); + }); +}); diff --git a/spec/frontend/profile/preferences/mock_data.js b/spec/frontend/profile/preferences/mock_data.js new file mode 100644 index 00000000000..d07d5f565dc --- /dev/null +++ b/spec/frontend/profile/preferences/mock_data.js @@ -0,0 +1,18 @@ +export const integrationViews = [ + { + name: 'sourcegraph', + help_link: 'http://foo.com/help', + message: 'Click %{linkStart}Foo%{linkEnd}!', + message_url: 'http://foo.com', + }, + { + name: 'gitpod', + help_link: 'http://bar.com/help', + message: 'Click %{linkStart}Bar%{linkEnd}!', + message_url: 'http://bar.com', + }, +]; + +export const userFields = { + foo_enabled: true, +}; diff --git a/spec/frontend/releases/components/app_edit_new_spec.js b/spec/frontend/releases/components/app_edit_new_spec.js index c0680acb7cd..1d409b5b590 100644 --- a/spec/frontend/releases/components/app_edit_new_spec.js +++ b/spec/frontend/releases/components/app_edit_new_spec.js @@ -24,7 +24,6 @@ describe('Release edit/new component', () => { state = { release, markdownDocsPath: 'path/to/markdown/docs', - updateReleaseApiDocsPath: 'path/to/update/release/api/docs', releasesPagePath: 'path/to/releases/page', projectId: '8', groupId: '42', diff --git a/spec/frontend/releases/components/tag_field_exsting_spec.js b/spec/frontend/releases/components/tag_field_exsting_spec.js index 70a195556df..d4110b57776 100644 --- a/spec/frontend/releases/components/tag_field_exsting_spec.js +++ b/spec/frontend/releases/components/tag_field_exsting_spec.js @@ -6,7 +6,6 @@ import createStore from '~/releases/stores'; import createDetailModule from '~/releases/stores/modules/detail'; const TEST_TAG_NAME = 'test-tag-name'; -const TEST_DOCS_PATH = '/help/test/docs/path'; const localVue = createLocalVue(); localVue.use(Vuex); @@ -24,21 +23,11 @@ describe('releases/components/tag_field_existing', () => { const findInput = () => wrapper.find(GlFormInput); const findHelp = () => wrapper.find('[data-testid="tag-name-help"]'); - const findHelpLink = () => { - const link = findHelp().find('a'); - - return { - text: link.text(), - href: link.attributes('href'), - target: link.attributes('target'), - }; - }; beforeEach(() => { store = createStore({ modules: { detail: createDetailModule({ - updateReleaseApiDocsPath: TEST_DOCS_PATH, tagName: TEST_TAG_NAME, }), }, @@ -68,16 +57,8 @@ describe('releases/components/tag_field_existing', () => { createComponent(mount); expect(findHelp().text()).toMatchInterpolatedText( - 'Changing a Release tag is only supported via Releases API. More information', + "The tag name can't be changed for an existing release.", ); - - const helpLink = findHelpLink(); - - expect(helpLink).toEqual({ - text: 'More information', - href: TEST_DOCS_PATH, - target: '_blank', - }); }); }); }); diff --git a/spec/frontend/releases/stores/modules/detail/actions_spec.js b/spec/frontend/releases/stores/modules/detail/actions_spec.js index d38f6766d4e..abd0db6a589 100644 --- a/spec/frontend/releases/stores/modules/detail/actions_spec.js +++ b/spec/frontend/releases/stores/modules/detail/actions_spec.js @@ -47,7 +47,6 @@ describe('Release detail actions', () => { releasesPagePath: 'path/to/releases/page', markdownDocsPath: 'path/to/markdown/docs', markdownPreviewPath: 'path/to/markdown/preview', - updateReleaseApiDocsPath: 'path/to/api/docs', }), ...getters, ...rootState, diff --git a/spec/frontend/releases/stores/modules/detail/mutations_spec.js b/spec/frontend/releases/stores/modules/detail/mutations_spec.js index f3e84262754..88eddc4019c 100644 --- a/spec/frontend/releases/stores/modules/detail/mutations_spec.js +++ b/spec/frontend/releases/stores/modules/detail/mutations_spec.js @@ -18,7 +18,6 @@ describe('Release detail mutations', () => { releasesPagePath: 'path/to/releases/page', markdownDocsPath: 'path/to/markdown/docs', markdownPreviewPath: 'path/to/markdown/preview', - updateReleaseApiDocsPath: 'path/to/api/docs', }); release = convertObjectPropsToCamelCase(originalRelease); }); diff --git a/spec/frontend/static_site_editor/services/renderers/render_image_spec.js b/spec/frontend/static_site_editor/services/renderers/render_image_spec.js index 5ea90b8184f..e9e40835982 100644 --- a/spec/frontend/static_site_editor/services/renderers/render_image_spec.js +++ b/spec/frontend/static_site_editor/services/renderers/render_image_spec.js @@ -3,9 +3,11 @@ import { mounts, project, branch, baseUrl } from '../../mock_data'; describe('rich_content_editor/renderers/render_image', () => { let renderer; + let imageRepository; beforeEach(() => { - renderer = imageRenderer.build(mounts, project, branch, baseUrl); + renderer = imageRenderer.build(mounts, project, branch, baseUrl, imageRepository); + imageRepository = { get: () => null }; }); describe('build', () => { @@ -27,6 +29,21 @@ describe('rich_content_editor/renderers/render_image', () => { }); describe('render', () => { + let skipChildren; + let context; + let node; + + beforeEach(() => { + skipChildren = jest.fn(); + context = { skipChildren }; + node = { + firstChild: { + type: 'img', + literal: 'Some Image', + }, + }; + }); + it.each` destination | isAbsolute | src ${'http://test.host/absolute/path/to/image.png'} | ${true} | ${'http://test.host/absolute/path/to/image.png'} @@ -36,15 +53,8 @@ describe('rich_content_editor/renderers/render_image', () => { ${'./relative/to/current/image.png'} | ${false} | ${'http://test.host/user1/project1/-/raw/master/./relative/to/current/image.png'} ${'../relative/to/current/image.png'} | ${false} | ${'http://test.host/user1/project1/-/raw/master/../relative/to/current/image.png'} `('returns an image with the correct attributes', ({ destination, isAbsolute, src }) => { - const skipChildren = jest.fn(); - const context = { skipChildren }; - const node = { - destination, - firstChild: { - type: 'img', - literal: 'Some Image', - }, - }; + node.destination = destination; + const result = renderer.render(node, context); expect(result).toEqual({ @@ -60,5 +70,27 @@ describe('rich_content_editor/renderers/render_image', () => { expect(skipChildren).toHaveBeenCalled(); }); + + it('renders an image if a cached image is found in the repository, use the base64 content as the source', () => { + const imageContent = 'some-content'; + const originalSrc = 'path/to/image.png'; + + imageRepository.get = () => imageContent; + renderer = imageRenderer.build(mounts, project, branch, baseUrl, imageRepository); + node.destination = originalSrc; + + const result = renderer.render(node, context); + + expect(result).toEqual({ + type: 'openTag', + tagName: 'img', + selfClose: true, + attributes: { + 'data-original-src': originalSrc, + src: `data:image;base64,${imageContent}`, + alt: 'Some Image', + }, + }); + }); }); }); diff --git a/spec/frontend/vue_shared/components/__snapshots__/integration_help_text_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/integration_help_text_spec.js.snap new file mode 100644 index 00000000000..df0fcf5da1c --- /dev/null +++ b/spec/frontend/vue_shared/components/__snapshots__/integration_help_text_spec.js.snap @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`IntegrationHelpText component should not render the link when start and end is not provided 1`] = ` + + Click nowhere! + +`; + +exports[`IntegrationHelpText component should render the help text 1`] = ` + + Click + + + Bar + + + + ! + +`; diff --git a/spec/frontend/vue_shared/components/integration_help_text_spec.js b/spec/frontend/vue_shared/components/integration_help_text_spec.js new file mode 100644 index 00000000000..4269d36d0e2 --- /dev/null +++ b/spec/frontend/vue_shared/components/integration_help_text_spec.js @@ -0,0 +1,57 @@ +import { shallowMount } from '@vue/test-utils'; + +import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui'; +import IntegrationHelpText from '~/vue_shared/components/integrations_help_text.vue'; + +describe('IntegrationHelpText component', () => { + let wrapper; + const defaultProps = { + message: 'Click %{linkStart}Bar%{linkEnd}!', + messageUrl: 'http://bar.com', + }; + + function createComponent(props = {}) { + return shallowMount(IntegrationHelpText, { + propsData: { + ...defaultProps, + ...props, + }, + stubs: { + GlSprintf, + }, + }); + } + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('should use the gl components', () => { + wrapper = createComponent(); + + expect(wrapper.find(GlSprintf).exists()).toBe(true); + expect(wrapper.find(GlIcon).exists()).toBe(true); + expect(wrapper.find(GlLink).exists()).toBe(true); + }); + + it('should render the help text', () => { + wrapper = createComponent(); + + expect(wrapper.element).toMatchSnapshot(); + }); + + it('should not use the gl-link and gl-icon components', () => { + wrapper = createComponent({ message: 'Click nowhere!' }); + + expect(wrapper.find(GlSprintf).exists()).toBe(true); + expect(wrapper.find(GlIcon).exists()).toBe(false); + expect(wrapper.find(GlLink).exists()).toBe(false); + }); + + it('should not render the link when start and end is not provided', () => { + wrapper = createComponent({ message: 'Click nowhere!' }); + + expect(wrapper.element).toMatchSnapshot(); + }); +}); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/editor_service_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/editor_service_spec.js index 0f2f263a776..d79df4d0557 100644 --- a/spec/frontend/vue_shared/components/rich_content_editor/editor_service_spec.js +++ b/spec/frontend/vue_shared/components/rich_content_editor/editor_service_spec.js @@ -91,12 +91,25 @@ describe('Editor Service', () => { }); describe('addImage', () => { - it('calls the exec method on the instance', () => { - const mockImage = { imageUrl: 'some/url.png', description: 'some description' }; + const file = new File([], 'some-file.jpg'); + const mockImage = { imageUrl: 'some/url.png', altText: 'some alt text' }; - addImage(mockInstance, mockImage); + it('calls the insertElement method on the squire instance when in WYSIWYG mode', () => { + jest.spyOn(URL, 'createObjectURL'); + mockInstance.editor.isWysiwygMode.mockReturnValue(true); + mockInstance.editor.getSquire.mockReturnValue({ insertElement: jest.fn() }); - expect(mockInstance.editor.exec).toHaveBeenCalledWith('AddImage', mockImage); + addImage(mockInstance, mockImage, file); + + expect(mockInstance.editor.getSquire().insertElement).toHaveBeenCalled(); + expect(global.URL.createObjectURL).toHaveBeenLastCalledWith(file); + }); + + it('calls the insertText method on the instance when in Markdown mode', () => { + mockInstance.editor.isWysiwygMode.mockReturnValue(false); + addImage(mockInstance, mockImage, file); + + expect(mockInstance.editor.insertText).toHaveBeenCalledWith('![some alt text](some/url.png)'); }); }); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal_spec.js index 0c2ac53aa52..16370a7aaad 100644 --- a/spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal_spec.js +++ b/spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal_spec.js @@ -15,10 +15,7 @@ describe('Add Image Modal', () => { const findDescriptionInput = () => wrapper.find({ ref: 'descriptionInput' }); beforeEach(() => { - wrapper = shallowMount(AddImageModal, { - provide: { glFeatures: { sseImageUploads: true } }, - propsData, - }); + wrapper = shallowMount(AddImageModal, { propsData }); }); describe('when content is loaded', () => { diff --git a/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js index 8c2c0413819..d50cf2915e8 100644 --- a/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js +++ b/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js @@ -180,7 +180,7 @@ describe('Rich Content Editor', () => { wrapper.vm.$refs.editor = mockInstance; findAddImageModal().vm.$emit('addImage', mockImage); - expect(addImage).toHaveBeenCalledWith(mockInstance, mockImage); + expect(addImage).toHaveBeenCalledWith(mockInstance, mockImage, undefined); }); }); diff --git a/spec/graphql/resolvers/concerns/caching_array_resolver_spec.rb b/spec/graphql/resolvers/concerns/caching_array_resolver_spec.rb new file mode 100644 index 00000000000..b6fe94a2312 --- /dev/null +++ b/spec/graphql/resolvers/concerns/caching_array_resolver_spec.rb @@ -0,0 +1,208 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::CachingArrayResolver do + include GraphqlHelpers + + let_it_be(:non_admins) { create_list(:user, 4, admin: false) } + let(:query_context) { {} } + let(:max_page_size) { 10 } + let(:field) { double('Field', max_page_size: max_page_size) } + let(:schema) { double('Schema', default_max_page_size: 3) } + + let_it_be(:caching_resolver) do + mod = described_class + + Class.new(::Resolvers::BaseResolver) do + include mod + + def query_input(is_admin:) + is_admin + end + + def query_for(is_admin) + if is_admin.nil? + model_class.all + else + model_class.where(admin: is_admin) + end + end + + def model_class + User # Happens to include FromUnion, and is cheap-ish to create + end + end + end + + describe '#resolve' do + context 'there are more than MAX_UNION_SIZE queries' do + let_it_be(:max_union) { 3 } + let_it_be(:resolver) do + mod = described_class + max = max_union + + Class.new(::Resolvers::BaseResolver) do + include mod + + def query_input(username:) + username + end + + def query_for(username) + if username.nil? + model_class.all + else + model_class.where(username: username) + end + end + + def model_class + User # Happens to include FromUnion, and is cheap-ish to create + end + + define_method :max_union_size do + max + end + end + end + + it 'executes the queries in multiple batches' do + users = create_list(:user, (max_union * 2) + 1) + expect(User).to receive(:from_union).twice.and_call_original + + results = users.in_groups_of(2, false).map do |users| + resolve(resolver, args: { username: users.map(&:username) }, field: field, schema: schema) + end + + expect(results.flat_map(&method(:force))).to match_array(users) + end + end + + context 'all queries return results' do + let_it_be(:admins) { create_list(:admin, 3) } + + it 'batches the queries' do + expect do + [resolve_users(true), resolve_users(false)].each(&method(:force)) + end.to issue_same_number_of_queries_as { force(resolve_users(nil)) } + end + + it 'finds the correct values' do + found_admins = resolve_users(true) + found_others = resolve_users(false) + admins_again = resolve_users(true) + found_all = resolve_users(nil) + + expect(force(found_admins)).to match_array(admins) + expect(force(found_others)).to match_array(non_admins) + expect(force(admins_again)).to match_array(admins) + expect(force(found_all)).to match_array(admins + non_admins) + end + end + + it 'does not perform a union of a query with itself' do + expect(User).to receive(:where).once.and_call_original + + [resolve_users(false), resolve_users(false)].each(&method(:force)) + end + + context 'one of the queries returns no results' do + it 'finds the correct values' do + found_admins = resolve_users(true) + found_others = resolve_users(false) + found_all = resolve_users(nil) + + expect(force(found_admins)).to be_empty + expect(force(found_others)).to match_array(non_admins) + expect(force(found_all)).to match_array(non_admins) + end + end + + context 'one of the queries has already been cached' do + before do + force(resolve_users(nil)) + end + + it 'avoids further queries' do + expect do + repeated_find = resolve_users(nil) + + expect(force(repeated_find)).to match_array(non_admins) + end.not_to exceed_query_limit(0) + end + end + + context 'the resolver overrides item_found' do + let_it_be(:admins) { create_list(:admin, 2) } + let(:query_context) do + { + found: { true => [], false => [], nil => [] } + } + end + + let_it_be(:with_item_found) do + Class.new(caching_resolver) do + def item_found(key, item) + context[:found][key] << item + end + end + end + + it 'receives item_found for each key the item mapped to' do + found_admins = resolve_users(true, with_item_found) + found_all = resolve_users(nil, with_item_found) + + [found_admins, found_all].each(&method(:force)) + + expect(query_context[:found]).to match({ + false => be_empty, + true => match_array(admins), + nil => match_array(admins + non_admins) + }) + end + end + + context 'the max_page_size is lower than the total result size' do + let(:max_page_size) { 2 } + + it 'respects the max_page_size, on a per subset basis' do + found_all = resolve_users(nil) + found_others = resolve_users(false) + + expect(force(found_all).size).to eq(2) + expect(force(found_others).size).to eq(2) + end + end + + context 'the field does not declare max_page_size' do + let(:max_page_size) { nil } + + it 'takes the page size from schema.default_max_page_size' do + found_all = resolve_users(nil) + found_others = resolve_users(false) + + expect(force(found_all).size).to eq(schema.default_max_page_size) + expect(force(found_others).size).to eq(schema.default_max_page_size) + end + end + + specify 'force . resolve === to_a . query_for . query_input' do + r = resolver_instance(caching_resolver) + args = { is_admin: false } + + naive = r.query_for(r.query_input(**args)).to_a + + expect(force(r.resolve(**args))).to eq(naive) + end + end + + def resolve_users(is_admin, resolver = caching_resolver) + args = { is_admin: is_admin } + resolve(resolver, args: args, field: field, ctx: query_context, schema: schema) + end + + def force(lazy) + ::Gitlab::Graphql::Lazy.force(lazy) + end +end diff --git a/spec/helpers/releases_helper_spec.rb b/spec/helpers/releases_helper_spec.rb index 7dc1328f065..e6bf91ceef6 100644 --- a/spec/helpers/releases_helper_spec.rb +++ b/spec/helpers/releases_helper_spec.rb @@ -71,7 +71,6 @@ RSpec.describe ReleasesHelper do markdown_preview_path markdown_docs_path releases_page_path - update_release_api_docs_path release_assets_docs_path manage_milestones_path new_milestone_path) @@ -89,7 +88,6 @@ RSpec.describe ReleasesHelper do releases_page_path markdown_preview_path markdown_docs_path - update_release_api_docs_path release_assets_docs_path manage_milestones_path new_milestone_path diff --git a/spec/helpers/sourcegraph_helper_spec.rb b/spec/helpers/sourcegraph_helper_spec.rb index 6a95c8e4a43..d03893ea9ae 100644 --- a/spec/helpers/sourcegraph_helper_spec.rb +++ b/spec/helpers/sourcegraph_helper_spec.rb @@ -5,60 +5,43 @@ require 'spec_helper' RSpec.describe SourcegraphHelper do describe '#sourcegraph_url_message' do let(:sourcegraph_url) { 'http://sourcegraph.example.com' } + let(:feature_conditional) { false } + let(:public_only) { false } + let(:is_com) { true } before do allow(Gitlab::CurrentSettings).to receive(:sourcegraph_url).and_return(sourcegraph_url) allow(Gitlab::CurrentSettings).to receive(:sourcegraph_url_is_com?).and_return(is_com) + allow(Gitlab::CurrentSettings).to receive(:sourcegraph_public_only).and_return(public_only) + allow(Gitlab::Sourcegraph).to receive(:feature_conditional?).and_return(feature_conditional) end subject { helper.sourcegraph_url_message } context 'with .com sourcegraph url' do - let(:is_com) { true } - - it { is_expected.to have_text('Uses Sourcegraph.com') } - it { is_expected.to have_link('Sourcegraph.com', href: sourcegraph_url) } + it { is_expected.to have_text('Uses %{linkStart}Sourcegraph.com%{linkEnd}. This feature is experimental.') } end context 'with custom sourcegraph url' do let(:is_com) { false } - it { is_expected.to have_text('Uses a custom Sourcegraph instance') } - it { is_expected.to have_link('Sourcegraph instance', href: sourcegraph_url) } - - context 'with unsafe url' do - let(:sourcegraph_url) { '\" onload=\"alert(1);\"' } - - it { is_expected.to have_link('Sourcegraph instance', href: sourcegraph_url) } - end + it { is_expected.to have_text('Uses a custom %{linkStart}Sourcegraph instance%{linkEnd}. This feature is experimental.') } end - end - - describe '#sourcegraph_experimental_message' do - let(:feature_conditional) { false } - let(:public_only) { false } - - before do - allow(Gitlab::CurrentSettings).to receive(:sourcegraph_public_only).and_return(public_only) - allow(Gitlab::Sourcegraph).to receive(:feature_conditional?).and_return(feature_conditional) - end - - subject { helper.sourcegraph_experimental_message } context 'when not limited by feature or public only' do - it { is_expected.to eq "This feature is experimental." } + it { is_expected.to eq 'Uses %{linkStart}Sourcegraph.com%{linkEnd}. This feature is experimental.' } end context 'when limited by feature' do let(:feature_conditional) { true } - it { is_expected.to eq "This feature is experimental and currently limited to certain projects." } + it { is_expected.to eq 'Uses %{linkStart}Sourcegraph.com%{linkEnd}. This feature is experimental and currently limited to certain projects.' } end context 'when limited by public only' do let(:public_only) { true } - it { is_expected.to eq "This feature is experimental and limited to public projects." } + it { is_expected.to eq 'Uses %{linkStart}Sourcegraph.com%{linkEnd}. This feature is experimental and limited to public projects.' } end end end diff --git a/spec/lib/gitlab/conflict/file_spec.rb b/spec/lib/gitlab/conflict/file_spec.rb index 80bd517ec92..0de944d3f8a 100644 --- a/spec/lib/gitlab/conflict/file_spec.rb +++ b/spec/lib/gitlab/conflict/file_spec.rb @@ -93,6 +93,51 @@ RSpec.describe Gitlab::Conflict::File do end end + describe '#diff_lines_for_serializer' do + let(:diff_line_types) { conflict_file.diff_lines_for_serializer.map(&:type) } + + it 'assigns conflict types to the diff lines' do + expect(diff_line_types[4]).to eq('conflict_marker') + expect(diff_line_types[5..10]).to eq(['conflict_marker_our'] * 6) + expect(diff_line_types[11]).to eq('conflict_marker') + expect(diff_line_types[12..17]).to eq(['conflict_marker_their'] * 6) + expect(diff_line_types[18]).to eq('conflict_marker') + + expect(diff_line_types[19..24]).to eq([nil] * 6) + + expect(diff_line_types[25]).to eq('conflict_marker') + expect(diff_line_types[26..27]).to eq(['conflict_marker_our'] * 2) + expect(diff_line_types[28]).to eq('conflict_marker') + expect(diff_line_types[29..30]).to eq(['conflict_marker_their'] * 2) + expect(diff_line_types[31]).to eq('conflict_marker') + end + + it 'does not add a match line to the end of the section' do + expect(diff_line_types.last).to eq(nil) + end + + context 'when there are unchanged trailing lines' do + let(:rugged_conflict) { index.conflicts.first } + let(:raw_conflict_content) { index.merge_file('files/ruby/popen.rb')[:data] } + + it 'assign conflict types and adds match line to the end of the section' do + expect(diff_line_types).to eq([ + 'match', + nil, nil, nil, + "conflict_marker", + "conflict_marker_our", + "conflict_marker", + "conflict_marker_their", + "conflict_marker_their", + "conflict_marker_their", + "conflict_marker", + nil, nil, nil, + "match" + ]) + end + end + end + describe '#sections' do it 'only inserts match lines when there is a gap between sections' do conflict_file.sections.each_with_index do |section, i| diff --git a/spec/lib/gitlab/etag_caching/middleware_spec.rb b/spec/lib/gitlab/etag_caching/middleware_spec.rb index 45916e78532..3122a3b1c07 100644 --- a/spec/lib/gitlab/etag_caching/middleware_spec.rb +++ b/spec/lib/gitlab/etag_caching/middleware_spec.rb @@ -10,6 +10,17 @@ RSpec.describe Gitlab::EtagCaching::Middleware, :clean_gitlab_redis_shared_state let(:enabled_path) { '/gitlab-org/gitlab-foss/noteable/issue/1/notes' } let(:endpoint) { 'issue_notes' } + describe '.skip!' do + it 'sets the skip header on the response' do + rsp = ActionDispatch::Response.new + rsp.set_header('Anything', 'Else') + + described_class.skip!(rsp) + + expect(rsp.headers.to_h).to eq(described_class::SKIP_HEADER_KEY => '1', 'Anything' => 'Else') + end + end + context 'when ETag caching is not enabled for current route' do let(:path) { '/gitlab-org/gitlab-foss/tree/master/noteable/issue/1/notes' } @@ -77,6 +88,28 @@ RSpec.describe Gitlab::EtagCaching::Middleware, :clean_gitlab_redis_shared_state end end + context 'when the matching route requests that the ETag is skipped' do + let(:path) { enabled_path } + let(:app) do + proc do |_env| + response = ActionDispatch::Response.new + + described_class.skip!(response) + + [200, response.headers.to_h, ''] + end + end + + it 'returns the correct headers' do + expect(app).to receive(:call).and_call_original + + _, headers, _ = middleware.call(build_request(path, if_none_match)) + + expect(headers).not_to have_key('ETag') + expect(headers).not_to have_key(described_class::SKIP_HEADER_KEY) + end + end + shared_examples 'sends a process_action.action_controller notification' do |status_code| let(:expected_items) do { diff --git a/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb b/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb index 6e671f127ef..1f7daaa308d 100644 --- a/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb +++ b/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb @@ -127,7 +127,7 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do end end - describe '.initialize_metrics', :prometheus, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/281164', type: :investigating } do + describe '.initialize_metrics', :prometheus do it "sets labels for http_requests_total" do expected_labels = [] diff --git a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb index ad106837c47..b99a5352717 100644 --- a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb +++ b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb @@ -119,6 +119,10 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do end context 'with SIDEKIQ_LOG_ARGUMENTS disabled' do + before do + stub_env('SIDEKIQ_LOG_ARGUMENTS', '0') + end + it 'logs start and end of job without args' do Timecop.freeze(timestamp) do expect(logger).to receive(:info).with(start_payload.except('args')).ordered @@ -150,8 +154,8 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do it 'logs with scheduling latency' do Timecop.freeze(timestamp) do - expect(logger).to receive(:info).with(start_payload.except('args')).ordered - expect(logger).to receive(:info).with(end_payload.except('args')).ordered + expect(logger).to receive(:info).with(start_payload).ordered + expect(logger).to receive(:info).with(end_payload).ordered expect(subject).to receive(:log_job_start).and_call_original expect(subject).to receive(:log_job_done).and_call_original @@ -173,12 +177,12 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do end let(:expected_end_payload) do - end_payload.except('args').merge(timing_data) + end_payload.merge(timing_data) end it 'logs with Gitaly and Rugged timing data' do Timecop.freeze(timestamp) do - expect(logger).to receive(:info).with(start_payload.except('args')).ordered + expect(logger).to receive(:info).with(start_payload).ordered expect(logger).to receive(:info).with(expected_end_payload).ordered subject.call(job, 'test_queue') do @@ -194,10 +198,10 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do allow(Process).to receive(:clock_gettime).and_call_original end - let(:expected_start_payload) { start_payload.except('args') } + let(:expected_start_payload) { start_payload } let(:expected_end_payload) do - end_payload.except('args').merge('cpu_s' => a_value >= 0) + end_payload.merge('cpu_s' => a_value >= 0) end let(:expected_end_payload_with_db) do @@ -228,10 +232,10 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do end context 'when there is extra metadata set for the done log' do - let(:expected_start_payload) { start_payload.except('args') } + let(:expected_start_payload) { start_payload } let(:expected_end_payload) do - end_payload.except('args').merge("#{ApplicationWorker::LOGGING_EXTRA_KEY}.key1" => 15, "#{ApplicationWorker::LOGGING_EXTRA_KEY}.key2" => 16) + end_payload.merge("#{ApplicationWorker::LOGGING_EXTRA_KEY}.key1" => 15, "#{ApplicationWorker::LOGGING_EXTRA_KEY}.key2" => 16) end it 'logs it in the done log' do diff --git a/spec/requests/projects/noteable_notes_spec.rb b/spec/requests/projects/noteable_notes_spec.rb new file mode 100644 index 00000000000..2bf1ffb2edc --- /dev/null +++ b/spec/requests/projects/noteable_notes_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Project noteable notes' do + describe '#index' do + let_it_be(:merge_request) { create(:merge_request) } + + let(:etag_store) { Gitlab::EtagCaching::Store.new } + let(:notes_path) { project_noteable_notes_path(project, target_type: merge_request.class.name.underscore, target_id: merge_request.id) } + let(:project) { merge_request.project } + let(:user) { project.owner } + + let(:response_etag) { response.headers['ETag'] } + let(:stored_etag) { "W/\"#{etag_store.get(notes_path)}\"" } + + before do + login_as(user) + end + + it 'does not set a Gitlab::EtagCaching ETag if there is a note' do + create(:note_on_merge_request, noteable: merge_request, project: merge_request.project) + + get notes_path + + expect(response).to have_gitlab_http_status(:ok) + + # Rack::ETag will set an etag based on the body digest, but that doesn't + # interfere with notes pagination + expect(response_etag).not_to eq(stored_etag) + end + + it 'sets a Gitlab::EtagCaching ETag if there is no note' do + get notes_path + + expect(response).to have_gitlab_http_status(:ok) + expect(response_etag).to eq(stored_etag) + end + end +end diff --git a/spec/serializers/diff_file_entity_spec.rb b/spec/serializers/diff_file_entity_spec.rb index bebe2e2dfb5..1b8456e5c49 100644 --- a/spec/serializers/diff_file_entity_spec.rb +++ b/spec/serializers/diff_file_entity_spec.rb @@ -69,4 +69,15 @@ RSpec.describe DiffFileEntity do end end end + + describe '#is_fully_expanded' do + context 'file with a conflict' do + let(:options) { { conflicts: { diff_file.new_path => double(diff_lines_for_serializer: []) } } } + + it 'returns false' do + expect(diff_file).not_to receive(:fully_expanded?) + expect(subject[:is_fully_expanded]).to eq(false) + end + end + end end diff --git a/spec/serializers/diffs_entity_spec.rb b/spec/serializers/diffs_entity_spec.rb index 5928a1c24b3..7569493573b 100644 --- a/spec/serializers/diffs_entity_spec.rb +++ b/spec/serializers/diffs_entity_spec.rb @@ -8,9 +8,12 @@ RSpec.describe DiffsEntity do let(:request) { EntityRequest.new(project: project, current_user: user) } let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) } let(:merge_request_diffs) { merge_request.merge_request_diffs } + let(:options) do + { request: request, merge_request: merge_request, merge_request_diffs: merge_request_diffs } + end let(:entity) do - described_class.new(merge_request_diffs.first.diffs, request: request, merge_request: merge_request, merge_request_diffs: merge_request_diffs) + described_class.new(merge_request_diffs.first.diffs, options) end context 'as json' do @@ -68,5 +71,50 @@ RSpec.describe DiffsEntity do end end end + + context 'when there are conflicts' do + let(:diff_files) { merge_request_diffs.first.diffs.diff_files } + let(:diff_file_with_conflict) { diff_files.to_a.last } + let(:diff_file_without_conflict) { diff_files.to_a[-2] } + + let(:resolvable_conflicts) { true } + let(:conflict_file) { double(our_path: diff_file_with_conflict.new_path) } + let(:conflicts) { double(conflicts: double(files: [conflict_file]), can_be_resolved_in_ui?: resolvable_conflicts) } + + let(:merge_ref_head_diff) { true } + let(:options) { super().merge(merge_ref_head_diff: merge_ref_head_diff) } + + before do + allow(MergeRequests::Conflicts::ListService).to receive(:new).and_return(conflicts) + end + + it 'conflicts are highlighted' do + expect(conflict_file).to receive(:diff_lines_for_serializer) + expect(diff_file_with_conflict).not_to receive(:diff_lines_for_serializer) + expect(diff_file_without_conflict).to receive(:diff_lines_for_serializer).twice # for highlighted_diff_lines and is_fully_expanded + + subject + end + + context 'merge ref head diff is not chosen to be displayed' do + let(:merge_ref_head_diff) { false } + + it 'conflicts are not calculated' do + expect(MergeRequests::Conflicts::ListService).not_to receive(:new) + end + end + + context 'when conflicts cannot be resolved' do + let(:resolvable_conflicts) { false } + + it 'conflicts are not highlighted' do + expect(conflict_file).not_to receive(:diff_lines_for_serializer) + expect(diff_file_with_conflict).to receive(:diff_lines_for_serializer).twice # for highlighted_diff_lines and is_fully_expanded + expect(diff_file_without_conflict).to receive(:diff_lines_for_serializer).twice # for highlighted_diff_lines and is_fully_expanded + + subject + end + end + end end end diff --git a/spec/serializers/paginated_diff_entity_spec.rb b/spec/serializers/paginated_diff_entity_spec.rb index 821ed34d3ec..551b392c9e9 100644 --- a/spec/serializers/paginated_diff_entity_spec.rb +++ b/spec/serializers/paginated_diff_entity_spec.rb @@ -31,4 +31,50 @@ RSpec.describe PaginatedDiffEntity do total_pages: 7 ) end + + context 'when there are conflicts' do + let(:diff_batch) { merge_request.merge_request_diff.diffs_in_batch(7, 3, diff_options: nil) } + let(:diff_files) { diff_batch.diff_files.to_a } + let(:diff_file_with_conflict) { diff_files.last } + let(:diff_file_without_conflict) { diff_files.first } + + let(:resolvable_conflicts) { true } + let(:conflict_file) { double(our_path: diff_file_with_conflict.new_path) } + let(:conflicts) { double(conflicts: double(files: [conflict_file]), can_be_resolved_in_ui?: resolvable_conflicts) } + + let(:merge_ref_head_diff) { true } + let(:options) { super().merge(merge_ref_head_diff: merge_ref_head_diff) } + + before do + allow(MergeRequests::Conflicts::ListService).to receive(:new).and_return(conflicts) + end + + it 'conflicts are highlighted' do + expect(conflict_file).to receive(:diff_lines_for_serializer) + expect(diff_file_with_conflict).not_to receive(:diff_lines_for_serializer) + expect(diff_file_without_conflict).to receive(:diff_lines_for_serializer).twice # for highlighted_diff_lines and is_fully_expanded + + subject + end + + context 'merge ref head diff is not chosen to be displayed' do + let(:merge_ref_head_diff) { false } + + it 'conflicts are not calculated' do + expect(MergeRequests::Conflicts::ListService).not_to receive(:new) + end + end + + context 'when conflicts cannot be resolved' do + let(:resolvable_conflicts) { false } + + it 'conflicts are not highlighted' do + expect(conflict_file).not_to receive(:diff_lines_for_serializer) + expect(diff_file_with_conflict).to receive(:diff_lines_for_serializer).twice # for highlighted_diff_lines and is_fully_expanded + expect(diff_file_without_conflict).to receive(:diff_lines_for_serializer).twice # for highlighted_diff_lines and is_fully_expanded + + subject + end + end + end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 3953193c3a7..65ab071f179 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -365,7 +365,7 @@ RSpec.configure do |config| end config.before(:example, :prometheus) do - matching_files = File.join(::Prometheus::Client.configuration.multiprocess_files_dir, "*.db") + matching_files = File.join(::Prometheus::Client.configuration.multiprocess_files_dir, "**/*.db") Dir[matching_files].map { |filename| File.delete(filename) if File.file?(filename) } Gitlab::Metrics.reset_registry! diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb index 1844e12ed7a..a1b4e6eee92 100644 --- a/spec/support/helpers/graphql_helpers.rb +++ b/spec/support/helpers/graphql_helpers.rb @@ -17,8 +17,8 @@ module GraphqlHelpers # ready, then the early return is returned instead. # # Then the resolve method is called. - def resolve(resolver_class, obj: nil, args: {}, ctx: {}, field: nil) - resolver = resolver_class.new(object: obj, context: ctx, field: field) + def resolve(resolver_class, args: {}, **resolver_args) + resolver = resolver_instance(resolver_class, **resolver_args) ready, early_return = sync_all { resolver.ready?(**args) } return early_return unless ready @@ -26,6 +26,15 @@ module GraphqlHelpers resolver.resolve(**args) end + def resolver_instance(resolver_class, obj: nil, ctx: {}, field: nil, schema: GitlabSchema) + if ctx.is_a?(Hash) + q = double('Query', schema: schema) + ctx = GraphQL::Query::Context.new(query: q, object: obj, values: ctx) + end + + resolver_class.new(object: obj, context: ctx, field: field) + end + # Eagerly run a loader's named resolver # (syncs any lazy values returned by resolve) def eager_resolve(resolver_class, **opts) diff --git a/spec/views/profiles/preferences/show.html.haml_spec.rb b/spec/views/profiles/preferences/show.html.haml_spec.rb index aab50209953..2fe941b9f14 100644 --- a/spec/views/profiles/preferences/show.html.haml_spec.rb +++ b/spec/views/profiles/preferences/show.html.haml_spec.rb @@ -68,61 +68,4 @@ RSpec.describe 'profiles/preferences/show' do expect(rendered).to have_css('#localization') end end - - context 'sourcegraph' do - def have_sourcegraph_field(*args) - have_field('user_sourcegraph_enabled', *args) - end - - def have_integrations_section - have_css('#integrations.profile-settings-sidebar', text: 'Integrations') - end - - before do - stub_feature_flags(sourcegraph: sourcegraph_feature) - stub_application_setting(sourcegraph_enabled: sourcegraph_enabled) - end - - context 'when not fully enabled' do - where(:feature, :admin_enabled) do - false | false - false | true - true | false - end - - with_them do - let(:sourcegraph_feature) { feature } - let(:sourcegraph_enabled) { admin_enabled } - - before do - render - end - - it 'does not display sourcegraph field' do - expect(rendered).not_to have_sourcegraph_field - end - - it 'does not display Integration Settings' do - expect(rendered).not_to have_integrations_section - end - end - end - - context 'when fully enabled' do - let(:sourcegraph_feature) { true } - let(:sourcegraph_enabled) { true } - - before do - render - end - - it 'displays the sourcegraph field' do - expect(rendered).to have_sourcegraph_field - end - - it 'displays the integrations section' do - expect(rendered).to have_integrations_section - end - end - end end