diff options
164 files changed, 2108 insertions, 1033 deletions
diff --git a/.gitlab/ci/reports.gitlab-ci.yml b/.gitlab/ci/reports.gitlab-ci.yml index 2197f916484..5622cd232ca 100644 --- a/.gitlab/ci/reports.gitlab-ci.yml +++ b/.gitlab/ci/reports.gitlab-ci.yml @@ -17,13 +17,23 @@ sast: variables: SAST_BRAKEMAN_LEVEL: 2 SAST_EXCLUDED_PATHS: qa,spec,doc + artifacts: + expire_in: 7 days + paths: + - gl-sast-report.json dependency_scanning: extends: .reports dast: - extends: .reports + extends: + - .reports + - .review-only stage: qa dependencies: ["review-deploy"] before_script: - export DAST_WEBSITE="$(cat review_app_url.txt)" + artifacts: + expire_in: 7 days + paths: + - gl-dast-report.json diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION index 943f9cbc4ec..27f9cd322bb 100644 --- a/GITLAB_PAGES_VERSION +++ b/GITLAB_PAGES_VERSION @@ -1 +1 @@ -1.7.1 +1.8.0 diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index 3b6825376ad..e5c15102d9b 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -8.8.0 +8.9.0 diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index 95e1e8af9b3..1d4a6e64f9d 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -111,12 +111,7 @@ export default { * @returns {Boolean|Undefined} */ canShowDate() { - return ( - this.model && - this.model.last_deployment && - this.model.last_deployment.deployable && - this.model.last_deployment.deployable !== undefined - ); + return this.model && this.model.last_deployment && this.model.last_deployment.deployed_at; }, /** @@ -124,14 +119,9 @@ export default { * * @returns {String} */ - createdDate() { - if ( - this.model && - this.model.last_deployment && - this.model.last_deployment.deployable && - this.model.last_deployment.deployable.created_at - ) { - return timeagoInstance.format(this.model.last_deployment.deployable.created_at); + deployedDate() { + if (this.canShowDate) { + return timeagoInstance.format(this.model.last_deployment.deployed_at); } return ''; }, @@ -547,7 +537,7 @@ export default { <div v-if="!model.isFolder" class="table-section section-10" role="gridcell"> <div role="rowheader" class="table-mobile-header">{{ s__('Environments|Updated') }}</div> <span v-if="canShowDate" class="environment-created-date-timeago table-mobile-content"> - {{ createdDate }} + {{ deployedDate }} </span> </div> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue index 8b356ee6e97..549324831e9 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue @@ -69,7 +69,11 @@ export default { :disabled="currentBranch && !currentBranch.can_push" :title="$options.currentBranchPermissionsTooltip" > - <span class="ide-radio-label" v-html="commitToCurrentBranchText"> </span> + <span + class="ide-radio-label" + data-qa-selector="commit_to_current_branch_radio" + v-html="commitToCurrentBranchText" + ></span> </radio-group> <radio-group :value="$options.commitToNewBranch" diff --git a/app/assets/stylesheets/csslab.scss b/app/assets/stylesheets/csslab.scss deleted file mode 100644 index 87c59cd42c0..00000000000 --- a/app/assets/stylesheets/csslab.scss +++ /dev/null @@ -1 +0,0 @@ -@import "@gitlab/csslab/dist/css/csslab-slim"; diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 821e6691fe4..69ef116043a 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -245,27 +245,3 @@ label { .input-group-text { max-height: $input-height; } - -.gl-form-checkbox { - align-items: baseline; - margin-right: 1rem; - margin-bottom: 0.25rem; - - .form-check-input { - margin-right: 0; - } - - .form-check-label { - padding-left: $gl-padding-8; - } - - &.form-check-inline .form-check-input { - align-self: flex-start; - height: 1.5 * $gl-font-size; - } - - .form-check-input:disabled, - .form-check-input:disabled ~ .form-check-label { - cursor: not-allowed; - } -} diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index c201605e83d..33caac4d725 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -2,7 +2,7 @@ * Apply Markup (Markdown/AsciiDoc) typography * */ -.md:not(.use-csslab) { +.md { color: $gl-text-color; word-wrap: break-word; @@ -384,8 +384,18 @@ @extend .fa-exclamation-circle; } } -} + .prometheus-graph-embed { + h3.popover-header { + /** Override <h3> .popover-header + * as embed metrics do not follow the same + * style as default md <h3> (which are deeply nested) + */ + margin: 0; + font-size: $gl-font-size-small; + } + } +} /** * Headers diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 9871771542d..7a3fd2adfbb 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -529,7 +529,7 @@ $award-emoji-width-xs: 90%; */ $search-input-border-color: rgba($blue-400, 0.8); $search-input-width: 200px; -$search-input-active-width: 320px; +$search-input-xl-width: 320px; $location-icon-color: #e7e9ed; /* diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index 58e46cfb70f..74380ec995a 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -48,6 +48,10 @@ input[type='checkbox']:hover { background-color ease-in-out $default-transition-duration, width ease-in-out $default-transition-duration; + @include media-breakpoint-up(xl) { + width: $search-input-xl-width; + } + &:hover { box-shadow: none; } @@ -116,7 +120,7 @@ input[type='checkbox']:hover { overflow: auto; @include media-breakpoint-up(xl) { - width: $search-input-active-width; + width: $search-input-xl-width; } } @@ -131,10 +135,6 @@ input[type='checkbox']:hover { border-color: $blue-300; box-shadow: none; - @include media-breakpoint-up(xl) { - width: $search-input-active-width; - } - .search-input-wrap { .search-icon, .clear-icon { diff --git a/app/controllers/boards/lists_controller.rb b/app/controllers/boards/lists_controller.rb index ccd02144671..08b4748d7e1 100644 --- a/app/controllers/boards/lists_controller.rb +++ b/app/controllers/boards/lists_controller.rb @@ -4,7 +4,7 @@ module Boards class ListsController < Boards::ApplicationController include BoardsResponses - before_action :authorize_admin_list, only: [:create, :update, :destroy, :generate] + before_action :authorize_admin_list, only: [:create, :destroy, :generate] before_action :authorize_read_list, only: [:index] skip_before_action :authenticate_user!, only: [:index] @@ -15,7 +15,7 @@ module Boards end def create - list = Boards::Lists::CreateService.new(board.parent, current_user, list_params).execute(board) + list = Boards::Lists::CreateService.new(board.parent, current_user, create_list_params).execute(board) if list.valid? render json: serialize_as_json(list) @@ -26,12 +26,13 @@ module Boards def update list = board.lists.movable.find(params[:id]) - service = Boards::Lists::MoveService.new(board_parent, current_user, move_params) + service = Boards::Lists::UpdateService.new(board_parent, current_user, update_list_params) + result = service.execute(list) - if service.execute(list) + if result[:status] == :success head :ok else - head :unprocessable_entity + head result[:http_status] end end @@ -50,7 +51,8 @@ module Boards service = Boards::Lists::GenerateService.new(board_parent, current_user) if service.execute(board) - render json: serialize_as_json(board.lists.movable) + lists = board.lists.movable.preload_associations(current_user) + render json: serialize_as_json(lists) else head :unprocessable_entity end @@ -62,12 +64,12 @@ module Boards %i[label_id] end - def list_params + def create_list_params params.require(:list).permit(list_creation_attrs) end - def move_params - params.require(:list).permit(:position) + def update_list_params + params.require(:list).permit(:collapsed, :position) end def serialize_as_json(resource) @@ -78,7 +80,9 @@ module Boards { only: [:id, :list_type, :position], methods: [:title], - label: true + label: true, + collapsed: true, + current_user: current_user } end end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index f4cc0a5851b..d492c5227cf 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -46,6 +46,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo @noteable = @merge_request @commits_count = @merge_request.commits_count @issuable_sidebar = serializer.represent(@merge_request, serializer: 'sidebar') + @current_user_data = UserSerializer.new(project: @project).represent(current_user, {}, MergeRequestUserEntity).to_json set_pipeline_variables diff --git a/app/finders/members_finder.rb b/app/finders/members_finder.rb index f730b015c0a..e8c7f9622a9 100644 --- a/app/finders/members_finder.rb +++ b/app/finders/members_finder.rb @@ -60,15 +60,32 @@ class MembersFinder # We're interested in a list of members without duplicates by user_id. # We prefer project members over group members, project members should go first. <<~SQL - SELECT DISTINCT ON (user_id, invite_email) member_union.* - FROM (#{union.to_sql}) AS member_union - ORDER BY user_id, - invite_email, - CASE - WHEN type = 'ProjectMember' THEN 1 - WHEN type = 'GroupMember' THEN 2 - ELSE 3 - END + SELECT DISTINCT ON (user_id, invite_email) #{member_columns} + FROM (#{union.to_sql}) AS #{member_union_table} + LEFT JOIN users on users.id = member_union.user_id + LEFT JOIN project_authorizations on project_authorizations.user_id = users.id + AND + project_authorizations.project_id = #{project.id} + ORDER BY user_id, + invite_email, + CASE + WHEN type = 'ProjectMember' THEN 1 + WHEN type = 'GroupMember' THEN 2 + ELSE 3 + END SQL end + + def member_union_table + 'member_union' + end + + def member_columns + Member.column_names.map do |column_name| + # fallback to members.access_level when project_authorizations.access_level is missing + next "COALESCE(#{ProjectAuthorization.table_name}.access_level, #{member_union_table}.access_level) access_level" if column_name == 'access_level' + + "#{member_union_table}.#{column_name}" + end.join(',') + end end diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb index f3a3203f7ad..542085ea307 100644 --- a/app/mailers/emails/issues.rb +++ b/app/mailers/emails/issues.rb @@ -11,7 +11,7 @@ module Emails def issue_due_email(recipient_id, issue_id, reason = nil) setup_issue_mail(issue_id, recipient_id) - mail_new_thread(@issue, issue_thread_options(@issue.author_id, recipient_id, reason)) + mail_answer_thread(@issue, issue_thread_options(@issue.author_id, recipient_id, reason)) end def new_mention_in_issue_email(recipient_id, issue_id, updated_by_user_id, reason = nil) diff --git a/app/models/list.rb b/app/models/list.rb index ccadd39bda2..ae7085f05a7 100644 --- a/app/models/list.rb +++ b/app/models/list.rb @@ -1,9 +1,11 @@ # frozen_string_literal: true class List < ApplicationRecord + include Importable + belongs_to :board belongs_to :label - include Importable + has_many :list_user_preferences enum list_type: { backlog: 0, label: 1, closed: 2, assignee: 3, milestone: 4 } @@ -16,9 +18,24 @@ class List < ApplicationRecord scope :destroyable, -> { where(list_type: list_types.slice(*destroyable_types).values) } scope :movable, -> { where(list_type: list_types.slice(*movable_types).values) } - scope :preload_associations, -> { preload(:board, :label) } + + scope :preload_associations, -> (user) do + preload(:board, label: :priorities) + .with_preferences_for(user) + end + scope :ordered, -> { order(:list_type, :position) } + # Loads list with preferences for given user + # if preferences exists for user or not + scope :with_preferences_for, -> (user) do + return unless user + + includes(:list_user_preferences).where(list_user_preferences: { user_id: [user.id, nil] }) + end + + alias_method :preferences, :list_user_preferences + class << self def destroyable_types [:label] @@ -29,6 +46,31 @@ class List < ApplicationRecord end end + def preferences_for(user) + return preferences.build unless user + + if preferences.loaded? + preloaded_preferences_for(user) + else + preferences.find_or_initialize_by(user: user) + end + end + + def preloaded_preferences_for(user) + user_preferences = + preferences.find do |preference| + preference.user_id == user.id + end + + user_preferences || preferences.build(user: user) + end + + def update_preferences_for(user, preferences = {}) + return unless user + + preferences_for(user).update(preferences) + end + def destroyable? self.class.destroyable_types.include?(list_type&.to_sym) end @@ -43,6 +85,14 @@ class List < ApplicationRecord def as_json(options = {}) super(options).tap do |json| + json[:collapsed] = false + + if options.key?(:collapsed) + preferences = preferences_for(options[:current_user]) + + json[:collapsed] = preferences.collapsed? + end + if options.key?(:label) json[:label] = label.as_json( project: board.project, diff --git a/app/models/list_user_preference.rb b/app/models/list_user_preference.rb new file mode 100644 index 00000000000..fe1cc7d5425 --- /dev/null +++ b/app/models/list_user_preference.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class ListUserPreference < ApplicationRecord + belongs_to :user + belongs_to :list + + validates :user, presence: true + validates :list, presence: true + validates :user_id, uniqueness: { scope: :list_id, message: "should have only one list preference per user" } +end diff --git a/app/models/project.rb b/app/models/project.rb index c67c5c7bc8c..8f568a5b840 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -61,6 +61,8 @@ class Project < ApplicationRecord delegate :feature_available?, :builds_enabled?, :wiki_enabled?, :merge_requests_enabled?, :issues_enabled?, :pages_enabled?, :public_pages?, + :merge_requests_access_level, :issues_access_level, :wiki_access_level, + :snippets_access_level, :builds_access_level, :repository_access_level, to: :project_feature, allow_nil: true delegate :base_dir, :disk_path, :ensure_storage_path_exists, to: :storage diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb index c9ee0653d86..41e63986286 100644 --- a/app/models/remote_mirror.rb +++ b/app/models/remote_mirror.rb @@ -200,6 +200,7 @@ class RemoteMirror < ApplicationRecord result.password = '*****' if result.password result.user = '*****' if result.user && result.user != 'git' # tokens or other data may be saved as user result.to_s + rescue URI::Error end def ensure_remote! diff --git a/app/serializers/merge_request_noteable_entity.rb b/app/serializers/merge_request_noteable_entity.rb new file mode 100644 index 00000000000..e22be6880bb --- /dev/null +++ b/app/serializers/merge_request_noteable_entity.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +class MergeRequestNoteableEntity < Grape::Entity + include RequestAwareEntity + + # Currently this attr is exposed to be used in app/assets/javascripts/notes/stores/getters.js + # in order to determine whether a noteable is an issue or an MR + expose :merge_params + + expose :state + expose :source_branch + expose :target_branch + expose :diff_head_sha + + expose :create_note_path do |merge_request| + project_notes_path(merge_request.project, target_type: 'merge_request', target_id: merge_request.id) + end + + expose :preview_note_path do |merge_request| + preview_markdown_path(merge_request.project, target_type: 'MergeRequest', target_id: merge_request.iid) + end + + expose :supports_suggestion?, as: :can_receive_suggestion + + expose :create_issue_to_resolve_discussions_path do |merge_request| + presenter(merge_request).create_issue_to_resolve_discussions_path + end + + expose :new_blob_path do |merge_request| + if presenter(merge_request).can_push_to_source_branch? + project_new_blob_path(merge_request.source_project, merge_request.source_branch) + end + end + + expose :current_user do + expose :can_create_note do |merge_request| + can?(current_user, :create_note, merge_request) + end + + expose :can_update do |merge_request| + can?(current_user, :update_merge_request, merge_request) + end + end + + private + + delegate :current_user, to: :request + + def presenter(merge_request) + @presenters ||= {} + @presenters[merge_request] ||= MergeRequestPresenter.new(merge_request, current_user: current_user) # rubocop: disable CodeReuse/Presenter + end +end diff --git a/app/serializers/merge_request_poll_widget_entity.rb b/app/serializers/merge_request_poll_widget_entity.rb index 65132b4b215..cd33ffa702a 100644 --- a/app/serializers/merge_request_poll_widget_entity.rb +++ b/app/serializers/merge_request_poll_widget_entity.rb @@ -65,8 +65,6 @@ class MergeRequestPollWidgetEntity < IssuableEntity end end - expose :supports_suggestion?, as: :can_receive_suggestion - expose :create_issue_to_resolve_discussions_path do |merge_request| presenter(merge_request).create_issue_to_resolve_discussions_path end @@ -84,17 +82,9 @@ class MergeRequestPollWidgetEntity < IssuableEntity presenter(merge_request).can_cherry_pick_on_current_merge_request? end - expose :can_create_note do |merge_request| - can?(current_user, :create_note, merge_request) - end - expose :can_create_issue do |merge_request| can?(current_user, :create_issue, merge_request.project) end - - expose :can_update do |merge_request| - can?(current_user, :update_merge_request, merge_request) - end end expose :can_push_to_source_branch do |merge_request| diff --git a/app/serializers/merge_request_serializer.rb b/app/serializers/merge_request_serializer.rb index bd2e682a122..aa67cd1f39e 100644 --- a/app/serializers/merge_request_serializer.rb +++ b/app/serializers/merge_request_serializer.rb @@ -13,6 +13,8 @@ class MergeRequestSerializer < BaseSerializer MergeRequestSidebarExtrasEntity when 'basic' MergeRequestBasicEntity + when 'noteable' + MergeRequestNoteableEntity else # fallback to widget for old poll requests without `serializer` set MergeRequestWidgetEntity diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb index c8088608cb0..2f2c42a7387 100644 --- a/app/serializers/merge_request_widget_entity.rb +++ b/app/serializers/merge_request_widget_entity.rb @@ -3,10 +3,6 @@ class MergeRequestWidgetEntity < Grape::Entity include RequestAwareEntity - # Currently this attr is exposed to be used in app/assets/javascripts/notes/stores/getters.js - # in order to determine whether a noteable is an issue or an MR - expose :merge_params - expose :source_project_full_path do |merge_request| merge_request.source_project&.full_path end @@ -35,18 +31,10 @@ class MergeRequestWidgetEntity < Grape::Entity cached_widget_project_json_merge_request_path(merge_request.target_project, merge_request, format: :json) end - expose :create_note_path do |merge_request| - project_notes_path(merge_request.project, target_type: 'merge_request', target_id: merge_request.id) - end - expose :commit_change_content_path do |merge_request| commit_change_content_project_merge_request_path(merge_request.project, merge_request) end - expose :preview_note_path do |merge_request| - preview_markdown_path(merge_request.project, target_type: 'MergeRequest', target_id: merge_request.iid) - end - expose :conflicts_docs_path do |merge_request| help_page_path('user/project/merge_requests/resolve_conflicts.md') end diff --git a/app/services/base_service.rb b/app/services/base_service.rb index 3e968c8f707..c39edd5c114 100644 --- a/app/services/base_service.rb +++ b/app/services/base_service.rb @@ -44,6 +44,10 @@ class BaseService model.errors.add(:visibility_level, "#{level_name} has been restricted by your GitLab administrator") end + def visibility_level + params[:visibility].is_a?(String) ? Gitlab::VisibilityLevel.level_value(params[:visibility]) : params[:visibility_level] + end + private def error(message, http_status = nil) diff --git a/app/services/boards/lists/list_service.rb b/app/services/boards/lists/list_service.rb index 5cf5f14a55b..1f20ec8df9e 100644 --- a/app/services/boards/lists/list_service.rb +++ b/app/services/boards/lists/list_service.rb @@ -6,7 +6,7 @@ module Boards def execute(board) board.lists.create(list_type: :backlog) unless board.lists.backlog.exists? - board.lists.preload_associations + board.lists.preload_associations(current_user) end end end diff --git a/app/services/boards/lists/update_service.rb b/app/services/boards/lists/update_service.rb new file mode 100644 index 00000000000..2ddeb6f0bd8 --- /dev/null +++ b/app/services/boards/lists/update_service.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +module Boards + module Lists + class UpdateService < Boards::BaseService + def execute(list) + return not_authorized if preferences? && !can_read?(list) + return not_authorized if position? && !can_admin?(list) + + if update_preferences(list) || update_position(list) + success(list: list) + else + error(list.errors.messages, 422) + end + end + + def update_preferences(list) + return unless preferences? + + list.update_preferences_for(current_user, preferences) + end + + def update_position(list) + return unless position? + + move_service = Boards::Lists::MoveService.new(parent, current_user, params) + + move_service.execute(list) + end + + def preferences + { collapsed: Gitlab::Utils.to_boolean(params[:collapsed]) } + end + + def not_authorized + error("Not authorized", 403) + end + + def preferences? + params.has_key?(:collapsed) + end + + def position? + params.has_key?(:position) + end + + def can_read?(list) + Ability.allowed?(current_user, :read_list, parent) + end + + def can_admin?(list) + Ability.allowed?(current_user, :admin_list, parent) + end + end + end +end diff --git a/app/services/create_snippet_service.rb b/app/services/create_snippet_service.rb index 6e5bf823cc7..0aa76df35ba 100644 --- a/app/services/create_snippet_service.rb +++ b/app/services/create_snippet_service.rb @@ -12,7 +12,7 @@ class CreateSnippetService < BaseService PersonalSnippet.new(params) end - unless Gitlab::VisibilityLevel.allowed_for?(current_user, params[:visibility_level]) + unless Gitlab::VisibilityLevel.allowed_for?(current_user, snippet.visibility_level) deny_visibility_level(snippet) return snippet end diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb index e78e5d5fc2c..1dd22d7a3ae 100644 --- a/app/services/groups/create_service.rb +++ b/app/services/groups/create_service.rb @@ -68,9 +68,5 @@ module Groups true end - - def visibility_level - params[:visibility].present? ? Gitlab::VisibilityLevel.level_value(params[:visibility]) : params[:visibility_level] - end end end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index ee7223d6349..1b48b20e28b 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -67,7 +67,7 @@ module SystemNoteService create_note(NoteSummary.new(noteable, project, author, body, action: 'assignee')) end - # Called when the assignees of an Issue is changed or removed + # Called when the assignees of an issuable is changed or removed # # issuable - Issuable object (responds to assignees) # project - Project owning noteable @@ -88,10 +88,12 @@ module SystemNoteService def change_issuable_assignees(issuable, project, author, old_assignees) unassigned_users = old_assignees - issuable.assignees added_users = issuable.assignees.to_a - old_assignees - text_parts = [] - text_parts << "assigned to #{added_users.map(&:to_reference).to_sentence}" if added_users.any? - text_parts << "unassigned #{unassigned_users.map(&:to_reference).to_sentence}" if unassigned_users.any? + + Gitlab::I18n.with_default_locale do + text_parts << "assigned to #{added_users.map(&:to_reference).to_sentence}" if added_users.any? + text_parts << "unassigned #{unassigned_users.map(&:to_reference).to_sentence}" if unassigned_users.any? + end body = text_parts.join(' and ') diff --git a/app/services/update_snippet_service.rb b/app/services/update_snippet_service.rb index 2969c360de5..a294812ef9e 100644 --- a/app/services/update_snippet_service.rb +++ b/app/services/update_snippet_service.rb @@ -12,7 +12,7 @@ class UpdateSnippetService < BaseService def execute # check that user is allowed to set specified visibility_level - new_visibility = params[:visibility_level] + new_visibility = visibility_level if new_visibility && new_visibility.to_i != snippet.visibility_level unless Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility) diff --git a/app/views/groups/_group_admin_settings.html.haml b/app/views/groups/_group_admin_settings.html.haml index 733cb36cc3d..c3c7d102b28 100644 --- a/app/views/groups/_group_admin_settings.html.haml +++ b/app/views/groups/_group_admin_settings.html.haml @@ -9,7 +9,7 @@ Allow projects within this group to use Git LFS = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') %br/ - %span.descr This setting can be overridden in each project. + %span This setting can be overridden in each project. .form-group.row .col-sm-2.col-form-label = f.label s_('ProjectCreationLevel|Allowed to create projects') diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml index c55730ccd31..4c88660ccb9 100644 --- a/app/views/groups/settings/_permissions.html.haml +++ b/app/views/groups/settings/_permissions.html.haml @@ -14,7 +14,7 @@ %span.d-block - group_link = link_to @group.name, group_path(@group) = s_('GroupSettings|Prevent sharing a project within %{group} with other groups').html_safe % { group: group_link } - %span.descr.text-muted= share_with_group_lock_help_text(@group) + %span.js-descr.text-muted= share_with_group_lock_help_text(@group) .form-group.append-bottom-default .form-check diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 271b73326fa..68abfd3f61f 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -36,7 +36,6 @@ = stylesheet_link_tag "print", media: "print" = stylesheet_link_tag "test", media: "all" if Rails.env.test? = stylesheet_link_tag 'performance_bar' if performance_bar_enabled? - = stylesheet_link_tag 'csslab' if Feature.enabled?(:csslab) = stylesheet_link_tag "highlight/themes/#{user_color_scheme}", media: "all" diff --git a/app/views/projects/_merge_request_merge_checks_settings.html.haml b/app/views/projects/_merge_request_merge_checks_settings.html.haml index c21d333f21a..d3fcb52422b 100644 --- a/app/views/projects/_merge_request_merge_checks_settings.html.haml +++ b/app/views/projects/_merge_request_merge_checks_settings.html.haml @@ -7,7 +7,7 @@ = form.check_box :only_allow_merge_if_pipeline_succeeds, class: 'form-check-input' = form.label :only_allow_merge_if_pipeline_succeeds, class: 'form-check-label' do = s_('ProjectSettings|Pipelines must succeed') - .descr.text-secondary + .text-secondary = s_('ProjectSettings|Pipelines need to be configured to enable this feature.') = link_to icon('question-circle'), help_page_path('ci/merge_request_pipelines/index.md', diff --git a/app/views/projects/_merge_request_merge_method_settings.html.haml b/app/views/projects/_merge_request_merge_method_settings.html.haml index 47c311f42d0..1be7f7bb418 100644 --- a/app/views/projects/_merge_request_merge_method_settings.html.haml +++ b/app/views/projects/_merge_request_merge_method_settings.html.haml @@ -7,14 +7,14 @@ = form.radio_button :merge_method, :merge, class: "js-merge-method-radio form-check-input" = label_tag :project_merge_method_merge, class: 'form-check-label' do = s_('ProjectSettings|Merge commit') - .descr.text-secondary + .text-secondary = s_('ProjectSettings|Every merge creates a merge commit') .form-check.mb-2 = form.radio_button :merge_method, :rebase_merge, class: "js-merge-method-radio form-check-input" = label_tag :project_merge_method_rebase_merge, class: 'form-check-label' do = s_('ProjectSettings|Merge commit with semi-linear history') - .descr.text-secondary + .text-secondary = s_('ProjectSettings|Every merge creates a merge commit') %br = s_('ProjectSettings|Fast-forward merges only') @@ -25,7 +25,7 @@ = form.radio_button :merge_method, :ff, class: "js-merge-method-radio qa-radio-button-merge-ff form-check-input" = label_tag :project_merge_method_ff, class: 'form-check-label' do = s_('ProjectSettings|Fast-forward merge') - .descr.text-secondary + .text-secondary = s_('ProjectSettings|No merge commits are created') %br = s_('ProjectSettings|Fast-forward merges only') diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml index 95c5eb32c7f..cf273aab108 100644 --- a/app/views/projects/blob/_blob.html.haml +++ b/app/views/projects/blob/_blob.html.haml @@ -9,6 +9,6 @@ = render "projects/blob/auxiliary_viewer", blob: blob #blob-content-holder.blob-content-holder - %article.file-holder{ class: ('use-csslab' if Feature.enabled?(:csslab)) } + %article.file-holder = render 'projects/blob/header', blob: blob = render 'projects/blob/content', blob: blob diff --git a/app/views/projects/blob/preview.html.haml b/app/views/projects/blob/preview.html.haml index 3e893343165..46e76e4d175 100644 --- a/app/views/projects/blob/preview.html.haml +++ b/app/views/projects/blob/preview.html.haml @@ -1,5 +1,5 @@ - if markup?(@blob.name) - .file-content.md.md-file{ class: ('use-csslab' if Feature.enabled?(:csslab)) } + .file-content.md.md-file = markup(@blob.name, @content) - else .diff-file diff --git a/app/views/projects/blob/viewers/_markup.html.haml b/app/views/projects/blob/viewers/_markup.html.haml index abc74b66e90..c71df29354b 100644 --- a/app/views/projects/blob/viewers/_markup.html.haml +++ b/app/views/projects/blob/viewers/_markup.html.haml @@ -1,4 +1,4 @@ - blob = viewer.blob - context = blob.respond_to?(:rendered_markup) ? { rendered: blob.rendered_markup } : {} -.file-content.md.md-file{ class: ('use-csslab' if Feature.enabled?(:csslab)) } +.file-content.md.md-file = markup(blob.name, blob.data, context) diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index a766dd51463..6c77036a85b 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -5,7 +5,7 @@ = render partial: 'signature', object: @commit.signature %strong #{ s_('CommitBoxTitle|Commit') } - %span.commit-sha= @commit.short_id + %span.commit-sha{ data: { qa_selector: 'commit_sha_content' } }= @commit.short_id = clipboard_button(text: @commit.id, title: _('Copy commit SHA to clipboard')) %span.d-none.d-sm-inline= _('authored') #{time_ago_with_tooltip(@commit.authored_date)} diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml index 752be02443c..ef2ab4c698e 100644 --- a/app/views/projects/deployments/_deployment.html.haml +++ b/app/views/projects/deployments/_deployment.html.haml @@ -22,7 +22,8 @@ .table-section.section-15{ role: 'gridcell' } .table-mobile-header{ role: 'rowheader' }= _("Created") - %span.table-mobile-content= time_ago_with_tooltip(deployment.created_at) + - if deployment.deployed_at + %span.table-mobile-content= time_ago_with_tooltip(deployment.deployed_at) .table-section.section-20.table-button-footer{ role: 'gridcell' } .btn-group.table-action-buttons diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml index af3bd8dcd69..ea166d622eb 100644 --- a/app/views/projects/merge_requests/show.html.haml +++ b/app/views/projects/merge_requests/show.html.haml @@ -6,6 +6,7 @@ - page_description @merge_request.description - page_card_attributes @merge_request.card_attributes - suggest_changes_help_path = help_page_path('user/discussions/index.md', anchor: 'suggest-changes') +- number_of_pipelines = @pipelines.size .merge-request{ data: { mr_action: j(params[:tab].presence || 'show'), url: merge_request_path(@merge_request, format: :json), project_path: project_path(@merge_request.project), lock_version: @merge_request.lock_version } } = render "projects/merge_requests/mr_title" @@ -41,11 +42,11 @@ = tab_link_for @merge_request, :commits do = _("Commits") %span.badge.badge-pill= @commits_count - - if @pipelines.any? + - if number_of_pipelines.nonzero? %li.pipelines-tab = tab_link_for @merge_request, :pipelines do = _("Pipelines") - %span.badge.badge-pill.js-pipelines-mr-count= @pipelines.size + %span.badge.badge-pill.js-pipelines-mr-count= number_of_pipelines %li.diffs-tab.qa-diffs-tab = tab_link_for @merge_request, :diffs do = _("Changes") @@ -63,21 +64,21 @@ %script.js-notes-data{ type: "application/json" }= initial_notes_data(true).to_json.html_safe .issuable-discussion.js-vue-notes-event #js-vue-mr-discussions{ data: { notes_data: notes_data(@merge_request).to_json, - noteable_data: serialize_issuable(@merge_request), + noteable_data: serialize_issuable(@merge_request, serializer: 'noteable'), noteable_type: 'MergeRequest', target_type: 'merge_request', help_page_path: suggest_changes_help_path, - current_user_data: UserSerializer.new(project: @project).represent(current_user, {}, MergeRequestUserEntity).to_json} } + current_user_data: @current_user_data} } #commits.commits.tab-pane -# This tab is always loaded via AJAX #pipelines.pipelines.tab-pane - - if @pipelines.any? + - if number_of_pipelines.nonzero? = render 'projects/commit/pipelines_list', disable_initialization: true, endpoint: pipelines_project_merge_request_path(@project, @merge_request) #js-diffs-app.diffs.tab-pane{ data: { "is-locked" => @merge_request.discussion_locked?, endpoint: diffs_project_merge_request_path(@project, @merge_request, 'json', request.query_parameters), help_page_path: suggest_changes_help_path, - current_user_data: UserSerializer.new(project: @project).represent(current_user, {}, MergeRequestUserEntity).to_json, + current_user_data: @current_user_data, project_path: project_path(@merge_request.project), changes_empty_state_illustration: image_path('illustrations/merge_request_changes_empty.svg'), is_fluid_layout: fluid_layout.to_s, diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index 5cc6b5a173b..e1797e6db2a 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -21,7 +21,7 @@ .form-actions - if @milestone.new_record? - = f.submit _('Create milestone'), class: 'btn-create btn qa-milestone-create-button' + = f.submit _('Create milestone'), class: 'btn-success btn qa-milestone-create-button' = link_to _('Cancel'), project_milestones_path(@project), class: 'btn btn-cancel' - else = f.submit _('Save changes'), class: 'btn-success btn' diff --git a/app/views/projects/mirrors/_mirror_repos.html.haml b/app/views/projects/mirrors/_mirror_repos.html.haml index 6f8a93fbcf5..84f0900d9c1 100644 --- a/app/views/projects/mirrors/_mirror_repos.html.haml +++ b/app/views/projects/mirrors/_mirror_repos.html.haml @@ -50,7 +50,7 @@ - @project.remote_mirrors.each_with_index do |mirror, index| - next if mirror.new_record? %tr.qa-mirrored-repository-row.rspec-mirrored-repository-row{ class: ('bg-secondary' if mirror.disabled?) } - %td.qa-mirror-repository-url= mirror.safe_url + %td.qa-mirror-repository-url= mirror.safe_url || _('Invalid URL') %td= _('Push') %td = mirror.last_update_started_at.present? ? time_ago_with_tooltip(mirror.last_update_started_at) : _('Never') diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml index 498a9744783..430d6071468 100644 --- a/app/views/projects/settings/ci_cd/_form.html.haml +++ b/app/views/projects/settings/ci_cd/_form.html.haml @@ -14,14 +14,14 @@ = f.label :build_allow_git_fetch_false, class: 'form-check-label' do %strong git clone %br - %span.descr + %span = _("Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job") .form-check = f.radio_button :build_allow_git_fetch, 'true', { class: 'form-check-input' } = f.label :build_allow_git_fetch_true, class: 'form-check-label' do %strong git fetch %br - %span.descr + %span = _("Faster as it re-uses the project workspace (falling back to clone if it doesn't exist)") %hr diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml index da48cb207a4..f495b4eaf30 100644 --- a/app/views/projects/snippets/show.html.haml +++ b/app/views/projects/snippets/show.html.haml @@ -6,7 +6,7 @@ = render 'shared/snippets/header' .project-snippets - %article.file-holder.snippet-file-content{ class: ('use-csslab' if Feature.enabled?(:csslab)) } + %article.file-holder.snippet-file-content = render 'shared/snippets/blob' .row-content-block.top-block.content-component-block diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml index c6197fe576e..51b7f2dd4b4 100644 --- a/app/views/projects/wikis/show.html.haml +++ b/app/views/projects/wikis/show.html.haml @@ -26,7 +26,7 @@ = (s_("WikiHistoricalPage|You can view the %{most_recent_link} or browse the %{history_link}.") % { most_recent_link: most_recent_link, history_link: history_link }).html_safe .prepend-top-default.append-bottom-default - .md.md-file{ class: ('use-csslab' if Feature.enabled?(:csslab)) } + .md.md-file = render_wiki_content(@page) = render 'sidebar' diff --git a/app/views/shared/empty_states/_profile_tabs.html.haml b/app/views/shared/empty_states/_profile_tabs.html.haml index 6da40e1b059..98a5a5953d0 100644 --- a/app/views/shared/empty_states/_profile_tabs.html.haml +++ b/app/views/shared/empty_states/_profile_tabs.html.haml @@ -12,7 +12,7 @@ %p= current_user_empty_message_description - if secondary_button_link.present? - = link_to secondary_button_label, secondary_button_link, class: 'btn btn-create btn-inverted' + = link_to secondary_button_label, secondary_button_link, class: 'btn btn-success btn-inverted' = link_to primary_button_label, primary_button_link, class: 'btn btn-success' - else diff --git a/app/views/shared/issuable/_close_reopen_button.html.haml b/app/views/shared/issuable/_close_reopen_button.html.haml index 4f6a71b6071..875cacd1f4f 100644 --- a/app/views/shared/issuable/_close_reopen_button.html.haml +++ b/app/views/shared/issuable/_close_reopen_button.html.haml @@ -9,7 +9,7 @@ class: "d-none d-sm-none d-md-block btn btn-grouped btn-close js-btn-issue-action #{issuable_button_visibility(issuable, true)}", title: "Close #{display_issuable_type}" - if can_reopen = link_to "Reopen #{display_issuable_type}", reopen_issuable_path(issuable), method: button_method, - class: "d-none d-sm-none d-md-block btn btn-grouped btn-reopen js-btn-issue-action #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}" + class: "d-none d-sm-none d-md-block btn btn-grouped btn-reopen js-btn-issue-action #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}", data: { qa_selector: 'reopen_issue_button' } - else - if can_update && !are_close_and_open_buttons_hidden = render 'shared/issuable/close_reopen_report_toggle', issuable: issuable diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml index 71123740ee4..93408e0bfc0 100644 --- a/app/views/shared/issuable/_nav.html.haml +++ b/app/views/shared/issuable/_nav.html.haml @@ -17,7 +17,7 @@ #{issuables_state_counter_text(type, :closed, display_count)} - else %li{ class: active_when(params[:state] == 'closed') }> - = link_to page_filter_path(state: 'closed'), id: 'state-closed', title: 'Filter by issues that are currently closed.', data: { state: 'closed' } do + = link_to page_filter_path(state: 'closed'), id: 'state-closed', title: 'Filter by issues that are currently closed.', data: { state: 'closed', qa_selector: 'closed_issues_link' } do #{issuables_state_counter_text(type, :closed, display_count)} = render 'shared/issuable/nav_links/all', page_context_word: page_context_word, counter: issuables_state_counter_text(type, :all, display_count) diff --git a/changelogs/unreleased/56130-operations-environments-shows-incorrect-deployment-date-for-manual-.yml b/changelogs/unreleased/56130-operations-environments-shows-incorrect-deployment-date-for-manual-.yml new file mode 100644 index 00000000000..92f25ac07e2 --- /dev/null +++ b/changelogs/unreleased/56130-operations-environments-shows-incorrect-deployment-date-for-manual-.yml @@ -0,0 +1,6 @@ +--- +title: Update the timestamp in Operations > Environments to show correct deployment + date for manual deploy jobs +merge_request: 32072 +author: +type: fixed diff --git a/changelogs/unreleased/62284-follow-up-from-resolve-api-to-get-all-project-group-members-returns-duplicates.yml b/changelogs/unreleased/62284-follow-up-from-resolve-api-to-get-all-project-group-members-returns-duplicates.yml new file mode 100644 index 00000000000..0c73f73c297 --- /dev/null +++ b/changelogs/unreleased/62284-follow-up-from-resolve-api-to-get-all-project-group-members-returns-duplicates.yml @@ -0,0 +1,5 @@ +--- +title: Uses projects_authorizations.access_level in MembersFinder +merge_request: 28887 +author: Jacopo Beschi @jacopo-beschi +type: fixed diff --git a/changelogs/unreleased/63262-notes-are-persisted-with-the-user-s-locale.yml b/changelogs/unreleased/63262-notes-are-persisted-with-the-user-s-locale.yml new file mode 100644 index 00000000000..e55beb9db09 --- /dev/null +++ b/changelogs/unreleased/63262-notes-are-persisted-with-the-user-s-locale.yml @@ -0,0 +1,5 @@ +--- +title: Do not translate system notes into author's language +merge_request: 32264 +author: +type: fixed diff --git a/changelogs/unreleased/65063-Embeded-metrics-inconsistent-styles.yml b/changelogs/unreleased/65063-Embeded-metrics-inconsistent-styles.yml new file mode 100644 index 00000000000..126cf29afc0 --- /dev/null +++ b/changelogs/unreleased/65063-Embeded-metrics-inconsistent-styles.yml @@ -0,0 +1,5 @@ +--- +title: Fixed embeded metrics tooltip inconsistent styling +merge_request: 31517 +author: +type: fixed diff --git a/changelogs/unreleased/66524-issue-due-notification-emails-are-threaded-incorrectly.yml b/changelogs/unreleased/66524-issue-due-notification-emails-are-threaded-incorrectly.yml new file mode 100644 index 00000000000..a6a0ba4b4f4 --- /dev/null +++ b/changelogs/unreleased/66524-issue-due-notification-emails-are-threaded-incorrectly.yml @@ -0,0 +1,5 @@ +--- +title: Fix issue due notification emails not being threaded correctly +merge_request: 32325 +author: +type: fixed diff --git a/changelogs/unreleased/bump-pages-1-8.yml b/changelogs/unreleased/bump-pages-1-8.yml new file mode 100644 index 00000000000..0201ff0fd5f --- /dev/null +++ b/changelogs/unreleased/bump-pages-1-8.yml @@ -0,0 +1,5 @@ +--- +title: Upgrade pages to 1.8.0 +merge_request: 32334 +author: +type: other diff --git a/changelogs/unreleased/ce-xanf-move-auto-merge-failed-to-jest.yml b/changelogs/unreleased/ce-xanf-move-auto-merge-failed-to-jest.yml new file mode 100644 index 00000000000..5a56a668c54 --- /dev/null +++ b/changelogs/unreleased/ce-xanf-move-auto-merge-failed-to-jest.yml @@ -0,0 +1,5 @@ +--- +title: Refactored Karma spec to Jest for mr_widget_auto_merge_failed +merge_request: 32282 +author: Illya Klymov +type: other diff --git a/changelogs/unreleased/cluster_deployments.yml b/changelogs/unreleased/cluster_deployments.yml new file mode 100644 index 00000000000..d854d16ea72 --- /dev/null +++ b/changelogs/unreleased/cluster_deployments.yml @@ -0,0 +1,5 @@ +--- +title: Add index to improve group cluster deployments query performance +merge_request: 31988 +author: +type: other diff --git a/changelogs/unreleased/fix-create-milestone-btn-success.yml b/changelogs/unreleased/fix-create-milestone-btn-success.yml new file mode 100644 index 00000000000..a1b1d305ce3 --- /dev/null +++ b/changelogs/unreleased/fix-create-milestone-btn-success.yml @@ -0,0 +1,5 @@ +--- +title: Fix style of secondary profile tab buttons. +merge_request: 32010 +author: Wolfgang Faust +type: fixed diff --git a/changelogs/unreleased/fix-search-input-dropdown.yml b/changelogs/unreleased/fix-search-input-dropdown.yml new file mode 100644 index 00000000000..a86f7eacfdb --- /dev/null +++ b/changelogs/unreleased/fix-search-input-dropdown.yml @@ -0,0 +1,5 @@ +--- +title: Fix top-nav search bar dropdown on xl displays +merge_request: 31864 +author: Kemais Ehlers +type: fixed diff --git a/changelogs/unreleased/georgekoltsov-13698-override-params.yml b/changelogs/unreleased/georgekoltsov-13698-override-params.yml new file mode 100644 index 00000000000..1bbde160e18 --- /dev/null +++ b/changelogs/unreleased/georgekoltsov-13698-override-params.yml @@ -0,0 +1,5 @@ +--- +title: Allow project feature permissions to be overridden during import with override_params +merge_request: 32348 +author: +type: fixed diff --git a/changelogs/unreleased/handle-invalid-mirror-url.yml b/changelogs/unreleased/handle-invalid-mirror-url.yml new file mode 100644 index 00000000000..c72707a2cad --- /dev/null +++ b/changelogs/unreleased/handle-invalid-mirror-url.yml @@ -0,0 +1,5 @@ +--- +title: Handle invalid mirror url +merge_request: 32353 +author: Lee Tickett +type: fixed diff --git a/changelogs/unreleased/id-optimize-sql-requests-mr-show.yml b/changelogs/unreleased/id-optimize-sql-requests-mr-show.yml new file mode 100644 index 00000000000..8b171a96316 --- /dev/null +++ b/changelogs/unreleased/id-optimize-sql-requests-mr-show.yml @@ -0,0 +1,5 @@ +--- +title: Reduce the number of SQL requests on MR-show +merge_request: 32192 +author: +type: performance diff --git a/changelogs/unreleased/issue_40630.yml b/changelogs/unreleased/issue_40630.yml new file mode 100644 index 00000000000..61cc773d5a9 --- /dev/null +++ b/changelogs/unreleased/issue_40630.yml @@ -0,0 +1,5 @@ +--- +title: Save collapsed option for board lists in database +merge_request: 31069 +author: +type: added diff --git a/changelogs/unreleased/new-project-milestone-primary-button.yml b/changelogs/unreleased/new-project-milestone-primary-button.yml new file mode 100644 index 00000000000..ac0305a2e21 --- /dev/null +++ b/changelogs/unreleased/new-project-milestone-primary-button.yml @@ -0,0 +1,5 @@ +--- +title: New project milestone primary button +merge_request: 32355 +author: Lee Tickett +type: fixed diff --git a/changelogs/unreleased/sh-fix-snippet-visibility-api.yml b/changelogs/unreleased/sh-fix-snippet-visibility-api.yml new file mode 100644 index 00000000000..5cfb9cdedc0 --- /dev/null +++ b/changelogs/unreleased/sh-fix-snippet-visibility-api.yml @@ -0,0 +1,5 @@ +--- +title: Fix snippets API not working with visibility level +merge_request: 32286 +author: +type: fixed diff --git a/changelogs/unreleased/sh-support-content-for-snippets-api.yml b/changelogs/unreleased/sh-support-content-for-snippets-api.yml new file mode 100644 index 00000000000..60a5d98da46 --- /dev/null +++ b/changelogs/unreleased/sh-support-content-for-snippets-api.yml @@ -0,0 +1,5 @@ +--- +title: Standardize use of `content` parameter in snippets API +merge_request: 32296 +author: +type: changed diff --git a/config/application.rb b/config/application.rb index 294ed470298..81889561473 100644 --- a/config/application.rb +++ b/config/application.rb @@ -165,7 +165,6 @@ module Gitlab config.assets.precompile << "locale/**/app.js" config.assets.precompile << "emoji_sprites.css" config.assets.precompile << "errors.css" - config.assets.precompile << "csslab.css" config.assets.precompile << "highlight/themes/*.css" diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 81433b620bc..4160f488a7a 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -515,7 +515,7 @@ Settings['sidekiq']['log_format'] ||= 'default' Settings['gitlab_shell'] ||= Settingslogic.new({}) Settings.gitlab_shell['path'] = Settings.absolute(Settings.gitlab_shell['path'] || Settings.gitlab['user_home'] + '/gitlab-shell/') Settings.gitlab_shell['hooks_path'] = :deprecated_use_gitlab_shell_path_instead -Settings.gitlab_shell['authorized_keys_file'] ||= nil +Settings.gitlab_shell['authorized_keys_file'] ||= File.join(Dir.home, '.ssh', 'authorized_keys') Settings.gitlab_shell['secret_file'] ||= Rails.root.join('.gitlab_shell_secret') Settings.gitlab_shell['receive_pack'] = true if Settings.gitlab_shell['receive_pack'].nil? Settings.gitlab_shell['upload_pack'] = true if Settings.gitlab_shell['upload_pack'].nil? diff --git a/config/initializers/peek.rb b/config/initializers/peek.rb index f9055285e5c..a3810be70b2 100644 --- a/config/initializers/peek.rb +++ b/config/initializers/peek.rb @@ -1,6 +1,7 @@ require 'peek/adapters/redis' Peek::Adapters::Redis.prepend ::Gitlab::PerformanceBar::RedisAdapterWhenPeekEnabled +Peek.singleton_class.prepend ::Gitlab::PerformanceBar::WithTopLevelWarnings Rails.application.config.peek.adapter = :redis, { client: ::Redis.new(Gitlab::Redis::Cache.params) } diff --git a/db/migrate/20190819131155_add_cluster_status_index_to_deployments.rb b/db/migrate/20190819131155_add_cluster_status_index_to_deployments.rb new file mode 100644 index 00000000000..bfa91e33558 --- /dev/null +++ b/db/migrate/20190819131155_add_cluster_status_index_to_deployments.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddClusterStatusIndexToDeployments < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index :deployments, [:cluster_id, :status] + end + + def down + remove_concurrent_index :deployments, [:cluster_id, :status] + end +end diff --git a/db/migrate/20190822181528_create_list_user_preferences.rb b/db/migrate/20190822181528_create_list_user_preferences.rb new file mode 100644 index 00000000000..a7993818b50 --- /dev/null +++ b/db/migrate/20190822181528_create_list_user_preferences.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class CreateListUserPreferences < ActiveRecord::Migration[5.2] + DOWNTIME = false + + def change + create_table :list_user_preferences do |t| + t.references :user, index: true, null: false, foreign_key: { on_delete: :cascade } + t.references :list, index: true, null: false, foreign_key: { on_delete: :cascade } + t.timestamps_with_timezone null: false + t.boolean :collapsed + end + + add_index :list_user_preferences, [:user_id, :list_id], unique: true + end +end diff --git a/db/migrate/20190826090628_remove_redundant_deployments_index.rb b/db/migrate/20190826090628_remove_redundant_deployments_index.rb new file mode 100644 index 00000000000..6b009c17d64 --- /dev/null +++ b/db/migrate/20190826090628_remove_redundant_deployments_index.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class RemoveRedundantDeploymentsIndex < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + def up + remove_concurrent_index :deployments, :cluster_id + end + + def down + add_concurrent_index :deployments, :cluster_id + end +end diff --git a/db/schema.rb b/db/schema.rb index 88a7ff77ab4..54774b0a65b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1146,7 +1146,7 @@ ActiveRecord::Schema.define(version: 2019_08_28_083843) do t.integer "status", limit: 2, null: false t.datetime_with_timezone "finished_at" t.integer "cluster_id" - t.index ["cluster_id"], name: "index_deployments_on_cluster_id" + t.index ["cluster_id", "status"], name: "index_deployments_on_cluster_id_and_status" t.index ["created_at"], name: "index_deployments_on_created_at" t.index ["deployable_type", "deployable_id"], name: "index_deployments_on_deployable_type_and_deployable_id" t.index ["environment_id", "id"], name: "index_deployments_on_environment_id_and_id" @@ -1922,6 +1922,17 @@ ActiveRecord::Schema.define(version: 2019_08_28_083843) do t.datetime "updated_at" end + create_table "list_user_preferences", force: :cascade do |t| + t.bigint "user_id", null: false + t.bigint "list_id", null: false + t.datetime_with_timezone "created_at", null: false + t.datetime_with_timezone "updated_at", null: false + t.boolean "collapsed" + t.index ["list_id"], name: "index_list_user_preferences_on_list_id" + t.index ["user_id", "list_id"], name: "index_list_user_preferences_on_user_id_and_list_id", unique: true + t.index ["user_id"], name: "index_list_user_preferences_on_user_id" + end + create_table "lists", id: :serial, force: :cascade do |t| t.integer "board_id", null: false t.integer "label_id" @@ -3880,6 +3891,8 @@ ActiveRecord::Schema.define(version: 2019_08_28_083843) do add_foreign_key "labels", "projects", name: "fk_7de4989a69", on_delete: :cascade add_foreign_key "lfs_file_locks", "projects", on_delete: :cascade add_foreign_key "lfs_file_locks", "users", on_delete: :cascade + add_foreign_key "list_user_preferences", "lists", on_delete: :cascade + add_foreign_key "list_user_preferences", "users", on_delete: :cascade add_foreign_key "lists", "boards", name: "fk_0d3f677137", on_delete: :cascade add_foreign_key "lists", "labels", name: "fk_7a5553d60f", on_delete: :cascade add_foreign_key "lists", "milestones", on_delete: :cascade diff --git a/doc/api/README.md b/doc/api/README.md index 33394d46a2d..036b46da6e5 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -396,7 +396,7 @@ GET /api/v4/projects/diaspora%2Fdiaspora NOTE: **Note:** A project's **path** is not necessarily the same as its **name**. A -project's path can found in the project's URL or in the project's settings +project's path can be found in the project's URL or in the project's settings under **General > Advanced > Change path**. ## Branches and tags name encoding @@ -422,7 +422,7 @@ We can call the API with `array` and `hash` types parameters as shown below: curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \ -d "import_sources[]=github" \ -d "import_sources[]=bitbucket" \ -"https://gitlab.example.com/api/v4/some_endpoint +https://gitlab.example.com/api/v4/some_endpoint ``` ### `hash` diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md index 1358851f3cd..391361a4b8f 100644 --- a/doc/development/ee_features.md +++ b/doc/development/ee_features.md @@ -1047,8 +1047,6 @@ code base. Examples of backports include the following: Here is a workflow to make sure those changes end up backported safely into CE too. -(This approach does not refer to changes introduced via [csslab](https://gitlab.com/gitlab-org/csslab/).) - 1. **Make your changes in the EE branch.** If possible, keep a separated commit (to be squashed) to help backporting and review. 1. **Open merge request to EE project.** 1. **Apply the changes you made to CE files in a branch of the CE project.** (Tip: Use `patch` with the diff from your commit in EE branch) diff --git a/doc/development/testing_guide/index.md b/doc/development/testing_guide/index.md index 96e8c30a679..173471e3af8 100644 --- a/doc/development/testing_guide/index.md +++ b/doc/development/testing_guide/index.md @@ -13,7 +13,7 @@ importance. GitLab is built on top of [Ruby on Rails](https://rubyonrails.org/), and we're using [RSpec] for all the backend tests, with [Capybara] for end-to-end integration testing. -On the frontend side, we're using [Karma] and [Jasmine] for JavaScript unit and +On the frontend side, we're using [Jest](https://jestjs.io/) and [Karma](http://karma-runner.github.io/)/[Jasmine](https://jasmine.github.io/) for JavaScript unit and integration testing. Following are two great articles that everyone should read to understand what @@ -64,6 +64,4 @@ Everything you should know about how to run end-to-end tests using [RSpec]: https://github.com/rspec/rspec-rails#feature-specs [Capybara]: https://github.com/teamcapybara/capybara -[Karma]: http://karma-runner.github.io/ -[Jasmine]: https://jasmine.github.io/ [gitlab-qa]: https://gitlab.com/gitlab-org/gitlab-qa diff --git a/doc/development/ux_guide/animation.md b/doc/development/ux_guide/animation.md index a998ab74a96..0f7a24042bb 100644 --- a/doc/development/ux_guide/animation.md +++ b/doc/development/ux_guide/animation.md @@ -1,5 +1,5 @@ --- -redirect_to: 'https://design.gitlab.com/product-foundations/motion' +redirect_to: 'https://design.gitlab.com/product-foundations/motion/' --- -The content of this document was moved into the [GitLab Design System](https://design.gitlab.com/product-foundations/motion). +The content of this document was moved into the [GitLab Design System](https://design.gitlab.com/product-foundations/motion/). diff --git a/doc/development/ux_guide/illustrations.md b/doc/development/ux_guide/illustrations.md index 3592d25c95d..815f870f8c5 100644 --- a/doc/development/ux_guide/illustrations.md +++ b/doc/development/ux_guide/illustrations.md @@ -1,5 +1,5 @@ --- -redirect_to: 'https://design.gitlab.com/product-foundations/illustration' +redirect_to: 'https://design.gitlab.com/product-foundations/illustration/' --- -The content of this document was moved into the [GitLab Design System](https://design.gitlab.com/product-foundations/illustration). +The content of this document was moved into the [GitLab Design System](https://design.gitlab.com/product-foundations/illustration/). diff --git a/doc/install/installation.md b/doc/install/installation.md index 2989a4edaf7..6039ddc45ae 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -611,7 +611,7 @@ You can specify a different Git repository by providing it as an extra parameter sudo -u git -H bundle exec rake "gitlab:gitaly:install[/home/git/gitaly,/home/git/repositories,https://example.com/gitaly.git]" RAILS_ENV=production ``` -Next, make sure gitaly configured: +Next, make sure that Gitaly is configured: ```sh # Restrict Gitaly socket access diff --git a/doc/legal/corporate_contributor_license_agreement.md b/doc/legal/corporate_contributor_license_agreement.md index 8c1f4a126b1..c8782a2cfc2 100644 --- a/doc/legal/corporate_contributor_license_agreement.md +++ b/doc/legal/corporate_contributor_license_agreement.md @@ -16,7 +16,7 @@ You accept and agree to the following terms and conditions for Your present and Subject to the terms and conditions of this Agreement, You hereby grant to GitLab B.V. and to recipients of software distributed by GitLab B.V. a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed. - You represent that You are legally entitled to grant the above license. You represent further that each of Your employees is authorized to submit Contributions on Your behalf, but excluding employees that are designated in writing by You as "Not authorized to submit Contributions on behalf of (name of Your corporation here)." Such designations of exclusion for unauthorized employees are to be submitted via email to legal@gitlab.com. It is Your responsibility to notify GitLab B.V. when any change is required to the list of designated employees excluded from submitting Contributions on Your behalf. Such notification should also be sent via email to legal@gitlab.com. + You represent that You are legally entitled to grant the above license. You represent further that each of Your employees is authorized to submit Contributions on Your behalf, but excluding employees that are designated in writing by You as "Not authorized to submit Contributions on behalf of (name of Your corporation here)." Such designations of exclusion for unauthorized employees are to be submitted via email to `legal@gitlab.com`. It is Your responsibility to notify GitLab B.V. when any change is required to the list of designated employees excluded from submitting Contributions on Your behalf. Such notification should also be sent via email to `legal@gitlab.com`. - **Contributions:** diff --git a/doc/migrate_ci_to_ce/README.md b/doc/migrate_ci_to_ce/README.md index 2b8379141c3..50e491f29a2 100644 --- a/doc/migrate_ci_to_ce/README.md +++ b/doc/migrate_ci_to_ce/README.md @@ -32,7 +32,7 @@ upgrade to 8.0 until you finish the migration procedure. ## Before upgrading -If you have GitLab CI installed using omnibus-gitlab packages but **you don't want to migrate your existing data**: +If you have GitLab CI installed using Omnibus GitLab packages but **you don't want to migrate your existing data**: ```bash mv /var/opt/gitlab/gitlab-ci/builds /var/opt/gitlab/gitlab-ci/builds.$(date +%s) @@ -353,7 +353,7 @@ Errno::EACCES: Permission denied @ rb_sysopen - config/secrets.yml This can happen if you are updating from versions prior to 7.13 straight to 8.0. The fix for this is to update to Omnibus 7.14 first and then update it to 8.0. -### Permission denied when accessing /var/opt/gitlab/gitlab-ci/builds +### Permission denied when accessing `/var/opt/gitlab/gitlab-ci/builds` To fix that issue you have to change builds/ folder permission before doing final backup: diff --git a/doc/push_rules/push_rules.md b/doc/push_rules/push_rules.md index b1754131e76..7455b577af7 100644 --- a/doc/push_rules/push_rules.md +++ b/doc/push_rules/push_rules.md @@ -10,7 +10,7 @@ regular expressions to reject pushes based on commit contents, branch names or f ## Overview GitLab already offers [protected branches][protected-branches], but there are -cases when you need some specific rules like preventing git tag removal or +cases when you need some specific rules like preventing Git tag removal or enforcing a special format for commit messages. Push rules are essentially [pre-receive Git hooks][hooks] that are easy to @@ -27,7 +27,7 @@ Every push rule could have its own use case, but let's consider some examples. Let's assume you have the following requirements for your workflow: - every commit should reference a Jira issue, for example: `Refactored css. Fixes JIRA-123.` -- users should not be able to remove git tags with `git push` +- users should not be able to remove Git tags with `git push` All you need to do is write a simple regular expression that requires the mention of a Jira issue in the commit message, like `JIRA\-\d+`. @@ -64,7 +64,7 @@ The following options are available. | Push rule | GitLab version | Description | | --------- | :------------: | ----------- | -| Removal of tags with `git push` | **Starter** 7.10 | Forbid users to remove git tags with `git push`. Tags will still be able to be deleted through the web UI. | +| Removal of tags with `git push` | **Starter** 7.10 | Forbid users to remove Git tags with `git push`. Tags will still be able to be deleted through the web UI. | | Check whether author is a GitLab user | **Starter** 7.10 | Restrict commits by author (email) to existing GitLab users. | | Committer restriction | **Premium** 10.2 | GitLab will reject any commit that was not committed by the current authenticated user | | Check whether commit is signed through GPG | **Premium** 10.1 | Reject commit when it is not signed through GPG. Read [signing commits with GPG][signing-commits]. | diff --git a/doc/security/information_exclusivity.md b/doc/security/information_exclusivity.md index 749ccf924b5..7c3d7284f25 100644 --- a/doc/security/information_exclusivity.md +++ b/doc/security/information_exclusivity.md @@ -15,7 +15,7 @@ another project that is under their control, or onto another server. Therefore, it is impossible to build access controls that prevent the intentional sharing of source code by users that have access to the source code. -This is an inherent feature of a DVCS. All git management systems have this +This is an inherent feature of a DVCS. All Git management systems have this limitation. You can take steps to prevent unintentional sharing and information diff --git a/doc/security/rack_attack.md b/doc/security/rack_attack.md index 4ad5fd0d16c..09d29bf3446 100644 --- a/doc/security/rack_attack.md +++ b/doc/security/rack_attack.md @@ -77,11 +77,12 @@ authentication requests were received in a 3-minute period from a single IP addr This applies only to Git requests and container registry (`/jwt/auth`) requests (combined). -This limit is reset by requests that authenticate successfully. For example, 29 -failed authentication requests followed by 1 successful request, followed by 29 -more failed authentication requests would not trigger a ban. +This limit: -JWT requests authenticated by gitlab-ci-token are excluded from this limit. +- Is reset by requests that authenticate successfully. For example, 29 + failed authentication requests followed by 1 successful request, followed by 29 + more failed authentication requests would not trigger a ban. +- Does not apply to JWT requests authenticated by `gitlab-ci-token`. No response headers are provided. diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index 19d8a6b5e2b..87dae0e7e1f 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -95,33 +95,45 @@ Auto DevOps. To make full use of Auto DevOps, you will need: -- **GitLab Runner** (needed for all stages) - Your Runner needs to be - configured to be able to run Docker. Generally this means using the - [Docker](https://docs.gitlab.com/runner/executors/docker.html) or [Kubernetes - executor](https://docs.gitlab.com/runner/executors/kubernetes.html), with +- **GitLab Runner** (for all stages) + + Your Runner needs to be configured to be able to run Docker. Generally this + means using the either the [Docker](https://docs.gitlab.com/runner/executors/docker.html) + or [Kubernetes](https://docs.gitlab.com/runner/executors/kubernetes.html) executors, with [privileged mode enabled](https://docs.gitlab.com/runner/executors/docker.html#use-docker-in-docker-with-privileged-mode). + The Runners do not need to be installed in the Kubernetes cluster, but the Kubernetes executor is easy to use and is automatically autoscaling. Docker-based Runners can be configured to autoscale as well, using [Docker - Machine](https://docs.gitlab.com/runner/install/autoscaling.html). Runners - should be registered as [shared Runners](../../ci/runners/README.md#registering-a-shared-runner) + Machine](https://docs.gitlab.com/runner/install/autoscaling.html). + + Runners should be registered as [shared Runners](../../ci/runners/README.md#registering-a-shared-runner) for the entire GitLab instance, or [specific Runners](../../ci/runners/README.md#registering-a-specific-runner) that are assigned to specific projects. -- **Base domain** (needed for Auto Review Apps and Auto Deploy) - You will need - a domain configured with wildcard DNS which is going to be used by all of your - Auto DevOps applications. [Read the specifics](#auto-devops-base-domain). -- **Kubernetes** (needed for Auto Review Apps, Auto Deploy, and Auto Monitoring) - - To enable deployments, you will need Kubernetes 1.5+. You will need a [Kubernetes cluster][kubernetes-clusters] - for the project. - - **A load balancer** - You can use NGINX ingress by deploying it to your - Kubernetes cluster using the - [`nginx-ingress`](https://github.com/kubernetes/charts/tree/master/stable/nginx-ingress) - Helm chart. -- **Prometheus** (needed for Auto Monitoring) - To enable Auto Monitoring, you +- **Base domain** (for Auto Review Apps and Auto Deploy) + + You will need a domain configured with wildcard DNS which is going to be used + by all of your Auto DevOps applications. + + Read the [specifics](#auto-devops-base-domain). +- **Kubernetes** (for Auto Review Apps, Auto Deploy, and Auto Monitoring) + + To enable deployments, you will need: + + - Kubernetes 1.5+. + - A [Kubernetes cluster][kubernetes-clusters] for the project. + - A load balancer. You can use NGINX ingress by deploying it to your + Kubernetes cluster by either: + - Using the [`nginx-ingress`](https://github.com/kubernetes/charts/tree/master/stable/nginx-ingress) Helm chart. + - Installing the Ingress [GitLab Managed App](../../user/clusters/applications.md#ingress). +- **Prometheus** (for Auto Monitoring) + + To enable Auto Monitoring, you will need Prometheus installed somewhere (inside or outside your cluster) and configured to scrape your Kubernetes cluster. To get response metrics (in addition to system metrics), you need to [configure Prometheus to monitor NGINX](../../user/project/integrations/prometheus_library/nginx_ingress.md#configuring-nginx-ingress-monitoring). + The [Prometheus service](../../user/project/integrations/prometheus.md) integration needs to be enabled for the project, or enabled as a [default service template](../../user/project/integrations/services_templates.md) diff --git a/doc/topics/git/partial_clone.md b/doc/topics/git/partial_clone.md index ea4223355d8..c9a5430b2c6 100644 --- a/doc/topics/git/partial_clone.md +++ b/doc/topics/git/partial_clone.md @@ -112,7 +112,7 @@ enabled on the Git server: git init # Add the remote - git remote add origin git@gitlab.com/example/jumbo-repo + git remote add origin <url> # Enable partial clone support for the remote git config --local extensions.partialClone origin diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md index 6a41c6aec32..3c9fcbf7c7d 100644 --- a/doc/update/patch_versions.md +++ b/doc/update/patch_versions.md @@ -64,7 +64,7 @@ sudo -u git -H bundle exec rake gettext:po_to_json RAILS_ENV=production sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile cache:clear RAILS_ENV=production NODE_ENV=production NODE_OPTIONS="--max_old_space_size=4096" ``` -### 4. Update gitlab-workhorse to the corresponding version +### 4. Update GitLab Workhorse to the corresponding version ```bash cd /home/git/gitlab @@ -80,7 +80,7 @@ cd /home/git/gitlab sudo -u git -H bundle exec rake "gitlab:gitaly:install[/home/git/gitaly,/home/git/repositories]" RAILS_ENV=production ``` -### 6. Update gitlab-shell to the corresponding version +### 6. Update GitLab Shell to the corresponding version ```bash cd /home/git/gitlab-shell @@ -90,7 +90,7 @@ sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION) -b v$(</h sudo -u git -H sh -c 'if [ -x bin/compile ]; then bin/compile; fi' ``` -### 7. Update gitlab-pages to the corresponding version (skip if not using pages) +### 7. Update GitLab Pages to the corresponding version (skip if not using pages) ```bash cd /home/git/gitlab-pages diff --git a/doc/update/upgrading_from_ce_to_ee.md b/doc/update/upgrading_from_ce_to_ee.md index 49e2fb2568e..9ac73725f87 100644 --- a/doc/update/upgrading_from_ce_to_ee.md +++ b/doc/update/upgrading_from_ce_to_ee.md @@ -17,7 +17,7 @@ GitLab edition you are using (Community or Enterprise), see the This guide assumes you have a correctly configured and tested installation of GitLab Community Edition. If you run into any trouble or if you have any -questions please contact us at [support@gitlab.com]. +questions please contact us at `support@gitlab.com`. In all examples, replace `EE_BRANCH` with the Enterprise Edition branch for the version you are using, and `CE_BRANCH` with the Community Edition branch. @@ -131,5 +131,4 @@ Example: Additional instructions here. --> -[support@gitlab.com]: mailto:support@gitlab.com [old-ee-upgrade-docs]: https://gitlab.com/gitlab-org/gitlab-ee/tree/11-8-stable-ee/doc/update diff --git a/doc/update/upgrading_from_source.md b/doc/update/upgrading_from_source.md index f6b64dcf694..890a5ec8698 100644 --- a/doc/update/upgrading_from_source.md +++ b/doc/update/upgrading_from_source.md @@ -118,7 +118,7 @@ sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/ rm go1.11.10.linux-amd64.tar.gz ``` -### 6. Update git +### 6. Update Git NOTE: **Note:** GitLab 11.11 and higher only supports Git 2.21.x and newer, and @@ -186,7 +186,7 @@ cd /home/git/gitlab sudo -u git -H git checkout BRANCH-ee ``` -### 8. Update gitlab-shell +### 8. Update GitLab Shell ```bash cd /home/git/gitlab-shell @@ -196,9 +196,9 @@ sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION) sudo -u git -H bin/compile ``` -### 9. Update gitlab-workhorse +### 9. Update GitLab Workhorse -Install and compile gitlab-workhorse. GitLab-Workhorse uses +Install and compile GitLab Workhorse. GitLab Workhorse uses [GNU Make](https://www.gnu.org/software/make/). If you are not using Linux you may have to run `gmake` instead of `make` below. @@ -222,11 +222,11 @@ sudo -u git -H git checkout v$(</home/git/gitlab/GITALY_SERVER_VERSION) sudo -u git -H make ``` -### 11. Update gitlab-pages +### 11. Update GitLab Pages #### Only needed if you use GitLab Pages -Install and compile gitlab-pages. GitLab-Pages uses +Install and compile GitLab Pages. GitLab Pages uses [GNU Make](https://www.gnu.org/software/make/). If you are not using Linux you may have to run `gmake` instead of `make` below. @@ -273,8 +273,8 @@ longer handles setting it. If you are using Apache instead of NGINX please see the updated [Apache templates]. Also note that because Apache does not support upstreams behind Unix sockets you -will need to let gitlab-workhorse listen on a TCP port. You can do this -via [/etc/default/gitlab]. +will need to let GitLab Workhorse listen on a TCP port. You can do this +via [`/etc/default/gitlab`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/support/init.d/gitlab.default.example#L38). #### SMTP configuration @@ -404,4 +404,3 @@ If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of [gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/support/init.d/gitlab.default.example [smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/initializers/smtp_settings.rb.sample#L13 [Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache -[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/support/init.d/gitlab.default.example#L38 diff --git a/doc/update/upgrading_postgresql_using_slony.md b/doc/update/upgrading_postgresql_using_slony.md index 835166837a8..cf3a389b149 100644 --- a/doc/update/upgrading_postgresql_using_slony.md +++ b/doc/update/upgrading_postgresql_using_slony.md @@ -77,7 +77,7 @@ make make install ``` -This assumes you have installed GitLab into /opt/gitlab. +This assumes you have installed GitLab into `/opt/gitlab`. To test if Slony is installed properly, run the following commands: diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md index 8f1048260f2..548e029f81f 100644 --- a/doc/user/gitlab_com/index.md +++ b/doc/user/gitlab_com/index.md @@ -320,11 +320,12 @@ authentication requests were received in a 3-minute period from a single IP addr This applies only to Git requests and container registry (`/jwt/auth`) requests (combined). -This limit is reset by requests that authenticate successfully. For example, 29 -failed authentication requests followed by 1 successful request, followed by 29 -more failed authentication requests would not trigger a ban. +This limit: -JWT requests authenticated by gitlab-ci-token are excluded from this limit. +- Is reset by requests that authenticate successfully. For example, 29 + failed authentication requests followed by 1 successful request, followed by 29 + more failed authentication requests would not trigger a ban. +- Does not apply to JWT requests authenticated by `gitlab-ci-token`. No response headers are provided. @@ -336,7 +337,7 @@ GitLab.com does not currently use these settings. In addition to the GitLab Enterprise Edition Omnibus install, GitLab.com uses the following applications and settings to achieve scale. All settings are -located publicly available [chef cookbooks](https://gitlab.com/gitlab-cookbooks). +publicly available at [chef cookbooks](https://gitlab.com/gitlab-cookbooks). ### ELK diff --git a/doc/user/markdown.md b/doc/user/markdown.md index 840e1856dd9..edf2fedab3c 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -1148,7 +1148,7 @@ GFM will autolink almost any URL you put into your text: - https://google.com/ - ftp://ftp.us.debian.org/debian/ - smb://foo/bar/baz -- irc://irc.freenode.net/gitlab +- irc://irc.freenode.net/ - http://localhost:3000 ``` @@ -1156,7 +1156,7 @@ GFM will autolink almost any URL you put into your text: - <https://google.com/> - <ftp://ftp.us.debian.org/debian/> - <smb://foo/bar/baz> -- <irc://irc.freenode.net/gitlab> +- <irc://irc.freenode.net/> - <http://localhost:3000> ### Lists diff --git a/doc/user/project/import/tfvc.md b/doc/user/project/import/tfvc.md index 375522b77d0..9b148224e10 100644 --- a/doc/user/project/import/tfvc.md +++ b/doc/user/project/import/tfvc.md @@ -6,7 +6,7 @@ type: concepts Team Foundation Server (TFS), renamed [Azure DevOps Server](https://azure.microsoft.com/en-us/services/devops/server/) in 2019, is a set of tools developed by Microsoft which also includes -[Team Foundation Version Control](https://docs.microsoft.com/en-us/azure/devops/repos/tfvc/overview) +[Team Foundation Version Control](https://docs.microsoft.com/en-us/azure/devops/repos/tfvc/overview?view=azure-devops) (TFVC), a centralized version control system similar to Git. In this document, we focus on the TFVC to Git migration. diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md index 84adb9637fc..ed0458ebc0f 100644 --- a/doc/user/project/integrations/webhooks.md +++ b/doc/user/project/integrations/webhooks.md @@ -1181,20 +1181,20 @@ X-Gitlab-Event: Job Hook ```json { - "object_kind": "job", + "object_kind": "build", "ref": "gitlab-script-trigger", "tag": false, "before_sha": "2293ada6b400935a1378653304eaf6221e0fdb8f", "sha": "2293ada6b400935a1378653304eaf6221e0fdb8f", - "job_id": 1977, - "job_name": "test", - "job_stage": "test", - "job_status": "created", - "job_started_at": null, - "job_finished_at": null, - "job_duration": null, - "job_allow_failure": false, - "job_failure_reason": "script_failure", + "build_id": 1977, + "build_name": "test", + "build_stage": "test", + "build_status": "created", + "build_started_at": null, + "build_finished_at": null, + "build_duration": null, + "build_allow_failure": false, + "build_failure_reason": "script_failure", "project_id": 380, "project_name": "gitlab-org/gitlab-test", "user": { diff --git a/doc/user/project/merge_requests/img/approvals_premium_project_edit_v12_3.png b/doc/user/project/merge_requests/img/approvals_premium_project_edit_v12_3.png Binary files differindex 32b9a3b9ce4..bbb131e86e9 100644 --- a/doc/user/project/merge_requests/img/approvals_premium_project_edit_v12_3.png +++ b/doc/user/project/merge_requests/img/approvals_premium_project_edit_v12_3.png diff --git a/doc/user/project/operations/tracing.md b/doc/user/project/operations/tracing.md index b92d2e49839..3fb3be3c21f 100644 --- a/doc/user/project/operations/tracing.md +++ b/doc/user/project/operations/tracing.md @@ -17,8 +17,8 @@ systems. ### Deploying Jaeger To learn more about deploying Jaeger, read the official -[Getting Started documentation](https://www.jaegertracing.io/docs/1.13/getting-started/). -There is an easy to use [all-in-one Docker image](https://www.jaegertracing.io/docs/1.13/getting-started/#AllinoneDockerimage), +[Getting Started documentation](https://www.jaegertracing.io/docs/latest/getting-started/). +There is an easy to use [all-in-one Docker image](https://www.jaegertracing.io/docs/latest/getting-started/#AllinoneDockerimage), as well as deployment options for [Kubernetes](https://github.com/jaegertracing/jaeger-kubernetes) and [OpenShift](https://github.com/jaegertracing/jaeger-openshift). @@ -27,7 +27,7 @@ and [OpenShift](https://github.com/jaegertracing/jaeger-openshift). GitLab provides an easy way to open the Jaeger UI from within your project: 1. [Set up Jaeger](#deploying-jaeger) and configure your application using one of the - [client libraries](https://www.jaegertracing.io/docs/1.13/client-libraries/). + [client libraries](https://www.jaegertracing.io/docs/latest/client-libraries/). 1. Navigate to your project's **Settings > Operations** and provide the Jaeger URL. 1. Click **Save changes** for the changes to take effect. 1. You can now visit **Operations > Tracing** in your project's sidebar and diff --git a/doc/user/project/repository/index.md b/doc/user/project/repository/index.md index bd966185c94..a838f06b2fd 100644 --- a/doc/user/project/repository/index.md +++ b/doc/user/project/repository/index.md @@ -73,7 +73,7 @@ according to the markup language. | [Markdown](../../markdown.md) | `mdown`, `mkd`, `mkdn`, `md`, `markdown` | | [reStructuredText](http://docutils.sourceforge.net/rst.html) | `rst` | | [AsciiDoc](../../asciidoc.md) | `adoc`, `ad`, `asciidoc` | -| [Textile](https://txstyle.org/) | `textile` | +| [Textile](https://textile-lang.com/) | `textile` | | [rdoc](http://rdoc.sourceforge.net/doc/index.html) | `rdoc` | | [Orgmode](https://orgmode.org/) | `org` | | [creole](http://www.wikicreole.org/) | `creole` | diff --git a/doc/user/reserved_names.md b/doc/user/reserved_names.md index 68532ccee65..45ece5f048e 100644 --- a/doc/user/reserved_names.md +++ b/doc/user/reserved_names.md @@ -14,27 +14,27 @@ For a list of words that are not allowed to be used as group or project names, s It is currently not possible to create a project with the following names: -- \- -- badges -- blame -- blob -- builds -- commits -- create -- create_dir -- edit -- environments/folders -- files -- find_file -- gitlab-lfs/objects -- info/lfs/objects -- new -- preview -- raw -- refs -- tree -- update -- wikis +- `\-` +- `badges` +- `blame` +- `blob` +- `builds` +- `commits` +- `create` +- `create_dir` +- `edit` +- `environments/folders` +- `files` +- `find_file` +- `gitlab-lfs/objects` +- `info/lfs/objects` +- `new` +- `preview` +- `raw` +- `refs` +- `tree` +- `update` +- `wikis` ## Reserved group names diff --git a/doc/workflow/git_annex.md b/doc/workflow/git_annex.md index cdc38a5b7eb..c3865aa953b 100644 --- a/doc/workflow/git_annex.md +++ b/doc/workflow/git_annex.md @@ -2,7 +2,7 @@ > **Warning:** GitLab has [completely removed][deprecate-annex-issue] in GitLab 9.0 (2017/03/22). -Read through the [migration guide from git-annex to git-lfs][guide]. +Read through the [migration guide from git-annex to Git LFS][guide]. The biggest limitation of Git, compared to some older centralized version control systems, has been the maximum size of the repositories. @@ -51,7 +51,7 @@ sudo yum install epel-release && sudo yum install git-annex ### Configuration for Omnibus packages -For omnibus-gitlab packages, only one configuration setting is needed. +For Omnibus GitLab packages, only one configuration setting is needed. The Omnibus package will internally set the correct options in all locations. 1. In `/etc/gitlab/gitlab.rb` add the following line: @@ -67,7 +67,7 @@ The Omnibus package will internally set the correct options in all locations. There are 2 settings to enable git-annex on your GitLab server. One is located in `config/gitlab.yml` of the GitLab repository and the other -one is located in `config.yml` of gitlab-shell. +one is located in `config.yml` of GitLab Shell. 1. In `config/gitlab.yml` add or edit the following lines: @@ -76,7 +76,7 @@ one is located in `config.yml` of gitlab-shell. git_annex_enabled: true ``` -1. In `config.yml` of gitlab-shell add or edit the following lines: +1. In `config.yml` of GitLab Shell add or edit the following lines: ```yaml git_annex_enabled: true @@ -184,7 +184,7 @@ access files of projects you have access to (developer, maintainer, or owner rol Internally GitLab uses [GitLab Shell] to handle SSH access and this was a great integration point for `git-annex`. -There is a setting in gitlab-shell so you can disable GitLab Annex support +There is a setting in GitLab Shell so you can disable GitLab Annex support if you want to. ## Troubleshooting tips @@ -200,7 +200,7 @@ searching for your distribution. Although there is no general guide for `git-annex` errors, there are a few tips on how to go around the warnings. -### git-annex-shell: Not a git-annex or gcrypt repository +### `git-annex-shell: Not a git-annex or gcrypt repository` This warning can appear on the initial `git annex sync --content` and is caused by differences in `git-annex-shell`. You can read more about it diff --git a/doc/workflow/releases.md b/doc/workflow/releases.md index 00cddda24a4..1fd63a556c6 100644 --- a/doc/workflow/releases.md +++ b/doc/workflow/releases.md @@ -3,20 +3,20 @@ NOTE: In GitLab 11.7, we introduced the full fledged [Releases](../user/project/releases/index.md) feature. You can still create release notes on this page, but the new method is preferred. -You can add release notes to any git tag using the notes feature. Release notes +You can add release notes to any Git tag using the notes feature. Release notes behave like any other markdown form in GitLab so you can write text and drag-n-drop files to it. Release notes are stored in GitLab's database. There are several ways to add release notes: -- In the interface, when you create a new git tag -- In the interface, by adding a note to an existing git tag +- In the interface, when you create a new Git tag +- In the interface, by adding a note to an existing Git tag - Using the GitLab API ## New tag page with release notes text area ![new_tag](releases/new_tag.png) -## Tags page with button to add or edit release notes for existing git tag +## Tags page with button to add or edit release notes for existing Git tag ![tags](releases/tags.png) diff --git a/doc/workflow/time_tracking.md b/doc/workflow/time_tracking.md index fad853f8a44..3d9f015b1fe 100644 --- a/doc/workflow/time_tracking.md +++ b/doc/workflow/time_tracking.md @@ -84,4 +84,4 @@ With this option enabled, `75h` is displayed instead of `1w 4d 3h`. ## Other interesting links -- [Time Tracking landing page on about.gitlab.com](https://about.gitlab.com/solutions/time-tracking/) +- [Time Tracking landing page in the GitLab handbook](https://about.gitlab.com/solutions/time-tracking/) diff --git a/doc/workflow/timezone.md b/doc/workflow/timezone.md index 60a4d0f19de..4101f891484 100644 --- a/doc/workflow/timezone.md +++ b/doc/workflow/timezone.md @@ -17,7 +17,7 @@ For Omnibus installations, run `gitlab-rake time:zones:all`. NOTE: **Note:** Currently, this rake task does not list timezones in TZInfo format required by GitLab Omnibus during a reconfigure: [#58672](https://gitlab.com/gitlab-org/gitlab-ce/issues/58672). -## Changing time zone in omnibus installations +## Changing time zone in Omnibus installations GitLab defaults its time zone to UTC. It has a global timezone configuration parameter in `/etc/gitlab/gitlab.rb`. diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 6f86adf6a5c..88be76d3c78 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -1705,6 +1705,18 @@ module API class ClusterGroup < Cluster expose :group, using: Entities::BasicGroupDetails end + + module InternalPostReceive + class Message < Grape::Entity + expose :message + expose :type + end + + class Response < Grape::Entity + expose :messages, using: Message + expose :reference_counter_decreased + end + end end end diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb index 9afe6c5b027..6b438235258 100644 --- a/lib/api/helpers/internal_helpers.rb +++ b/lib/api/helpers/internal_helpers.rb @@ -44,8 +44,6 @@ module API end def process_mr_push_options(push_options, project, user, changes) - output = {} - Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/61359') service = ::MergeRequests::PushOptionsHandlerService.new( @@ -56,15 +54,13 @@ module API ).execute if service.errors.present? - output[:warnings] = push_options_warning(service.errors.join("\n\n")) + push_options_warning(service.errors.join("\n\n")) end - - output end def push_options_warning(warning) options = Array.wrap(params[:push_options]).map { |p| "'#{p}'" }.join(' ') - "Error encountered with push options #{options}: #{warning}" + "WARNINGS:\nError encountered with push options #{options}: #{warning}" end def redis_ping diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 224aaaaf006..088ea5bd79a 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -256,25 +256,26 @@ module API post '/post_receive' do status 200 - output = {} # Messages to gitlab-shell + response = Gitlab::InternalPostReceive::Response.new user = identify(params[:identifier]) project = Gitlab::GlRepository.parse(params[:gl_repository]).first push_options = Gitlab::PushOptions.new(params[:push_options]) + response.reference_counter_decreased = Gitlab::ReferenceCounter.new(params[:gl_repository]).decrease + PostReceive.perform_async(params[:gl_repository], params[:identifier], params[:changes], push_options.as_json) mr_options = push_options.get(:merge_request) - output.merge!(process_mr_push_options(mr_options, project, user, params[:changes])) if mr_options.present? + if mr_options.present? + message = process_mr_push_options(mr_options, project, user, params[:changes]) + response.add_alert_message(message) + end broadcast_message = BroadcastMessage.current&.last&.message - reference_counter_decreased = Gitlab::ReferenceCounter.new(params[:gl_repository]).decrease + response.add_alert_message(broadcast_message) - output.merge!( - broadcast_message: broadcast_message, - reference_counter_decreased: reference_counter_decreased, - merge_request_urls: merge_request_urls - ) + response.add_merge_request_urls(merge_request_urls) # A user is not guaranteed to be returned; an orphaned write deploy # key could be used @@ -282,11 +283,11 @@ module API redirect_message = Gitlab::Checks::ProjectMoved.fetch_message(user.id, project.id) project_created_message = Gitlab::Checks::ProjectCreated.fetch_message(user.id, project.id) - output[:redirected_message] = redirect_message if redirect_message - output[:project_created_message] = project_created_message if project_created_message + response.add_basic_message(redirect_message) + response.add_basic_message(project_created_message) end - output + present response, with: Entities::InternalPostReceive::Response end end end diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb index a607df411a6..b4545295d54 100644 --- a/lib/api/project_snippets.rb +++ b/lib/api/project_snippets.rb @@ -51,16 +51,18 @@ module API params do requires :title, type: String, desc: 'The title of the snippet' requires :file_name, type: String, desc: 'The file name of the snippet' - requires :code, type: String, allow_blank: false, desc: 'The content of the snippet' + optional :code, type: String, allow_blank: false, desc: 'The content of the snippet (deprecated in favor of "content")' + optional :content, type: String, allow_blank: false, desc: 'The content of the snippet' optional :description, type: String, desc: 'The description of a snippet' requires :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The visibility of the snippet' + mutually_exclusive :code, :content end post ":id/snippets" do authorize! :create_project_snippet, user_project - snippet_params = declared_params.merge(request: request, api: true) - snippet_params[:content] = snippet_params.delete(:code) + snippet_params = declared_params(include_missing: false).merge(request: request, api: true) + snippet_params[:content] = snippet_params.delete(:code) if snippet_params[:code].present? snippet = CreateSnippetService.new(user_project, current_user, snippet_params).execute @@ -80,12 +82,14 @@ module API requires :snippet_id, type: Integer, desc: 'The ID of a project snippet' optional :title, type: String, desc: 'The title of the snippet' optional :file_name, type: String, desc: 'The file name of the snippet' - optional :code, type: String, allow_blank: false, desc: 'The content of the snippet' + optional :code, type: String, allow_blank: false, desc: 'The content of the snippet (deprecated in favor of "content")' + optional :content, type: String, allow_blank: false, desc: 'The content of the snippet' optional :description, type: String, desc: 'The description of a snippet' optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The visibility of the snippet' - at_least_one_of :title, :file_name, :code, :visibility_level + at_least_one_of :title, :file_name, :code, :content, :visibility_level + mutually_exclusive :code, :content end # rubocop: disable CodeReuse/ActiveRecord put ":id/snippets/:snippet_id" do diff --git a/lib/gitlab/authorized_keys.rb b/lib/gitlab/authorized_keys.rb index 3fe72f5fd43..820a78b653c 100644 --- a/lib/gitlab/authorized_keys.rb +++ b/lib/gitlab/authorized_keys.rb @@ -13,6 +13,24 @@ module Gitlab @logger = logger end + # Checks if the file is accessible or not + # + # @return [Boolean] + def accessible? + open_authorized_keys_file('r') { true } + rescue Errno::ENOENT, Errno::EACCES + false + end + + # Creates the authorized_keys file if it doesn't exist + # + # @return [Boolean] + def create + open_authorized_keys_file(File::CREAT) { true } + rescue Errno::EACCES + false + end + # Add id and its key to the authorized_keys file # # @param [String] id identifier of key prefixed by `key-` @@ -102,10 +120,14 @@ module Gitlab [] end + def file + @file ||= Gitlab.config.gitlab_shell.authorized_keys_file + end + private def lock(timeout = 10) - File.open("#{authorized_keys_file}.lock", "w+") do |f| + File.open("#{file}.lock", "w+") do |f| f.flock File::LOCK_EX Timeout.timeout(timeout) { yield } ensure @@ -114,7 +136,7 @@ module Gitlab end def open_authorized_keys_file(mode) - File.open(authorized_keys_file, mode, 0o600) do |file| + File.open(file, mode, 0o600) do |file| file.chmod(0o600) yield file end @@ -141,9 +163,5 @@ module Gitlab def strip(key) key.split(/[ ]+/)[0, 2].join(' ') end - - def authorized_keys_file - Gitlab.config.gitlab_shell.authorized_keys_file - end end end diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb index 201db9fec26..d65c0d3e78d 100644 --- a/lib/gitlab/gitaly_client.rb +++ b/lib/gitlab/gitaly_client.rb @@ -50,7 +50,7 @@ module Gitlab def self.interceptors return [] unless Labkit::Tracing.enabled? - [Labkit::Tracing::GRPCInterceptor.instance] + [Labkit::Tracing::GRPC::ClientInterceptor.instance] end private_class_method :interceptors diff --git a/lib/gitlab/internal_post_receive/response.rb b/lib/gitlab/internal_post_receive/response.rb new file mode 100644 index 00000000000..7e7ec2aa45c --- /dev/null +++ b/lib/gitlab/internal_post_receive/response.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module Gitlab + module InternalPostReceive + class Response + attr_accessor :reference_counter_decreased + attr_reader :messages + + Message = Struct.new(:message, :type) do + def self.basic(text) + new(text, :basic) + end + + def self.alert(text) + new(text, :alert) + end + end + + def initialize + @messages = [] + @reference_counter_decreased = false + end + + def add_merge_request_urls(urls_data) + urls_data.each do |url_data| + add_merge_request_url(url_data) + end + end + + def add_merge_request_url(url_data) + message = if url_data[:new_merge_request] + "To create a merge request for #{url_data[:branch_name]}, visit:" + else + "View merge request for #{url_data[:branch_name]}:" + end + + message += "\n #{url_data[:url]}" + + add_basic_message(message) + end + + def add_basic_message(text) + @messages << Message.basic(text) if text.present? + end + + def add_alert_message(text) + @messages.unshift(Message.alert(text)) if text.present? + end + end + end +end diff --git a/lib/gitlab/performance_bar/with_top_level_warnings.rb b/lib/gitlab/performance_bar/with_top_level_warnings.rb new file mode 100644 index 00000000000..fb5c5c5959d --- /dev/null +++ b/lib/gitlab/performance_bar/with_top_level_warnings.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Gitlab + module PerformanceBar + module WithTopLevelWarnings + def results + results = super + + results.merge(has_warnings: has_warnings?(results)) + end + + def has_warnings?(results) + results[:data].any? do |_, value| + value[:warnings].present? + end + end + end + end +end diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb index 0fa17b3f559..9e813968093 100644 --- a/lib/gitlab/shell.rb +++ b/lib/gitlab/shell.rb @@ -165,16 +165,7 @@ module Gitlab def add_key(key_id, key_content) return unless self.authorized_keys_enabled? - if shell_out_for_gitlab_keys? - gitlab_shell_fast_execute([ - gitlab_shell_keys_path, - 'add-key', - key_id, - strip_key(key_content) - ]) - else - gitlab_authorized_keys.add_key(key_id, key_content) - end + gitlab_authorized_keys.add_key(key_id, key_content) end # Batch-add keys to authorized_keys @@ -184,19 +175,7 @@ module Gitlab def batch_add_keys(keys) return unless self.authorized_keys_enabled? - if shell_out_for_gitlab_keys? - begin - IO.popen("#{gitlab_shell_keys_path} batch-add-keys", 'w') do |io| - add_keys_to_io(keys, io) - end - - $?.success? - rescue Error - false - end - else - gitlab_authorized_keys.batch_add_keys(keys) - end + gitlab_authorized_keys.batch_add_keys(keys) end # Remove ssh key from authorized_keys @@ -207,11 +186,7 @@ module Gitlab def remove_key(id, _ = nil) return unless self.authorized_keys_enabled? - if shell_out_for_gitlab_keys? - gitlab_shell_fast_execute([gitlab_shell_keys_path, 'rm-key', id]) - else - gitlab_authorized_keys.rm_key(id) - end + gitlab_authorized_keys.rm_key(id) end # Remove all ssh keys from gitlab shell @@ -222,11 +197,7 @@ module Gitlab def remove_all_keys return unless self.authorized_keys_enabled? - if shell_out_for_gitlab_keys? - gitlab_shell_fast_execute([gitlab_shell_keys_path, 'clear']) - else - gitlab_authorized_keys.clear - end + gitlab_authorized_keys.clear end # Remove ssh keys from gitlab shell that are not in the DB @@ -341,14 +312,6 @@ module Gitlab File.join(Gitlab.config.repositories.storages[storage].legacy_disk_path, dir_name) end - def gitlab_shell_projects_path - File.join(gitlab_shell_path, 'bin', 'gitlab-projects') - end - - def gitlab_shell_keys_path - File.join(gitlab_shell_path, 'bin', 'gitlab-keys') - end - def authorized_keys_enabled? # Return true if nil to ensure the authorized_keys methods work while # fixing the authorized_keys file during migration. @@ -359,35 +322,6 @@ module Gitlab private - def shell_out_for_gitlab_keys? - Gitlab.config.gitlab_shell.authorized_keys_file.blank? - end - - def gitlab_shell_fast_execute(cmd) - output, status = gitlab_shell_fast_execute_helper(cmd) - - return true if status.zero? - - Rails.logger.error("gitlab-shell failed with error #{status}: #{output}") # rubocop:disable Gitlab/RailsLogger - false - end - - def gitlab_shell_fast_execute_raise_error(cmd, vars = {}) - output, status = gitlab_shell_fast_execute_helper(cmd, vars) - - raise Error, output unless status.zero? - - true - end - - def gitlab_shell_fast_execute_helper(cmd, vars = {}) - vars.merge!(ENV.to_h.slice(*GITLAB_SHELL_ENV_VARS)) - - # Don't pass along the entire parent environment to prevent gitlab-shell - # from wasting I/O by searching through GEM_PATH - Bundler.with_original_env { Popen.popen(cmd, nil, vars) } - end - def git_timeout Gitlab.config.gitlab_shell.git_timeout end @@ -407,16 +341,8 @@ module Gitlab def batch_read_key_ids(batch_size: 100, &block) return unless self.authorized_keys_enabled? - if shell_out_for_gitlab_keys? - IO.popen("#{gitlab_shell_keys_path} list-key-ids") do |key_id_stream| - key_id_stream.lazy.each_slice(batch_size) do |lines| - yield(lines.map { |l| l.chomp.to_i }) - end - end - else - gitlab_authorized_keys.list_key_ids.lazy.each_slice(batch_size) do |key_ids| - yield(key_ids) - end + gitlab_authorized_keys.list_key_ids.lazy.each_slice(batch_size) do |key_ids| + yield(key_ids) end end diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb index 3b77fe838ae..29087d26007 100644 --- a/lib/gitlab/workhorse.rb +++ b/lib/gitlab/workhorse.rb @@ -34,7 +34,8 @@ module Gitlab GitConfigOptions: [], GitalyServer: { address: Gitlab::GitalyClient.address(project.repository_storage), - token: Gitlab::GitalyClient.token(project.repository_storage) + token: Gitlab::GitalyClient.token(project.repository_storage), + features: Feature::Gitaly.server_feature_flags } } @@ -250,7 +251,8 @@ module Gitlab def gitaly_server_hash(repository) { address: Gitlab::GitalyClient.address(repository.project.repository_storage), - token: Gitlab::GitalyClient.token(repository.project.repository_storage) + token: Gitlab::GitalyClient.token(repository.project.repository_storage), + features: Feature::Gitaly.server_feature_flags } end diff --git a/lib/peek/views/active_record.rb b/lib/peek/views/active_record.rb index 2d78818630d..a35783c1971 100644 --- a/lib/peek/views/active_record.rb +++ b/lib/peek/views/active_record.rb @@ -3,6 +3,24 @@ module Peek module Views class ActiveRecord < DetailedView + DEFAULT_THRESHOLDS = { + calls: 100, + duration: 3, + individual_call: 1 + }.freeze + + THRESHOLDS = { + production: { + calls: 100, + duration: 15, + individual_call: 5 + } + }.freeze + + def self.thresholds + @thresholds ||= THRESHOLDS.fetch(Rails.env.to_sym, DEFAULT_THRESHOLDS) + end + private def setup_subscribers diff --git a/lib/peek/views/detailed_view.rb b/lib/peek/views/detailed_view.rb index f4ca1cb5075..4f3eddaf11b 100644 --- a/lib/peek/views/detailed_view.rb +++ b/lib/peek/views/detailed_view.rb @@ -3,11 +3,16 @@ module Peek module Views class DetailedView < View + def self.thresholds + {} + end + def results { - duration: formatted_duration, + duration: format_duration(duration), calls: calls, - details: details + details: details, + warnings: warnings } end @@ -18,30 +23,48 @@ module Peek private def duration - detail_store.map { |entry| entry[:duration] }.sum # rubocop:disable CodeReuse/ActiveRecord + detail_store.map { |entry| entry[:duration] }.sum * 1000 # rubocop:disable CodeReuse/ActiveRecord end def calls detail_store.count end + def details + call_details + .sort { |a, b| b[:duration] <=> a[:duration] } + .map(&method(:format_call_details)) + end + + def warnings + [ + warning_for(calls, self.class.thresholds[:calls], label: "#{key} calls"), + warning_for(duration, self.class.thresholds[:duration], label: "#{key} duration") + ].flatten.compact + end + def call_details detail_store end def format_call_details(call) - call.merge(duration: (call[:duration] * 1000).round(3)) - end + duration = (call[:duration] * 1000).round(3) - def details - call_details - .sort { |a, b| b[:duration] <=> a[:duration] } - .map(&method(:format_call_details)) + call.merge(duration: duration, + warnings: warning_for(duration, self.class.thresholds[:individual_call])) end - def formatted_duration - ms = duration * 1000 + def warning_for(actual, threshold, label: nil) + if threshold && actual > threshold + prefix = "#{label}: " if label + + ["#{prefix}#{actual} over #{threshold}"] + else + [] + end + end + def format_duration(ms) if ms >= 1000 "%.2fms" % ms else diff --git a/lib/peek/views/gitaly.rb b/lib/peek/views/gitaly.rb index 6ad6ddfd89d..f669feae254 100644 --- a/lib/peek/views/gitaly.rb +++ b/lib/peek/views/gitaly.rb @@ -3,6 +3,24 @@ module Peek module Views class Gitaly < DetailedView + DEFAULT_THRESHOLDS = { + calls: 30, + duration: 1, + individual_call: 0.5 + }.freeze + + THRESHOLDS = { + production: { + calls: 30, + duration: 1, + individual_call: 0.5 + } + }.freeze + + def self.thresholds + @thresholds ||= THRESHOLDS.fetch(Rails.env.to_sym, DEFAULT_THRESHOLDS) + end + private def duration diff --git a/lib/peek/views/rugged.rb b/lib/peek/views/rugged.rb index 18b3f422852..3ed54a010f8 100644 --- a/lib/peek/views/rugged.rb +++ b/lib/peek/views/rugged.rb @@ -12,7 +12,7 @@ module Peek private def duration - ::Gitlab::RuggedInstrumentation.query_time + ::Gitlab::RuggedInstrumentation.query_time_ms end def calls diff --git a/lib/system_check/app/authorized_keys_permission_check.rb b/lib/system_check/app/authorized_keys_permission_check.rb new file mode 100644 index 00000000000..1246a6875a3 --- /dev/null +++ b/lib/system_check/app/authorized_keys_permission_check.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module SystemCheck + module App + class AuthorizedKeysPermissionCheck < SystemCheck::BaseCheck + set_name 'Is authorized keys file accessible?' + set_skip_reason 'skipped (authorized keys not enabled)' + + def skip? + !authorized_keys_enabled? + end + + def check? + authorized_keys.accessible? + end + + def repair! + authorized_keys.create + end + + def show_error + try_fixing_it([ + "sudo chmod 700 #{File.dirname(authorized_keys.file)}", + "touch #{authorized_keys.file}", + "sudo chmod 600 #{authorized_keys.file}" + ]) + fix_and_rerun + end + + private + + def authorized_keys_enabled? + Gitlab::CurrentSettings.current_application_settings.authorized_keys_enabled + end + + def authorized_keys + @authorized_keys ||= Gitlab::AuthorizedKeys.new + end + end + end +end diff --git a/lib/system_check/rake_task/app_task.rb b/lib/system_check/rake_task/app_task.rb index cc32feb8604..e98cee510ff 100644 --- a/lib/system_check/rake_task/app_task.rb +++ b/lib/system_check/rake_task/app_task.rb @@ -30,7 +30,8 @@ module SystemCheck SystemCheck::App::RubyVersionCheck, SystemCheck::App::GitVersionCheck, SystemCheck::App::GitUserDefaultSSHConfigCheck, - SystemCheck::App::ActiveUsersCheck + SystemCheck::App::ActiveUsersCheck, + SystemCheck::App::AuthorizedKeysPermissionCheck ] end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 12138d2db3a..51e00d6e0d2 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -6110,6 +6110,9 @@ msgstr "" msgid "Invalid Login or password" msgstr "" +msgid "Invalid URL" +msgstr "" + msgid "Invalid date" msgstr "" diff --git a/package.json b/package.json index 3d785b305f1..c3168fc9071 100644 --- a/package.json +++ b/package.json @@ -36,9 +36,8 @@ "@babel/plugin-syntax-dynamic-import": "^7.2.0", "@babel/plugin-syntax-import-meta": "^7.2.0", "@babel/preset-env": "^7.5.5", - "@gitlab/csslab": "^1.9.0", - "@gitlab/svgs": "^1.70.0", - "@gitlab/ui": "5.19.0", + "@gitlab/svgs": "^1.71.0", + "@gitlab/ui": "5.20.1", "@gitlab/visual-review-tools": "^1.0.0", "apollo-cache-inmemory": "^1.5.1", "apollo-client": "^2.5.1", diff --git a/qa/qa/page/project/commit/show.rb b/qa/qa/page/project/commit/show.rb index 9770b8a657c..ba09dd1b92a 100644 --- a/qa/qa/page/project/commit/show.rb +++ b/qa/qa/page/project/commit/show.rb @@ -9,6 +9,7 @@ module QA element :options_button element :email_patches element :plain_diff + element :commit_sha_content end def select_email_patches @@ -20,6 +21,10 @@ module QA click_element :options_button click_element :plain_diff end + + def commit_sha + find_element(:commit_sha_content).text + end end end end diff --git a/qa/qa/page/project/issue/index.rb b/qa/qa/page/project/issue/index.rb index a630c7373bd..f74366f6967 100644 --- a/qa/qa/page/project/issue/index.rb +++ b/qa/qa/page/project/issue/index.rb @@ -9,9 +9,17 @@ module QA element :issue_link, 'link_to issue.title' # rubocop:disable QA/ElementWithPattern end + view 'app/views/shared/issuable/_nav.html.haml' do + element :closed_issues_link + end + def click_issue_link(title) click_link(title) end + + def click_closed_issues_link + click_element :closed_issues_link + end end end end diff --git a/qa/qa/page/project/issue/show.rb b/qa/qa/page/project/issue/show.rb index 2fb947436be..52929ece9ed 100644 --- a/qa/qa/page/project/issue/show.rb +++ b/qa/qa/page/project/issue/show.rb @@ -37,6 +37,10 @@ module QA element :dropdown_menu_labels end + view 'app/views/shared/issuable/_close_reopen_button.html.haml' do + element :reopen_issue_button + end + # Adds a comment to an issue # attachment option should be an absolute path def comment(text, attachment: nil, filter: :all_activities) diff --git a/qa/qa/page/project/web_ide/edit.rb b/qa/qa/page/project/web_ide/edit.rb index 37bca97fec7..7541baed467 100644 --- a/qa/qa/page/project/web_ide/edit.rb +++ b/qa/qa/page/project/web_ide/edit.rb @@ -34,6 +34,10 @@ module QA element :dropdown_filter_input end + view 'app/assets/javascripts/ide/components/commit_sidebar/actions.vue' do + element :commit_to_current_branch_radio + end + view 'app/assets/javascripts/ide/components/commit_sidebar/form.vue' do element :begin_commit_button element :commit_button @@ -104,7 +108,7 @@ module QA # animation is still in process even when the buttons have the # expected visibility. commit_success_msg_shown = retry_until do - uncheck_element :start_new_mr_checkbox + click_element :commit_to_current_branch_radio click_element :commit_button wait(reload: false) do diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/close_issue_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/close_issue_spec.rb new file mode 100644 index 00000000000..7b6a3579af0 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/close_issue_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module QA + context 'Plan' do + describe 'Close issue' do + let(:issue_title) { 'issue title' } + let(:commit_message) { 'Closes' } + + before do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.perform(&:sign_in_using_credentials) + + issue = Resource::Issue.fabricate_via_api! do |issue| + issue.title = issue_title + end + + @project = issue.project + @issue_id = issue.api_response[:iid] + + # Initial commit should be pushed because + # the very first commit to the project doesn't close the issue + # https://gitlab.com/gitlab-org/gitlab-ce/issues/38965 + push_commit('Initial commit') + end + + it 'user closes an issue by pushing commit' do + push_commit("#{commit_message} ##{@issue_id}", false) + + @project.visit! + Page::Project::Show.perform do |show| + show.click_commit(commit_message) + end + commit_sha = Page::Project::Commit::Show.perform(&:commit_sha) + + Page::Project::Menu.perform(&:click_issues) + Page::Project::Issue::Index.perform do |index| + index.click_closed_issues_link + index.click_issue_link(issue_title) + end + + Page::Project::Issue::Show.perform do |show| + expect(show).to have_element(:reopen_issue_button) + expect(show).to have_content("closed via commit #{commit_sha}") + end + end + + def push_commit(commit_message, new_branch = true) + Resource::Repository::ProjectPush.fabricate! do |push| + push.commit_message = commit_message + push.new_branch = new_branch + push.file_content = commit_message + push.project = @project + end + end + end + end +end diff --git a/spec/controllers/boards/lists_controller_spec.rb b/spec/controllers/boards/lists_controller_spec.rb index 418ca6f3210..1e8a8145b35 100644 --- a/spec/controllers/boards/lists_controller_spec.rb +++ b/spec/controllers/boards/lists_controller_spec.rb @@ -30,6 +30,21 @@ describe Boards::ListsController do expect(json_response.length).to eq 3 end + it 'avoids n+1 queries when serializing lists' do + list_1 = create(:list, board: board) + list_1.update_preferences_for(user, { collapsed: true }) + + control_count = ActiveRecord::QueryRecorder.new { read_board_list user: user, board: board }.count + + list_2 = create(:list, board: board) + list_2.update_preferences_for(user, { collapsed: true }) + + list_3 = create(:list, board: board) + list_3.update_preferences_for(user, { collapsed: true }) + + expect { read_board_list user: user, board: board }.not_to exceed_query_limit(control_count) + end + context 'with unauthorized user' do let(:unauth_user) { create(:user) } @@ -154,6 +169,22 @@ describe Boards::ListsController do end end + context 'with collapsed preference' do + it 'saves collapsed preference for user' do + save_setting user: user, board: board, list: planning, setting: { collapsed: true } + + expect(planning.preferences_for(user).collapsed).to eq(true) + expect(response).to have_gitlab_http_status(200) + end + + it 'saves not collapsed preference for user' do + save_setting user: user, board: board, list: planning, setting: { collapsed: false } + + expect(planning.preferences_for(user).collapsed).to eq(false) + expect(response).to have_gitlab_http_status(200) + end + end + def move(user:, board:, list:, position:) sign_in(user) @@ -166,6 +197,19 @@ describe Boards::ListsController do patch :update, params: params, as: :json end + + def save_setting(user:, board:, list:, setting: {}) + sign_in(user) + + params = { namespace_id: project.namespace.to_param, + project_id: project, + board_id: board.to_param, + id: list.to_param, + list: setting, + format: :json } + + patch :update, params: params, as: :json + end end describe 'DELETE destroy' do diff --git a/spec/features/projects/files/user_browses_files_spec.rb b/spec/features/projects/files/user_browses_files_spec.rb index a090461261b..0b3f905b5de 100644 --- a/spec/features/projects/files/user_browses_files_spec.rb +++ b/spec/features/projects/files/user_browses_files_spec.rb @@ -14,7 +14,6 @@ describe "User browses files" do before do stub_feature_flags(vue_file_list: false) - stub_feature_flags(csslab: false) sign_in(user) end diff --git a/spec/finders/members_finder_spec.rb b/spec/finders/members_finder_spec.rb index 4203f58fe81..6920fb4e572 100644 --- a/spec/finders/members_finder_spec.rb +++ b/spec/finders/members_finder_spec.rb @@ -17,11 +17,10 @@ describe MembersFinder, '#execute' do result = described_class.new(project, user2).execute - expect(result.to_a).to match_array([member1, member2, member3]) + expect(result).to contain_exactly(member1, member2, member3) end - it 'includes nested group members if asked' do - project = create(:project, namespace: group) + it 'includes nested group members if asked', :nested_groups do nested_group.request_access(user1) member1 = group.add_maintainer(user2) member2 = nested_group.add_maintainer(user3) @@ -29,7 +28,28 @@ describe MembersFinder, '#execute' do result = described_class.new(project, user2).execute(include_descendants: true) - expect(result.to_a).to match_array([member1, member2, member3]) + expect(result).to contain_exactly(member1, member2, member3) + end + + it 'returns the members.access_level when the user is invited', :nested_groups do + member_invite = create(:project_member, :invited, project: project, invite_email: create(:user).email) + member1 = group.add_maintainer(user2) + + result = described_class.new(project, user2).execute(include_descendants: true) + + expect(result).to contain_exactly(member1, member_invite) + expect(result.last.access_level).to eq(member_invite.access_level) + end + + it 'returns the highest access_level for the user', :nested_groups do + member1 = project.add_guest(user1) + group.add_developer(user1) + nested_group.add_reporter(user1) + + result = described_class.new(project, user1).execute(include_descendants: true) + + expect(result).to contain_exactly(member1) + expect(result.first.access_level).to eq(Gitlab::Access::DEVELOPER) end context 'when include_invited_groups_members == true' do @@ -37,8 +57,8 @@ describe MembersFinder, '#execute' do set(:linked_group) { create(:group, :public, :access_requestable) } set(:nested_linked_group) { create(:group, parent: linked_group) } - set(:linked_group_member) { linked_group.add_developer(user1) } - set(:nested_linked_group_member) { nested_linked_group.add_developer(user2) } + set(:linked_group_member) { linked_group.add_guest(user1) } + set(:nested_linked_group_member) { nested_linked_group.add_guest(user2) } it 'includes all the invited_groups members including members inherited from ancestor groups' do create(:project_group_link, project: project, group: nested_linked_group) @@ -60,5 +80,17 @@ describe MembersFinder, '#execute' do expect(subject).to contain_exactly(linked_group_member) end + + context 'when the user is a member of invited group and ancestor groups' do + it 'returns the highest access_level for the user limited by project_group_link.group_access', :nested_groups do + create(:project_group_link, project: project, group: nested_linked_group, group_access: Gitlab::Access::REPORTER) + nested_linked_group.add_developer(user1) + + result = subject + + expect(result).to contain_exactly(linked_group_member, nested_linked_group_member) + expect(result.first.access_level).to eq(Gitlab::Access::REPORTER) + end + end end end diff --git a/spec/fixtures/api/schemas/entities/merge_request_noteable.json b/spec/fixtures/api/schemas/entities/merge_request_noteable.json new file mode 100644 index 00000000000..88b0fecc24c --- /dev/null +++ b/spec/fixtures/api/schemas/entities/merge_request_noteable.json @@ -0,0 +1,28 @@ +{ + "type": "object", + "properties" : { + "merge_params": { "type": ["object", "null"] }, + "state": { "type": "string" }, + "source_branch": { "type": "string" }, + "target_branch": { "type": "string" }, + "diff_head_sha": { "type": "string" }, + "create_note_path": { "type": ["string", "null"] }, + "preview_note_path": { "type": ["string", "null"] }, + "create_issue_to_resolve_discussions_path": { "type": ["string", "null"] }, + "new_blob_path": { "type": ["string", "null"] }, + "can_receive_suggestion": { "type": "boolean" }, + "current_user": { + "type": "object", + "required": [ + "can_create_note", + "can_update" + ], + "properties": { + "can_create_note": { "type": "boolean" }, + "can_update": { "type": "boolean" } + }, + "additionalProperties": false + } + }, + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/entities/merge_request_poll_widget.json b/spec/fixtures/api/schemas/entities/merge_request_poll_widget.json index 2052892dfa3..1eda0e12920 100644 --- a/spec/fixtures/api/schemas/entities/merge_request_poll_widget.json +++ b/spec/fixtures/api/schemas/entities/merge_request_poll_widget.json @@ -24,22 +24,20 @@ "ci_status": { "type": ["string", "null"] }, "cancel_auto_merge_path": { "type": ["string", "null"] }, "test_reports_path": { "type": ["string", "null"] }, - "can_receive_suggestion": { "type": "boolean" }, "create_issue_to_resolve_discussions_path": { "type": ["string", "null"] }, "current_user": { "type": "object", "required": [ "can_remove_source_branch", "can_revert_on_current_merge_request", - "can_cherry_pick_on_current_merge_request" + "can_cherry_pick_on_current_merge_request", + "can_create_issue" ], "properties": { "can_remove_source_branch": { "type": "boolean" }, "can_revert_on_current_merge_request": { "type": ["boolean", "null"] }, "can_cherry_pick_on_current_merge_request": { "type": ["boolean", "null"] }, - "can_create_note": { "type": "boolean" }, - "can_create_issue": { "type": "boolean" }, - "can_update": { "type": "boolean" } + "can_create_issue": { "type": "boolean" } }, "additionalProperties": false }, diff --git a/spec/fixtures/api/schemas/entities/merge_request_widget.json b/spec/fixtures/api/schemas/entities/merge_request_widget.json index 779a47222b7..e2df7952d8f 100644 --- a/spec/fixtures/api/schemas/entities/merge_request_widget.json +++ b/spec/fixtures/api/schemas/entities/merge_request_widget.json @@ -5,7 +5,6 @@ { "$ref": "merge_request_poll_widget.json" }, { "properties" : { - "merge_params": { "type": ["object", "null"] }, "source_project_full_path": { "type": ["string", "null"]}, "target_project_full_path": { "type": ["string", "null"]}, "email_patches_path": { "type": "string" }, @@ -13,9 +12,7 @@ "merge_request_basic_path": { "type": "string" }, "merge_request_widget_path": { "type": "string" }, "merge_request_cached_widget_path": { "type": "string" }, - "create_note_path": { "type": ["string", "null"] }, "commit_change_content_path": { "type": "string" }, - "preview_note_path": { "type": ["string", "null"] }, "conflicts_docs_path": { "type": ["string", "null"] }, "merge_request_pipelines_docs_path": { "type": ["string", "null"] }, "ci_environments_status_path": { "type": "string" }, diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js new file mode 100644 index 00000000000..1f4d1e17ea0 --- /dev/null +++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js @@ -0,0 +1,55 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlLoadingIcon } from '@gitlab/ui'; +import AutoMergeFailedComponent from '~/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue'; +import eventHub from '~/vue_merge_request_widget/event_hub'; + +describe('MRWidgetAutoMergeFailed', () => { + let wrapper; + const mergeError = 'This is the merge error'; + const findButton = () => wrapper.find('button'); + + const createComponent = (props = {}) => { + wrapper = shallowMount(AutoMergeFailedComponent, { + sync: false, + propsData: { ...props }, + }); + }; + + beforeEach(() => { + createComponent({ + mr: { mergeError }, + }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders failed message', () => { + expect(wrapper.text()).toContain('This merge request failed to be merged automatically'); + }); + + it('renders merge error provided', () => { + expect(wrapper.text()).toContain(mergeError); + }); + + it('render refresh button', () => { + expect(findButton().text()).toEqual('Refresh'); + }); + + it('emits event and shows loading icon when button is clicked', () => { + jest.spyOn(eventHub, '$emit'); + findButton().trigger('click'); + + expect(eventHub.$emit.mock.calls[0][0]).toBe('MRWidgetUpdateRequested'); + + return wrapper.vm.$nextTick(() => { + expect(findButton().attributes('disabled')).toEqual('disabled'); + expect( + findButton() + .find(GlLoadingIcon) + .exists(), + ).toBe(true); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/file_icon_spec.js b/spec/frontend/vue_shared/components/file_icon_spec.js new file mode 100644 index 00000000000..328eec0a80a --- /dev/null +++ b/spec/frontend/vue_shared/components/file_icon_spec.js @@ -0,0 +1,75 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlLoadingIcon } from '@gitlab/ui'; +import FileIcon from '~/vue_shared/components/file_icon.vue'; +import Icon from '~/vue_shared/components/icon.vue'; + +describe('File Icon component', () => { + let wrapper; + const findIcon = () => wrapper.find('svg'); + const getIconName = () => + findIcon() + .find('use') + .element.getAttribute('xlink:href') + .replace(`${gon.sprite_file_icons}#`, ''); + + const createComponent = (props = {}) => { + wrapper = shallowMount(FileIcon, { + sync: false, + propsData: { ...props }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it('should render a span element and an icon', () => { + createComponent({ + fileName: 'test.js', + }); + + expect(wrapper.element.tagName).toEqual('SPAN'); + expect(findIcon().exists()).toBeDefined(); + }); + + it.each` + fileName | iconName + ${'test.js'} | ${'javascript'} + ${'test.png'} | ${'image'} + ${'webpack.js'} | ${'webpack'} + `('should render a $iconName icon based on file ending', ({ fileName, iconName }) => { + createComponent({ fileName }); + expect(getIconName()).toBe(iconName); + }); + + it('should render a standard folder icon', () => { + createComponent({ + fileName: 'js', + folder: true, + }); + + expect(findIcon().exists()).toBe(false); + expect(wrapper.find(Icon).props('cssClasses')).toContain('folder-icon'); + }); + + it('should render a loading icon', () => { + createComponent({ + fileName: 'test.js', + loading: true, + }); + + expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + }); + + it('should add a special class and a size class', () => { + const size = 120; + createComponent({ + fileName: 'test.js', + cssClasses: 'extraclasses', + size, + }); + + expect(findIcon().classes()).toContain(`s${size}`); + expect(findIcon().classes()).toContain('extraclasses'); + }); +}); diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index a70bfc2adc7..29c43d1977e 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -503,7 +503,7 @@ describe ProjectsHelper do allow(Gitlab::CurrentSettings.current_application_settings).to receive(:enabled_git_access_protocol) { 'ssh' } allow(Gitlab.config.gitlab_shell).to receive(:ssh_path_prefix).and_return('git@localhost:') - expect(helper.push_to_create_project_command(user)).to eq('git push --set-upstream git@localhost:john/$(git rev-parse --show-toplevel | xargs basename).git $(git rev-parse --abbrev-ref HEAD)') + expect(helper.push_to_create_project_command(user)).to eq("git push --set-upstream #{Gitlab.config.gitlab.user}@localhost:john/$(git rev-parse --show-toplevel | xargs basename).git $(git rev-parse --abbrev-ref HEAD)") end end diff --git a/spec/javascripts/environments/environment_item_spec.js b/spec/javascripts/environments/environment_item_spec.js index 388d7063d13..f9ee4648128 100644 --- a/spec/javascripts/environments/environment_item_spec.js +++ b/spec/javascripts/environments/environment_item_spec.js @@ -106,6 +106,7 @@ describe('Environment item', () => { play_path: '/play', }, ], + deployed_at: '2016-11-29T18:11:58.430Z', }, has_stop_action: true, environment_path: 'root/ci-folders/environments/31', @@ -139,9 +140,7 @@ describe('Environment item', () => { it('should render last deployment date', () => { const timeagoInstance = new timeago(); // eslint-disable-line - const formatedDate = timeagoInstance.format( - environment.last_deployment.deployable.created_at, - ); + const formatedDate = timeagoInstance.format(environment.last_deployment.deployed_at); expect( component.$el.querySelector('.environment-created-date-timeago').textContent, diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js deleted file mode 100644 index 55a11a72551..00000000000 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js +++ /dev/null @@ -1,47 +0,0 @@ -import Vue from 'vue'; -import autoMergeFailedComponent from '~/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue'; -import eventHub from '~/vue_merge_request_widget/event_hub'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; - -describe('MRWidgetAutoMergeFailed', () => { - let vm; - const mergeError = 'This is the merge error'; - - beforeEach(() => { - const Component = Vue.extend(autoMergeFailedComponent); - vm = mountComponent(Component, { - mr: { mergeError }, - }); - }); - - afterEach(() => { - vm.$destroy(); - }); - - it('renders failed message', () => { - expect(vm.$el.textContent).toContain('This merge request failed to be merged automatically'); - }); - - it('renders merge error provided', () => { - expect(vm.$el.innerText).toContain(mergeError); - }); - - it('render refresh button', () => { - expect(vm.$el.querySelector('button').textContent.trim()).toEqual('Refresh'); - }); - - it('emits event and shows loading icon when button is clicked', done => { - spyOn(eventHub, '$emit'); - vm.$el.querySelector('button').click(); - - expect(eventHub.$emit.calls.argsFor(0)[0]).toEqual('MRWidgetUpdateRequested'); - - Vue.nextTick(() => { - expect(vm.$el.querySelector('button').getAttribute('disabled')).toEqual('disabled'); - expect(vm.$el.querySelector('button .loading-container span').classList).toContain( - 'gl-spinner', - ); - done(); - }); - }); -}); diff --git a/spec/javascripts/vue_shared/components/file_icon_spec.js b/spec/javascripts/vue_shared/components/file_icon_spec.js deleted file mode 100644 index 1f61e19fa84..00000000000 --- a/spec/javascripts/vue_shared/components/file_icon_spec.js +++ /dev/null @@ -1,92 +0,0 @@ -import Vue from 'vue'; -import fileIcon from '~/vue_shared/components/file_icon.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; - -describe('File Icon component', () => { - let vm; - let FileIcon; - - beforeEach(() => { - FileIcon = Vue.extend(fileIcon); - }); - - afterEach(() => { - vm.$destroy(); - }); - - it('should render a span element with an svg', () => { - vm = mountComponent(FileIcon, { - fileName: 'test.js', - }); - - expect(vm.$el.tagName).toEqual('SPAN'); - expect(vm.$el.querySelector('span > svg')).toBeDefined(); - }); - - it('should render a javascript icon based on file ending', () => { - vm = mountComponent(FileIcon, { - fileName: 'test.js', - }); - - expect(vm.$el.firstChild.firstChild.getAttribute('xlink:href')).toBe( - `${gon.sprite_file_icons}#javascript`, - ); - }); - - it('should render a image icon based on file ending', () => { - vm = mountComponent(FileIcon, { - fileName: 'test.png', - }); - - expect(vm.$el.firstChild.firstChild.getAttribute('xlink:href')).toBe( - `${gon.sprite_file_icons}#image`, - ); - }); - - it('should render a webpack icon based on file namer', () => { - vm = mountComponent(FileIcon, { - fileName: 'webpack.js', - }); - - expect(vm.$el.firstChild.firstChild.getAttribute('xlink:href')).toBe( - `${gon.sprite_file_icons}#webpack`, - ); - }); - - it('should render a standard folder icon', () => { - vm = mountComponent(FileIcon, { - fileName: 'js', - folder: true, - }); - - expect(vm.$el.querySelector('span > svg > use').getAttribute('xlink:href')).toBe( - `${gon.sprite_file_icons}#folder`, - ); - }); - - it('should render a loading icon', () => { - vm = mountComponent(FileIcon, { - fileName: 'test.js', - loading: true, - }); - - const { classList } = vm.$el.querySelector('.loading-container span'); - - expect(classList.contains('gl-spinner')).toEqual(true); - }); - - it('should add a special class and a size class', () => { - vm = mountComponent(FileIcon, { - fileName: 'test.js', - cssClasses: 'extraclasses', - size: 120, - }); - - const { classList } = vm.$el.firstChild; - const containsSizeClass = classList.contains('s120'); - const containsCustomClass = classList.contains('extraclasses'); - - expect(containsSizeClass).toBe(true); - expect(containsCustomClass).toBe(true); - }); -}); diff --git a/spec/lib/gitlab/authorized_keys_spec.rb b/spec/lib/gitlab/authorized_keys_spec.rb index 42bc509eeef..adf36cf1050 100644 --- a/spec/lib/gitlab/authorized_keys_spec.rb +++ b/spec/lib/gitlab/authorized_keys_spec.rb @@ -5,10 +5,81 @@ require 'spec_helper' describe Gitlab::AuthorizedKeys do let(:logger) { double('logger').as_null_object } - subject { described_class.new(logger) } + subject(:authorized_keys) { described_class.new(logger) } + + describe '#accessible?' do + subject { authorized_keys.accessible? } + + context 'authorized_keys file exists' do + before do + create_authorized_keys_fixture + end + + after do + delete_authorized_keys_file + end + + context 'can open file' do + it { is_expected.to be_truthy } + end + + context 'cannot open file' do + before do + allow(File).to receive(:open).and_raise(Errno::EACCES) + end + + it { is_expected.to be_falsey } + end + end + + context 'authorized_keys file does not exist' do + it { is_expected.to be_falsey } + end + end + + describe '#create' do + subject { authorized_keys.create } + + context 'authorized_keys file exists' do + before do + create_authorized_keys_fixture + end + + after do + delete_authorized_keys_file + end + + it { is_expected.to be_truthy } + end + + context 'authorized_keys file does not exist' do + after do + delete_authorized_keys_file + end + + it 'creates authorized_keys file' do + expect(subject).to be_truthy + expect(File.exist?(tmp_authorized_keys_path)).to be_truthy + end + end + + context 'cannot create file' do + before do + allow(File).to receive(:open).and_raise(Errno::EACCES) + end + + it { is_expected.to be_falsey } + end + end describe '#add_key' do + let(:id) { 'key-741' } + + subject { authorized_keys.add_key(id, key) } + context 'authorized_keys file exists' do + let(:key) { 'ssh-rsa AAAAB3NzaDAxx2E trailing garbage' } + before do create_authorized_keys_fixture end @@ -21,19 +92,20 @@ describe Gitlab::AuthorizedKeys do auth_line = "command=\"#{Gitlab.config.gitlab_shell.path}/bin/gitlab-shell key-741\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa AAAAB3NzaDAxx2E" expect(logger).to receive(:info).with('Adding key (key-741): ssh-rsa AAAAB3NzaDAxx2E') - expect(subject.add_key('key-741', 'ssh-rsa AAAAB3NzaDAxx2E trailing garbage')) - .to be_truthy + expect(subject).to be_truthy expect(File.read(tmp_authorized_keys_path)).to eq("existing content\n#{auth_line}\n") end end context 'authorized_keys file does not exist' do + let(:key) { 'ssh-rsa AAAAB3NzaDAxx2E' } + before do delete_authorized_keys_file end it 'creates the file' do - expect(subject.add_key('key-741', 'ssh-rsa AAAAB3NzaDAxx2E')).to be_truthy + expect(subject).to be_truthy expect(File.exist?(tmp_authorized_keys_path)).to be_truthy end end @@ -47,6 +119,8 @@ describe Gitlab::AuthorizedKeys do ] end + subject { authorized_keys.batch_add_keys(keys) } + context 'authorized_keys file exists' do before do create_authorized_keys_fixture @@ -62,7 +136,7 @@ describe Gitlab::AuthorizedKeys do expect(logger).to receive(:info).with('Adding key (key-12): ssh-dsa ASDFASGADG') expect(logger).to receive(:info).with('Adding key (key-123): ssh-rsa GFDGDFSGSDFG') - expect(subject.batch_add_keys(keys)).to be_truthy + expect(subject).to be_truthy expect(File.read(tmp_authorized_keys_path)).to eq("existing content\n#{auth_line1}\n#{auth_line2}\n") end @@ -70,7 +144,7 @@ describe Gitlab::AuthorizedKeys do let(:keys) { [double(shell_id: 'key-123', key: "ssh-rsa A\tSDFA\nSGADG")] } it "doesn't add keys" do - expect(subject.batch_add_keys(keys)).to be_falsey + expect(subject).to be_falsey expect(File.read(tmp_authorized_keys_path)).to eq("existing content\n") end end @@ -82,16 +156,28 @@ describe Gitlab::AuthorizedKeys do end it 'creates the file' do - expect(subject.batch_add_keys(keys)).to be_truthy + expect(subject).to be_truthy expect(File.exist?(tmp_authorized_keys_path)).to be_truthy end end end describe '#rm_key' do + let(:key) { 'key-741' } + + subject { authorized_keys.rm_key(key) } + context 'authorized_keys file exists' do + let(:other_line) { "command=\"#{Gitlab.config.gitlab_shell.path}/bin/gitlab-shell key-742\",options ssh-rsa AAAAB3NzaDAxx2E" } + let(:delete_line) { "command=\"#{Gitlab.config.gitlab_shell.path}/bin/gitlab-shell key-741\",options ssh-rsa AAAAB3NzaDAxx2E" } + before do create_authorized_keys_fixture + + File.open(tmp_authorized_keys_path, 'a') do |auth_file| + auth_file.puts delete_line + auth_file.puts other_line + end end after do @@ -99,16 +185,10 @@ describe Gitlab::AuthorizedKeys do end it "removes the right line" do - other_line = "command=\"#{Gitlab.config.gitlab_shell.path}/bin/gitlab-shell key-742\",options ssh-rsa AAAAB3NzaDAxx2E" - delete_line = "command=\"#{Gitlab.config.gitlab_shell.path}/bin/gitlab-shell key-741\",options ssh-rsa AAAAB3NzaDAxx2E" erased_line = delete_line.gsub(/./, '#') - File.open(tmp_authorized_keys_path, 'a') do |auth_file| - auth_file.puts delete_line - auth_file.puts other_line - end expect(logger).to receive(:info).with('Removing key (key-741)') - expect(subject.rm_key('key-741')).to be_truthy + expect(subject).to be_truthy expect(File.read(tmp_authorized_keys_path)).to eq("existing content\n#{erased_line}\n#{other_line}\n") end end @@ -118,13 +198,13 @@ describe Gitlab::AuthorizedKeys do delete_authorized_keys_file end - it 'returns false' do - expect(subject.rm_key('key-741')).to be_falsey - end + it { is_expected.to be_falsey } end end describe '#clear' do + subject { authorized_keys.clear } + context 'authorized_keys file exists' do before do create_authorized_keys_fixture @@ -134,9 +214,7 @@ describe Gitlab::AuthorizedKeys do delete_authorized_keys_file end - it "returns true" do - expect(subject.clear).to be_truthy - end + it { is_expected.to be_truthy } end context 'authorized_keys file does not exist' do @@ -144,13 +222,13 @@ describe Gitlab::AuthorizedKeys do delete_authorized_keys_file end - it "still returns true" do - expect(subject.clear).to be_truthy - end + it { is_expected.to be_truthy } end end describe '#list_key_ids' do + subject { authorized_keys.list_key_ids } + context 'authorized_keys file exists' do before do create_authorized_keys_fixture( @@ -163,9 +241,7 @@ describe Gitlab::AuthorizedKeys do delete_authorized_keys_file end - it 'returns array of key IDs' do - expect(subject.list_key_ids).to eq([1, 2, 3, 9000]) - end + it { is_expected.to eq([1, 2, 3, 9000]) } end context 'authorized_keys file does not exist' do @@ -173,9 +249,7 @@ describe Gitlab::AuthorizedKeys do delete_authorized_keys_file end - it 'returns an empty array' do - expect(subject.list_key_ids).to be_empty - end + it { is_expected.to be_empty } end end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 3c6b17c10ec..ec4a6ef05b9 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -483,3 +483,4 @@ lists: - milestone - board - label +- list_user_preferences diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index d6e1fbaa979..0aef4887c75 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -396,6 +396,27 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do expect(project.lfs_enabled).to be_falsey end + + it 'overrides project feature access levels' do + access_level_keys = project.project_feature.attributes.keys.select { |a| a =~ /_access_level/ } + + # `pages_access_level` is not included, since it is not available in the public API + # and has a dependency on project's visibility level + # see ProjectFeature model + access_level_keys.delete('pages_access_level') + + disabled_access_levels = Hash[access_level_keys.collect { |item| [item, 'disabled'] }] + + project.create_import_data(data: { override_params: disabled_access_levels }) + + restored_project_json + + aggregate_failures do + access_level_keys.each do |key| + expect(project.public_send(key)).to eq(ProjectFeature::DISABLED) + end + end + end end context 'with a project that has a group' do diff --git a/spec/lib/gitlab/internal_post_receive/response_spec.rb b/spec/lib/gitlab/internal_post_receive/response_spec.rb new file mode 100644 index 00000000000..f43762c9248 --- /dev/null +++ b/spec/lib/gitlab/internal_post_receive/response_spec.rb @@ -0,0 +1,121 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::InternalPostReceive::Response do + subject { described_class.new } + + describe '#add_merge_request_urls' do + context 'when there are urls_data' do + it 'adds a message for each merge request URL' do + urls_data = [ + { new_merge_request: false, branch_name: 'foo', url: 'http://example.com/foo/bar/merge_requests/1' }, + { new_merge_request: true, branch_name: 'bar', url: 'http://example.com/foo/bar/merge_requests/new?merge_request%5Bsource_branch%5D=bar' } + ] + + subject.add_merge_request_urls(urls_data) + + expected = [a_kind_of(described_class::Message), a_kind_of(described_class::Message)] + expect(subject.messages).to match(expected) + end + end + end + + describe '#add_merge_request_url' do + context 'when :new_merge_request is false' do + it 'adds a basic message to view the existing merge request' do + url_data = { new_merge_request: false, branch_name: 'foo', url: 'http://example.com/foo/bar/merge_requests/1' } + + subject.add_merge_request_url(url_data) + + message = <<~MESSAGE.strip + View merge request for foo: + http://example.com/foo/bar/merge_requests/1 + MESSAGE + + expect(subject.messages.first.message).to eq(message) + expect(subject.messages.first.type).to eq(:basic) + end + end + + context 'when :new_merge_request is true' do + it 'adds a basic message to create a new merge request' do + url_data = { new_merge_request: true, branch_name: 'bar', url: 'http://example.com/foo/bar/merge_requests/new?merge_request%5Bsource_branch%5D=bar' } + + subject.add_merge_request_url(url_data) + + message = <<~MESSAGE.strip + To create a merge request for bar, visit: + http://example.com/foo/bar/merge_requests/new?merge_request%5Bsource_branch%5D=bar + MESSAGE + + expect(subject.messages.first.message).to eq(message) + expect(subject.messages.first.type).to eq(:basic) + end + end + end + + describe '#add_basic_message' do + context 'when text is present' do + it 'adds a basic message' do + subject.add_basic_message('hello') + + expect(subject.messages.first.message).to eq('hello') + expect(subject.messages.first.type).to eq(:basic) + end + end + + context 'when text is blank' do + it 'does not add a message' do + subject.add_basic_message(' ') + + expect(subject.messages).to be_blank + end + end + end + + describe '#add_alert_message' do + context 'when text is present' do + it 'adds a alert message' do + subject.add_alert_message('hello') + + expect(subject.messages.first.message).to eq('hello') + expect(subject.messages.first.type).to eq(:alert) + end + end + + context 'when text is blank' do + it 'does not add a message' do + subject.add_alert_message(' ') + + expect(subject.messages).to be_blank + end + end + end + + describe '#reference_counter_decreased' do + context 'initially' do + it 'reference_counter_decreased is set to false' do + expect(subject.reference_counter_decreased).to eq(false) + end + end + end + + describe '#reference_counter_decreased=' do + context 'when the argument is truthy' do + it 'reference_counter_decreased is truthy' do + subject.reference_counter_decreased = true + + expect(subject.reference_counter_decreased).to be_truthy + end + end + + context 'when the argument is falsey' do + it 'reference_counter_decreased is falsey' do + subject.reference_counter_decreased = false + + expect(subject.reference_counter_decreased).to be_falsey + end + end + end +end diff --git a/spec/lib/gitlab/middleware/go_spec.rb b/spec/lib/gitlab/middleware/go_spec.rb index f52095bf633..16595102375 100644 --- a/spec/lib/gitlab/middleware/go_spec.rb +++ b/spec/lib/gitlab/middleware/go_spec.rb @@ -202,7 +202,7 @@ describe Gitlab::Middleware::Go do def expect_response_with_path(response, protocol, path) repository_url = case protocol when :ssh - "ssh://git@#{Gitlab.config.gitlab.host}/#{path}.git" + "ssh://#{Gitlab.config.gitlab.user}@#{Gitlab.config.gitlab.host}/#{path}.git" when :http, nil "http://#{Gitlab.config.gitlab.host}/#{path}.git" end diff --git a/spec/lib/gitlab/performance_bar/with_top_level_warnings_spec.rb b/spec/lib/gitlab/performance_bar/with_top_level_warnings_spec.rb new file mode 100644 index 00000000000..3b92261f0fe --- /dev/null +++ b/spec/lib/gitlab/performance_bar/with_top_level_warnings_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' +require 'rspec-parameterized' + +describe Gitlab::PerformanceBar::WithTopLevelWarnings do + using RSpec::Parameterized::TableSyntax + + subject { Module.new } + + before do + subject.singleton_class.prepend(described_class) + end + + describe '#has_warnings?' do + where(:has_warnings, :results) do + false | { data: {} } + false | { data: { gitaly: { warnings: [] } } } + true | { data: { gitaly: { warnings: [1] } } } + true | { data: { gitaly: { warnings: [] }, redis: { warnings: [1] } } } + end + + with_them do + it do + expect(subject.has_warnings?(results)).to eq(has_warnings) + end + end + end +end diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb index 0ba16b93ee7..fe4853fd819 100644 --- a/spec/lib/gitlab/shell_spec.rb +++ b/spec/lib/gitlab/shell_spec.rb @@ -52,38 +52,14 @@ describe Gitlab::Shell do describe '#add_key' do context 'when authorized_keys_enabled is true' do - context 'authorized_keys_file not set' do - before do - stub_gitlab_shell_setting(authorized_keys_file: nil) - allow(gitlab_shell) - .to receive(:gitlab_shell_keys_path) - .and_return(:gitlab_shell_keys_path) - end - - it 'calls #gitlab_shell_fast_execute with add-key command' do - expect(gitlab_shell) - .to receive(:gitlab_shell_fast_execute) - .with([ - :gitlab_shell_keys_path, - 'add-key', - 'key-123', - 'ssh-rsa foobar' - ]) - - gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') - end - end - - context 'authorized_keys_file set' do - it 'calls Gitlab::AuthorizedKeys#add_key with id and key' do - expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) + it 'calls Gitlab::AuthorizedKeys#add_key with id and key' do + expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) - expect(gitlab_authorized_keys) - .to receive(:add_key) - .with('key-123', 'ssh-rsa foobar') + expect(gitlab_authorized_keys) + .to receive(:add_key) + .with('key-123', 'ssh-rsa foobar') - gitlab_shell.add_key('key-123', 'ssh-rsa foobar') - end + gitlab_shell.add_key('key-123', 'ssh-rsa foobar') end end @@ -92,24 +68,10 @@ describe Gitlab::Shell do stub_application_setting(authorized_keys_enabled: false) end - context 'authorized_keys_file not set' do - before do - stub_gitlab_shell_setting(authorized_keys_file: nil) - end + it 'does nothing' do + expect(Gitlab::AuthorizedKeys).not_to receive(:new) - it 'does nothing' do - expect(gitlab_shell).not_to receive(:gitlab_shell_fast_execute) - - gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') - end - end - - context 'authorized_keys_file set' do - it 'does nothing' do - expect(Gitlab::AuthorizedKeys).not_to receive(:new) - - gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') - end + gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') end end @@ -118,38 +80,14 @@ describe Gitlab::Shell do stub_application_setting(authorized_keys_enabled: nil) end - context 'authorized_keys_file not set' do - before do - stub_gitlab_shell_setting(authorized_keys_file: nil) - allow(gitlab_shell) - .to receive(:gitlab_shell_keys_path) - .and_return(:gitlab_shell_keys_path) - end - - it 'calls #gitlab_shell_fast_execute with add-key command' do - expect(gitlab_shell) - .to receive(:gitlab_shell_fast_execute) - .with([ - :gitlab_shell_keys_path, - 'add-key', - 'key-123', - 'ssh-rsa foobar' - ]) - - gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') - end - end - - context 'authorized_keys_file set' do - it 'calls Gitlab::AuthorizedKeys#add_key with id and key' do - expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) + it 'calls Gitlab::AuthorizedKeys#add_key with id and key' do + expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) - expect(gitlab_authorized_keys) - .to receive(:add_key) - .with('key-123', 'ssh-rsa foobar') + expect(gitlab_authorized_keys) + .to receive(:add_key) + .with('key-123', 'ssh-rsa foobar') - gitlab_shell.add_key('key-123', 'ssh-rsa foobar') - end + gitlab_shell.add_key('key-123', 'ssh-rsa foobar') end end end @@ -158,50 +96,14 @@ describe Gitlab::Shell do let(:keys) { [double(shell_id: 'key-123', key: 'ssh-rsa foobar')] } context 'when authorized_keys_enabled is true' do - context 'authorized_keys_file not set' do - let(:io) { double } - - before do - stub_gitlab_shell_setting(authorized_keys_file: nil) - end - - context 'valid keys' do - before do - allow(gitlab_shell) - .to receive(:gitlab_shell_keys_path) - .and_return(:gitlab_shell_keys_path) - end - - it 'calls gitlab-keys with batch-add-keys command' do - expect(IO) - .to receive(:popen) - .with("gitlab_shell_keys_path batch-add-keys", 'w') - .and_yield(io) - - expect(io).to receive(:puts).with("key-123\tssh-rsa foobar") - expect(gitlab_shell.batch_add_keys(keys)).to be_truthy - end - end - - context 'invalid keys' do - let(:keys) { [double(shell_id: 'key-123', key: "ssh-rsa A\tSDFA\nSGADG")] } - - it 'catches failure and returns false' do - expect(gitlab_shell.batch_add_keys(keys)).to be_falsey - end - end - end + it 'calls Gitlab::AuthorizedKeys#batch_add_keys with keys to be added' do + expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) - context 'authorized_keys_file set' do - it 'calls Gitlab::AuthorizedKeys#batch_add_keys with keys to be added' do - expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) + expect(gitlab_authorized_keys) + .to receive(:batch_add_keys) + .with(keys) - expect(gitlab_authorized_keys) - .to receive(:batch_add_keys) - .with(keys) - - gitlab_shell.batch_add_keys(keys) - end + gitlab_shell.batch_add_keys(keys) end end @@ -210,24 +112,10 @@ describe Gitlab::Shell do stub_application_setting(authorized_keys_enabled: false) end - context 'authorized_keys_file not set' do - before do - stub_gitlab_shell_setting(authorized_keys_file: nil) - end - - it 'does nothing' do - expect(IO).not_to receive(:popen) - - gitlab_shell.batch_add_keys(keys) - end - end - - context 'authorized_keys_file set' do - it 'does nothing' do - expect(Gitlab::AuthorizedKeys).not_to receive(:new) + it 'does nothing' do + expect(Gitlab::AuthorizedKeys).not_to receive(:new) - gitlab_shell.batch_add_keys(keys) - end + gitlab_shell.batch_add_keys(keys) end end @@ -236,72 +124,25 @@ describe Gitlab::Shell do stub_application_setting(authorized_keys_enabled: nil) end - context 'authorized_keys_file not set' do - let(:io) { double } + it 'calls Gitlab::AuthorizedKeys#batch_add_keys with keys to be added' do + expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) - before do - stub_gitlab_shell_setting(authorized_keys_file: nil) - allow(gitlab_shell) - .to receive(:gitlab_shell_keys_path) - .and_return(:gitlab_shell_keys_path) - end - - it 'calls gitlab-keys with batch-add-keys command' do - expect(IO) - .to receive(:popen) - .with("gitlab_shell_keys_path batch-add-keys", 'w') - .and_yield(io) + expect(gitlab_authorized_keys) + .to receive(:batch_add_keys) + .with(keys) - expect(io).to receive(:puts).with("key-123\tssh-rsa foobar") - - gitlab_shell.batch_add_keys(keys) - end - end - - context 'authorized_keys_file set' do - it 'calls Gitlab::AuthorizedKeys#batch_add_keys with keys to be added' do - expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) - - expect(gitlab_authorized_keys) - .to receive(:batch_add_keys) - .with(keys) - - gitlab_shell.batch_add_keys(keys) - end + gitlab_shell.batch_add_keys(keys) end end end describe '#remove_key' do context 'when authorized_keys_enabled is true' do - context 'authorized_keys_file not set' do - before do - stub_gitlab_shell_setting(authorized_keys_file: nil) - allow(gitlab_shell) - .to receive(:gitlab_shell_keys_path) - .and_return(:gitlab_shell_keys_path) - end - - it 'calls #gitlab_shell_fast_execute with rm-key command' do - expect(gitlab_shell) - .to receive(:gitlab_shell_fast_execute) - .with([ - :gitlab_shell_keys_path, - 'rm-key', - 'key-123' - ]) - - gitlab_shell.remove_key('key-123') - end - end + it 'calls Gitlab::AuthorizedKeys#rm_key with the key to be removed' do + expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) + expect(gitlab_authorized_keys).to receive(:rm_key).with('key-123') - context 'authorized_keys_file not set' do - it 'calls Gitlab::AuthorizedKeys#rm_key with the key to be removed' do - expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) - expect(gitlab_authorized_keys).to receive(:rm_key).with('key-123') - - gitlab_shell.remove_key('key-123') - end + gitlab_shell.remove_key('key-123') end end @@ -310,24 +151,10 @@ describe Gitlab::Shell do stub_application_setting(authorized_keys_enabled: false) end - context 'authorized_keys_file not set' do - before do - stub_gitlab_shell_setting(authorized_keys_file: nil) - end - - it 'does nothing' do - expect(gitlab_shell).not_to receive(:gitlab_shell_fast_execute) + it 'does nothing' do + expect(Gitlab::AuthorizedKeys).not_to receive(:new) - gitlab_shell.remove_key('key-123') - end - end - - context 'authorized_keys_file set' do - it 'does nothing' do - expect(Gitlab::AuthorizedKeys).not_to receive(:new) - - gitlab_shell.remove_key('key-123') - end + gitlab_shell.remove_key('key-123') end end @@ -336,64 +163,22 @@ describe Gitlab::Shell do stub_application_setting(authorized_keys_enabled: nil) end - context 'authorized_keys_file not set' do - before do - stub_gitlab_shell_setting(authorized_keys_file: nil) - allow(gitlab_shell) - .to receive(:gitlab_shell_keys_path) - .and_return(:gitlab_shell_keys_path) - end - - it 'calls #gitlab_shell_fast_execute with rm-key command' do - expect(gitlab_shell) - .to receive(:gitlab_shell_fast_execute) - .with([ - :gitlab_shell_keys_path, - 'rm-key', - 'key-123' - ]) - - gitlab_shell.remove_key('key-123') - end - end - - context 'authorized_keys_file not set' do - it 'calls Gitlab::AuthorizedKeys#rm_key with the key to be removed' do - expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) - expect(gitlab_authorized_keys).to receive(:rm_key).with('key-123') + it 'calls Gitlab::AuthorizedKeys#rm_key with the key to be removed' do + expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) + expect(gitlab_authorized_keys).to receive(:rm_key).with('key-123') - gitlab_shell.remove_key('key-123') - end + gitlab_shell.remove_key('key-123') end end end describe '#remove_all_keys' do context 'when authorized_keys_enabled is true' do - context 'authorized_keys_file not set' do - before do - stub_gitlab_shell_setting(authorized_keys_file: nil) - allow(gitlab_shell) - .to receive(:gitlab_shell_keys_path) - .and_return(:gitlab_shell_keys_path) - end + it 'calls Gitlab::AuthorizedKeys#clear' do + expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) + expect(gitlab_authorized_keys).to receive(:clear) - it 'calls #gitlab_shell_fast_execute with clear command' do - expect(gitlab_shell) - .to receive(:gitlab_shell_fast_execute) - .with([:gitlab_shell_keys_path, 'clear']) - - gitlab_shell.remove_all_keys - end - end - - context 'authorized_keys_file set' do - it 'calls Gitlab::AuthorizedKeys#clear' do - expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) - expect(gitlab_authorized_keys).to receive(:clear) - - gitlab_shell.remove_all_keys - end + gitlab_shell.remove_all_keys end end @@ -402,24 +187,10 @@ describe Gitlab::Shell do stub_application_setting(authorized_keys_enabled: false) end - context 'authorized_keys_file not set' do - before do - stub_gitlab_shell_setting(authorized_keys_file: nil) - end - - it 'does nothing' do - expect(gitlab_shell).not_to receive(:gitlab_shell_fast_execute) + it 'does nothing' do + expect(Gitlab::AuthorizedKeys).not_to receive(:new) - gitlab_shell.remove_all_keys - end - end - - context 'authorized_keys_file set' do - it 'does nothing' do - expect(Gitlab::AuthorizedKeys).not_to receive(:new) - - gitlab_shell.remove_all_keys - end + gitlab_shell.remove_all_keys end end @@ -428,163 +199,73 @@ describe Gitlab::Shell do stub_application_setting(authorized_keys_enabled: nil) end - context 'authorized_keys_file not set' do - before do - stub_gitlab_shell_setting(authorized_keys_file: nil) - allow(gitlab_shell) - .to receive(:gitlab_shell_keys_path) - .and_return(:gitlab_shell_keys_path) - end - - it 'calls #gitlab_shell_fast_execute with clear command' do - expect(gitlab_shell) - .to receive(:gitlab_shell_fast_execute) - .with([:gitlab_shell_keys_path, 'clear']) + it 'calls Gitlab::AuthorizedKeys#clear' do + expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) + expect(gitlab_authorized_keys).to receive(:clear) - gitlab_shell.remove_all_keys - end - end - - context 'authorized_keys_file set' do - it 'calls Gitlab::AuthorizedKeys#clear' do - expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) - expect(gitlab_authorized_keys).to receive(:clear) - - gitlab_shell.remove_all_keys - end + gitlab_shell.remove_all_keys end end end describe '#remove_keys_not_found_in_db' do context 'when keys are in the file that are not in the DB' do - context 'authorized_keys_file not set' do - before do - stub_gitlab_shell_setting(authorized_keys_file: nil) - gitlab_shell.remove_all_keys - gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') - gitlab_shell.add_key('key-9876', 'ssh-rsa ASDFASDF') - @another_key = create(:key) # this one IS in the DB - end - - it 'removes the keys' do - expect(gitlab_shell).to receive(:remove_key).with('key-1234') - expect(gitlab_shell).to receive(:remove_key).with('key-9876') - expect(gitlab_shell).not_to receive(:remove_key).with("key-#{@another_key.id}") - - gitlab_shell.remove_keys_not_found_in_db - end + before do + gitlab_shell.remove_all_keys + gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') + gitlab_shell.add_key('key-9876', 'ssh-rsa ASDFASDF') + @another_key = create(:key) # this one IS in the DB end - context 'authorized_keys_file set' do - before do - gitlab_shell.remove_all_keys - gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') - gitlab_shell.add_key('key-9876', 'ssh-rsa ASDFASDF') - @another_key = create(:key) # this one IS in the DB - end - - it 'removes the keys' do - expect(gitlab_shell).to receive(:remove_key).with('key-1234') - expect(gitlab_shell).to receive(:remove_key).with('key-9876') - expect(gitlab_shell).not_to receive(:remove_key).with("key-#{@another_key.id}") + it 'removes the keys' do + expect(gitlab_shell).to receive(:remove_key).with('key-1234') + expect(gitlab_shell).to receive(:remove_key).with('key-9876') + expect(gitlab_shell).not_to receive(:remove_key).with("key-#{@another_key.id}") - gitlab_shell.remove_keys_not_found_in_db - end + gitlab_shell.remove_keys_not_found_in_db end end context 'when keys there are duplicate keys in the file that are not in the DB' do - context 'authorized_keys_file not set' do - before do - stub_gitlab_shell_setting(authorized_keys_file: nil) - gitlab_shell.remove_all_keys - gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') - gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') - end - - it 'removes the keys' do - expect(gitlab_shell).to receive(:remove_key).with('key-1234') - - gitlab_shell.remove_keys_not_found_in_db - end + before do + gitlab_shell.remove_all_keys + gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') + gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') end - context 'authorized_keys_file set' do - before do - gitlab_shell.remove_all_keys - gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') - gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') - end - - it 'removes the keys' do - expect(gitlab_shell).to receive(:remove_key).with('key-1234') + it 'removes the keys' do + expect(gitlab_shell).to receive(:remove_key).with('key-1234') - gitlab_shell.remove_keys_not_found_in_db - end + gitlab_shell.remove_keys_not_found_in_db end end context 'when keys there are duplicate keys in the file that ARE in the DB' do - context 'authorized_keys_file not set' do - before do - stub_gitlab_shell_setting(authorized_keys_file: nil) - gitlab_shell.remove_all_keys - @key = create(:key) - gitlab_shell.add_key(@key.shell_id, @key.key) - end - - it 'does not remove the key' do - expect(gitlab_shell).not_to receive(:remove_key).with("key-#{@key.id}") - - gitlab_shell.remove_keys_not_found_in_db - end + before do + gitlab_shell.remove_all_keys + @key = create(:key) + gitlab_shell.add_key(@key.shell_id, @key.key) end - context 'authorized_keys_file set' do - before do - gitlab_shell.remove_all_keys - @key = create(:key) - gitlab_shell.add_key(@key.shell_id, @key.key) - end - - it 'does not remove the key' do - expect(gitlab_shell).not_to receive(:remove_key).with("key-#{@key.id}") + it 'does not remove the key' do + expect(gitlab_shell).not_to receive(:remove_key).with("key-#{@key.id}") - gitlab_shell.remove_keys_not_found_in_db - end + gitlab_shell.remove_keys_not_found_in_db end end unless ENV['CI'] # Skip in CI, it takes 1 minute context 'when the first batch can be skipped, but the next batch has keys that are not in the DB' do - context 'authorized_keys_file not set' do - before do - stub_gitlab_shell_setting(authorized_keys_file: nil) - gitlab_shell.remove_all_keys - 100.times { |i| create(:key) } # first batch is all in the DB - gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') - end - - it 'removes the keys not in the DB' do - expect(gitlab_shell).to receive(:remove_key).with('key-1234') - - gitlab_shell.remove_keys_not_found_in_db - end + before do + gitlab_shell.remove_all_keys + 100.times { |i| create(:key) } # first batch is all in the DB + gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') end - context 'authorized_keys_file set' do - before do - gitlab_shell.remove_all_keys - 100.times { |i| create(:key) } # first batch is all in the DB - gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') - end - - it 'removes the keys not in the DB' do - expect(gitlab_shell).to receive(:remove_key).with('key-1234') + it 'removes the keys not in the DB' do + expect(gitlab_shell).to receive(:remove_key).with('key-1234') - gitlab_shell.remove_keys_not_found_in_db - end + gitlab_shell.remove_keys_not_found_in_db end end end diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index 5c5ff46112f..98421cd12d3 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -14,6 +14,12 @@ describe Gitlab::Workhorse do [key, command, params] end + before do + allow(Feature::Gitaly).to receive(:server_feature_flags).and_return({ + 'gitaly-feature-foobar' => 'true' + }) + end + describe ".send_git_archive" do let(:ref) { 'master' } let(:format) { 'zip' } @@ -41,6 +47,7 @@ describe Gitlab::Workhorse do expected_params = metadata.merge( 'GitalyRepository' => repository.gitaly_repository.to_h, 'GitalyServer' => { + features: { 'gitaly-feature-foobar' => 'true' }, address: Gitlab::GitalyClient.address(project.repository_storage), token: Gitlab::GitalyClient.token(project.repository_storage) } @@ -69,6 +76,7 @@ describe Gitlab::Workhorse do expect(command).to eq('git-archive') expect(params).to eq({ 'GitalyServer' => { + features: { 'gitaly-feature-foobar' => 'true' }, address: Gitlab::GitalyClient.address(project.repository_storage), token: Gitlab::GitalyClient.token(project.repository_storage) }, @@ -117,6 +125,7 @@ describe Gitlab::Workhorse do expect(command).to eq("git-format-patch") expect(params).to eq({ 'GitalyServer' => { + features: { 'gitaly-feature-foobar' => 'true' }, address: Gitlab::GitalyClient.address(project.repository_storage), token: Gitlab::GitalyClient.token(project.repository_storage) }, @@ -178,6 +187,7 @@ describe Gitlab::Workhorse do expect(command).to eq("git-diff") expect(params).to eq({ 'GitalyServer' => { + features: { 'gitaly-feature-foobar' => 'true' }, address: Gitlab::GitalyClient.address(project.repository_storage), token: Gitlab::GitalyClient.token(project.repository_storage) }, @@ -315,6 +325,7 @@ describe Gitlab::Workhorse do let(:gitaly_params) do { GitalyServer: { + features: { 'gitaly-feature-foobar' => 'true' }, address: Gitlab::GitalyClient.address('default'), token: Gitlab::GitalyClient.token('default') } @@ -463,6 +474,7 @@ describe Gitlab::Workhorse do expect(command).to eq('git-blob') expect(params).to eq({ 'GitalyServer' => { + features: { 'gitaly-feature-foobar' => 'true' }, address: Gitlab::GitalyClient.address(project.repository_storage), token: Gitlab::GitalyClient.token(project.repository_storage) }, @@ -504,6 +516,7 @@ describe Gitlab::Workhorse do expect(command).to eq('git-snapshot') expect(params).to eq( 'GitalyServer' => { + 'features' => { 'gitaly-feature-foobar' => 'true' }, 'address' => Gitlab::GitalyClient.address(project.repository_storage), 'token' => Gitlab::GitalyClient.token(project.repository_storage) }, diff --git a/spec/lib/peek/views/detailed_view_spec.rb b/spec/lib/peek/views/detailed_view_spec.rb new file mode 100644 index 00000000000..d8660a55ea9 --- /dev/null +++ b/spec/lib/peek/views/detailed_view_spec.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +describe Peek::Views::DetailedView, :request_store do + context 'when a class defines thresholds' do + let(:threshold_view) do + Class.new(described_class) do + def self.thresholds + { + calls: 1, + duration: 10, + individual_call: 5 + } + end + + def key + 'threshold-view' + end + end.new + end + + context 'when the results exceed the calls threshold' do + before do + allow(threshold_view) + .to receive(:detail_store).and_return([{ duration: 0.001 }, { duration: 0.001 }]) + end + + it 'adds a warning to the results key' do + expect(threshold_view.results).to include(warnings: [a_string_matching('threshold-view calls')]) + end + end + + context 'when the results exceed the duration threshold' do + before do + allow(threshold_view) + .to receive(:detail_store).and_return([{ duration: 0.011 }]) + end + + it 'adds a warning to the results key' do + expect(threshold_view.results).to include(warnings: [a_string_matching('threshold-view duration')]) + end + end + + context 'when a single call exceeds the duration threshold' do + before do + allow(threshold_view) + .to receive(:detail_store).and_return([{ duration: 0.001 }, { duration: 0.006 }]) + end + + it 'adds a warning to that call detail entry' do + expect(threshold_view.results) + .to include(details: a_collection_containing_exactly( + { duration: 1.0, warnings: [] }, + { duration: 6.0, warnings: ['6.0 over 5'] } + )) + end + end + end + + context 'when a view does not define thresholds' do + let(:no_threshold_view) { Class.new(described_class).new } + + before do + allow(no_threshold_view) + .to receive(:detail_store).and_return([{ duration: 100 }, { duration: 100 }]) + end + + it 'does not add warnings to the top level' do + expect(no_threshold_view.results).to include(warnings: []) + end + + it 'does not add warnings to call details entries' do + expect(no_threshold_view.results) + .to include(details: a_collection_containing_exactly( + { duration: 100000, warnings: [] }, + { duration: 100000, warnings: [] } + )) + end + end +end diff --git a/spec/lib/peek/views/redis_detailed_spec.rb b/spec/lib/peek/views/redis_detailed_spec.rb index 61096e6c69e..fa9532226f2 100644 --- a/spec/lib/peek/views/redis_detailed_spec.rb +++ b/spec/lib/peek/views/redis_detailed_spec.rb @@ -21,10 +21,10 @@ describe Peek::Views::RedisDetailed, :request_store do expect(subject.results[:details].count).to eq(1) expect(subject.results[:details].first) - .to eq({ - cmd: expected, - duration: 1000 - }) + .to include({ + cmd: expected, + duration: 1000 + }) end end diff --git a/spec/lib/system_check/app/authorized_keys_permission_check_spec.rb b/spec/lib/system_check/app/authorized_keys_permission_check_spec.rb new file mode 100644 index 00000000000..1a8123c3f0a --- /dev/null +++ b/spec/lib/system_check/app/authorized_keys_permission_check_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe SystemCheck::App::AuthorizedKeysPermissionCheck do + subject(:system_check) { described_class.new } + + describe '#skip?' do + subject { system_check.skip? } + + context 'authorized keys enabled' do + it { is_expected.to eq(false) } + end + + context 'authorized keys not enabled' do + before do + stub_application_setting(authorized_keys_enabled: false) + end + + it { is_expected.to eq(true) } + end + end + + describe '#check?' do + subject { system_check.check? } + + before do + expect_next_instance_of(Gitlab::AuthorizedKeys) do |instance| + allow(instance).to receive(:accessible?) { accessible? } + end + end + + context 'authorized keys is accessible' do + let(:accessible?) { true } + + it { is_expected.to eq(true) } + end + + context 'authorized keys is not accessible' do + let(:accessible?) { false } + + it { is_expected.to eq(false) } + end + end + + describe '#repair!' do + subject { system_check.repair! } + + before do + expect_next_instance_of(Gitlab::AuthorizedKeys) do |instance| + allow(instance).to receive(:create) { created } + end + end + + context 'authorized_keys file created' do + let(:created) { true } + + it { is_expected.to eq(true) } + end + + context 'authorized_keys file is not created' do + let(:created) { false } + + it { is_expected.to eq(false) } + end + end +end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 6cba7df114c..56fa26d5f23 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -187,6 +187,22 @@ describe Notify do end end + describe 'that are due soon' do + subject { described_class.issue_due_email(recipient.id, issue.id) } + + before do + issue.update(due_date: Date.tomorrow) + end + + it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do + let(:model) { issue } + end + it_behaves_like 'it should show Gmail Actions View Issue link' + it_behaves_like 'an unsubscribeable thread' + it_behaves_like 'appearance header and footer enabled' + it_behaves_like 'appearance header and footer not enabled' + end + describe 'status changed' do let(:status) { 'closed' } subject { described_class.issue_status_changed_email(recipient.id, issue.id, status, current_user.id) } diff --git a/spec/models/list_spec.rb b/spec/models/list_spec.rb index 18d4549977c..2429cd408a6 100644 --- a/spec/models/list_spec.rb +++ b/spec/models/list_spec.rb @@ -81,4 +81,83 @@ describe List do expect(subject.title).to eq 'Closed' end end + + describe '#update_preferences_for' do + let(:user) { create(:user) } + let(:list) { create(:list) } + + context 'when user is present' do + context 'when there are no preferences for user' do + it 'creates new user preferences' do + expect { list.update_preferences_for(user, collapsed: true) }.to change { ListUserPreference.count }.by(1) + expect(list.preferences_for(user).collapsed).to eq(true) + end + end + + context 'when there are preferences for user' do + it 'updates user preferences' do + list.update_preferences_for(user, collapsed: false) + + expect { list.update_preferences_for(user, collapsed: true) }.not_to change { ListUserPreference.count } + expect(list.preferences_for(user).collapsed).to eq(true) + end + end + + context 'when user is nil' do + it 'does not create user preferences' do + expect { list.update_preferences_for(nil, collapsed: true) }.not_to change { ListUserPreference.count } + end + end + end + end + + describe '#preferences_for' do + let(:user) { create(:user) } + let(:list) { create(:list) } + + context 'when user is nil' do + it 'returns not persisted preferences' do + preferences = list.preferences_for(nil) + + expect(preferences.persisted?).to eq(false) + expect(preferences.list_id).to eq(list.id) + expect(preferences.user_id).to be_nil + end + end + + context 'when a user preference already exists' do + before do + list.update_preferences_for(user, collapsed: true) + end + + it 'loads preference for user' do + preferences = list.preferences_for(user) + + expect(preferences).to be_persisted + expect(preferences.collapsed).to eq(true) + end + + context 'when preferences are already loaded for user' do + it 'gets preloaded user preferences' do + fetched_list = described_class.where(id: list.id).with_preferences_for(user).first + + expect(fetched_list).to receive(:preloaded_preferences_for).with(user).and_call_original + + preferences = fetched_list.preferences_for(user) + + expect(preferences.collapsed).to eq(true) + end + end + end + + context 'when preferences for user does not exist' do + it 'returns not persisted preferences' do + preferences = list.preferences_for(user) + + expect(preferences.persisted?).to eq(false) + expect(preferences.user_id).to eq(user.id) + expect(preferences.list_id).to eq(list.id) + end + end + end end diff --git a/spec/models/list_user_preference_spec.rb b/spec/models/list_user_preference_spec.rb new file mode 100644 index 00000000000..1335a3700dc --- /dev/null +++ b/spec/models/list_user_preference_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ListUserPreference do + set(:user) { create(:user) } + set(:list) { create(:list) } + + before do + list.update_preferences_for(user, { collapsed: true }) + end + + describe 'relationships' do + it { is_expected.to belong_to(:list) } + it { is_expected.to belong_to(:user) } + + it do + is_expected.to validate_uniqueness_of(:user_id).scoped_to(:list_id) + .with_message("should have only one list preference per user") + end + end +end diff --git a/spec/models/remote_mirror_spec.rb b/spec/models/remote_mirror_spec.rb index 7edeb56efe2..f8d6e500e10 100644 --- a/spec/models/remote_mirror_spec.rb +++ b/spec/models/remote_mirror_spec.rb @@ -40,6 +40,13 @@ describe RemoteMirror, :mailer do expect(remote_mirror).to be_invalid expect(remote_mirror.errors[:url].first).to include('Requests to the local network are not allowed') end + + it 'returns a nil safe_url' do + remote_mirror = build(:remote_mirror, url: 'http://[0:0:0:0:ffff:123.123.123.123]/foo.git') + + expect(remote_mirror.url).to eq('http://[0:0:0:0:ffff:123.123.123.123]/foo.git') + expect(remote_mirror.safe_url).to be_nil + end end end diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index 3ab1818bebb..c94f6d22e74 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -925,19 +925,20 @@ describe API::Internal do it 'returns link to create new merge request' do post api('/internal/post_receive'), params: valid_params - expect(json_response['merge_request_urls']).to match [{ - "branch_name" => branch_name, - "url" => "http://#{Gitlab.config.gitlab.host}/#{project.full_path}/merge_requests/new?merge_request%5Bsource_branch%5D=#{branch_name}", - "new_merge_request" => true - }] + message = <<~MESSAGE.strip + To create a merge request for #{branch_name}, visit: + http://#{Gitlab.config.gitlab.host}/#{project.full_path}/merge_requests/new?merge_request%5Bsource_branch%5D=#{branch_name} + MESSAGE + + expect(json_response['messages']).to include(build_basic_message(message)) end - it 'returns empty array if printing_merge_request_link_enabled is false' do + it 'returns no merge request messages if printing_merge_request_link_enabled is false' do project.update!(printing_merge_request_link_enabled: false) post api('/internal/post_receive'), params: valid_params - expect(json_response['merge_request_urls']).to eq([]) + expect(json_response['messages']).to be_blank end it 'does not invoke MergeRequests::PushOptionsHandlerService' do @@ -968,11 +969,12 @@ describe API::Internal do it 'links to the newly created merge request' do post api('/internal/post_receive'), params: valid_params - expect(json_response['merge_request_urls']).to match [{ - 'branch_name' => branch_name, - 'url' => "http://#{Gitlab.config.gitlab.host}/#{project.full_path}/merge_requests/1", - 'new_merge_request' => false - }] + message = <<~MESSAGE.strip + View merge request for #{branch_name}: + http://#{Gitlab.config.gitlab.host}/#{project.full_path}/merge_requests/1 + MESSAGE + + expect(json_response['messages']).to include(build_basic_message(message)) end it 'adds errors on the service instance to warnings' do @@ -982,7 +984,8 @@ describe API::Internal do post api('/internal/post_receive'), params: valid_params - expect(json_response['warnings']).to eq('Error encountered with push options \'merge_request.create\': my error') + message = "WARNINGS:\nError encountered with push options 'merge_request.create': my error" + expect(json_response['messages']).to include(build_alert_message(message)) end it 'adds ActiveRecord errors on invalid MergeRequest records to warnings' do @@ -995,38 +998,39 @@ describe API::Internal do post api('/internal/post_receive'), params: valid_params - expect(json_response['warnings']).to eq('Error encountered with push options \'merge_request.create\': my error') + message = "WARNINGS:\nError encountered with push options 'merge_request.create': my error" + expect(json_response['messages']).to include(build_alert_message(message)) end end context 'broadcast message exists' do let!(:broadcast_message) { create(:broadcast_message, starts_at: 1.day.ago, ends_at: 1.day.from_now ) } - it 'returns one broadcast message' do + it 'outputs a broadcast message' do post api('/internal/post_receive'), params: valid_params expect(response).to have_gitlab_http_status(200) - expect(json_response['broadcast_message']).to eq(broadcast_message.message) + expect(json_response['messages']).to include(build_alert_message(broadcast_message.message)) end end context 'broadcast message does not exist' do - it 'returns empty string' do + it 'does not output a broadcast message' do post api('/internal/post_receive'), params: valid_params expect(response).to have_gitlab_http_status(200) - expect(json_response['broadcast_message']).to eq(nil) + expect(has_alert_messages?(json_response['messages'])).to be_falsey end end context 'nil broadcast message' do - it 'returns empty string' do + it 'does not output a broadcast message' do allow(BroadcastMessage).to receive(:current).and_return(nil) post api('/internal/post_receive'), params: valid_params expect(response).to have_gitlab_http_status(200) - expect(json_response['broadcast_message']).to eq(nil) + expect(has_alert_messages?(json_response['messages'])).to be_falsey end end @@ -1038,8 +1042,7 @@ describe API::Internal do post api('/internal/post_receive'), params: valid_params expect(response).to have_gitlab_http_status(200) - expect(json_response["redirected_message"]).to be_present - expect(json_response["redirected_message"]).to eq(project_moved.message) + expect(json_response['messages']).to include(build_basic_message(project_moved.message)) end end @@ -1051,8 +1054,7 @@ describe API::Internal do post api('/internal/post_receive'), params: valid_params expect(response).to have_gitlab_http_status(200) - expect(json_response["project_created_message"]).to be_present - expect(json_response["project_created_message"]).to eq(project_created.message) + expect(json_response['messages']).to include(build_basic_message(project_created.message)) end end @@ -1172,4 +1174,18 @@ describe API::Internal do } ) end + + def build_alert_message(message) + { 'type' => 'alert', 'message' => message } + end + + def build_basic_message(message) + { 'type' => 'basic', 'message' => message } + end + + def has_alert_messages?(messages) + messages.any? do |message| + message['type'] == 'alert' + end + end end diff --git a/spec/requests/api/project_snapshots_spec.rb b/spec/requests/api/project_snapshots_spec.rb index 44b5ee1f130..2857715cdbe 100644 --- a/spec/requests/api/project_snapshots_spec.rb +++ b/spec/requests/api/project_snapshots_spec.rb @@ -6,6 +6,12 @@ describe API::ProjectSnapshots do let(:project) { create(:project) } let(:admin) { create(:admin) } + before do + allow(Feature::Gitaly).to receive(:server_feature_flags).and_return({ + 'gitaly-feature-foobar' => 'true' + }) + end + describe 'GET /projects/:id/snapshot' do def expect_snapshot_response_for(repository) type, params = workhorse_send_data @@ -13,6 +19,7 @@ describe API::ProjectSnapshots do expect(type).to eq('git-snapshot') expect(params).to eq( 'GitalyServer' => { + 'features' => { 'gitaly-feature-foobar' => 'true' }, 'address' => Gitlab::GitalyClient.address(repository.project.repository_storage), 'token' => Gitlab::GitalyClient.token(repository.project.repository_storage) }, diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb index 29f69b6ce20..58a28e636f1 100644 --- a/spec/requests/api/project_snippets_spec.rb +++ b/spec/requests/api/project_snippets_spec.rb @@ -96,6 +96,28 @@ describe API::ProjectSnippets do } end + context 'with a regular user' do + let(:user) { create(:user) } + + before do + project.add_developer(user) + stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC, Gitlab::VisibilityLevel::PRIVATE]) + params['visibility'] = 'internal' + end + + it 'creates a new snippet' do + post api("/projects/#{project.id}/snippets/", user), params: params + + expect(response).to have_gitlab_http_status(201) + snippet = ProjectSnippet.find(json_response['id']) + expect(snippet.content).to eq(params[:code]) + expect(snippet.description).to eq(params[:description]) + expect(snippet.title).to eq(params[:title]) + expect(snippet.file_name).to eq(params[:file_name]) + expect(snippet.visibility_level).to eq(Snippet::INTERNAL) + end + end + it 'creates a new snippet' do post api("/projects/#{project.id}/snippets/", admin), params: params @@ -108,6 +130,29 @@ describe API::ProjectSnippets do expect(snippet.visibility_level).to eq(Snippet::PUBLIC) end + it 'creates a new snippet with content parameter' do + params[:content] = params.delete(:code) + + post api("/projects/#{project.id}/snippets/", admin), params: params + + expect(response).to have_gitlab_http_status(201) + snippet = ProjectSnippet.find(json_response['id']) + expect(snippet.content).to eq(params[:content]) + expect(snippet.description).to eq(params[:description]) + expect(snippet.title).to eq(params[:title]) + expect(snippet.file_name).to eq(params[:file_name]) + expect(snippet.visibility_level).to eq(Snippet::PUBLIC) + end + + it 'returns 400 when both code and content parameters specified' do + params[:content] = params[:code] + + post api("/projects/#{project.id}/snippets/", admin), params: params + + expect(response).to have_gitlab_http_status(400) + expect(json_response['error']).to eq('code, content are mutually exclusive') + end + it 'returns 400 for missing parameters' do params.delete(:title) @@ -167,7 +212,20 @@ describe API::ProjectSnippets do new_content = 'New content' new_description = 'New description' - put api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin), params: { code: new_content, description: new_description } + put api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin), params: { code: new_content, description: new_description, visibility: 'private' } + + expect(response).to have_gitlab_http_status(200) + snippet.reload + expect(snippet.content).to eq(new_content) + expect(snippet.description).to eq(new_description) + expect(snippet.visibility).to eq('private') + end + + it 'updates snippet with content parameter' do + new_content = 'New content' + new_description = 'New description' + + put api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin), params: { content: new_content, description: new_description } expect(response).to have_gitlab_http_status(200) snippet.reload @@ -175,6 +233,13 @@ describe API::ProjectSnippets do expect(snippet.description).to eq(new_description) end + it 'returns 400 when both code and content parameters specified' do + put api("/projects/#{snippet.project.id}/snippets/1234", admin), params: { code: 'some content', content: 'other content' } + + expect(response).to have_gitlab_http_status(400) + expect(json_response['error']).to eq('code, content are mutually exclusive') + end + it 'returns 404 for invalid snippet id' do put api("/projects/#{snippet.project.id}/snippets/1234", admin), params: { title: 'foo' } diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb index d600076e9fb..cc05b8d5b45 100644 --- a/spec/requests/api/snippets_spec.rb +++ b/spec/requests/api/snippets_spec.rb @@ -193,18 +193,32 @@ describe API::Snippets do } end - it 'creates a new snippet' do - expect do - post api("/snippets/", user), params: params - end.to change { PersonalSnippet.count }.by(1) + shared_examples 'snippet creation' do + it 'creates a new snippet' do + expect do + post api("/snippets/", user), params: params + end.to change { PersonalSnippet.count }.by(1) + + expect(response).to have_gitlab_http_status(201) + expect(json_response['title']).to eq(params[:title]) + expect(json_response['description']).to eq(params[:description]) + expect(json_response['file_name']).to eq(params[:file_name]) + expect(json_response['visibility']).to eq(params[:visibility]) + end + end + + context 'with restricted visibility settings' do + before do + stub_application_setting(restricted_visibility_levels: + [Gitlab::VisibilityLevel::INTERNAL, + Gitlab::VisibilityLevel::PRIVATE]) + end - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq(params[:title]) - expect(json_response['description']).to eq(params[:description]) - expect(json_response['file_name']).to eq(params[:file_name]) - expect(json_response['visibility']).to eq(params[:visibility]) + it_behaves_like 'snippet creation' end + it_behaves_like 'snippet creation' + it 'returns 400 for missing parameters' do params.delete(:title) @@ -253,18 +267,33 @@ describe API::Snippets do create(:personal_snippet, author: user, visibility_level: visibility_level) end - it 'updates snippet' do - new_content = 'New content' - new_description = 'New description' + shared_examples 'snippet updates' do + it 'updates a snippet' do + new_content = 'New content' + new_description = 'New description' - put api("/snippets/#{snippet.id}", user), params: { content: new_content, description: new_description } + put api("/snippets/#{snippet.id}", user), params: { content: new_content, description: new_description, visibility: 'internal' } - expect(response).to have_gitlab_http_status(200) - snippet.reload - expect(snippet.content).to eq(new_content) - expect(snippet.description).to eq(new_description) + expect(response).to have_gitlab_http_status(200) + snippet.reload + expect(snippet.content).to eq(new_content) + expect(snippet.description).to eq(new_description) + expect(snippet.visibility).to eq('internal') + end end + context 'with restricted visibility settings' do + before do + stub_application_setting(restricted_visibility_levels: + [Gitlab::VisibilityLevel::PUBLIC, + Gitlab::VisibilityLevel::PRIVATE]) + end + + it_behaves_like 'snippet updates' + end + + it_behaves_like 'snippet updates' + it 'returns 404 for invalid snippet id' do put api("/snippets/1234", user), params: { title: 'foo' } diff --git a/spec/serializers/merge_request_serializer_spec.rb b/spec/serializers/merge_request_serializer_spec.rb index 276e0f6ff3d..d1483c3c41e 100644 --- a/spec/serializers/merge_request_serializer_spec.rb +++ b/spec/serializers/merge_request_serializer_spec.rb @@ -41,6 +41,14 @@ describe MergeRequestSerializer do end end + context 'noteable merge request serialization' do + let(:serializer) { 'noteable' } + + it 'matches noteable merge request json schema' do + expect(json_entity).to match_schema('entities/merge_request_noteable', strict: true) + end + end + context 'no serializer' do let(:serializer) { nil } diff --git a/spec/services/boards/lists/list_service_spec.rb b/spec/services/boards/lists/list_service_spec.rb index 2ebfd295fa2..2535f339495 100644 --- a/spec/services/boards/lists/list_service_spec.rb +++ b/spec/services/boards/lists/list_service_spec.rb @@ -3,13 +3,15 @@ require 'spec_helper' describe Boards::Lists::ListService do + let(:user) { create(:user) } + describe '#execute' do context 'when board parent is a project' do let(:project) { create(:project) } let(:board) { create(:board, project: project) } let(:label) { create(:label, project: project) } let!(:list) { create(:list, board: board, label: label) } - let(:service) { described_class.new(project, double) } + let(:service) { described_class.new(project, user) } it_behaves_like 'lists list service' end @@ -19,7 +21,7 @@ describe Boards::Lists::ListService do let(:board) { create(:board, group: group) } let(:label) { create(:group_label, group: group) } let!(:list) { create(:list, board: board, label: label) } - let(:service) { described_class.new(group, double) } + let(:service) { described_class.new(group, user) } it_behaves_like 'lists list service' end diff --git a/spec/services/boards/lists/update_service_spec.rb b/spec/services/boards/lists/update_service_spec.rb new file mode 100644 index 00000000000..f28bbab941a --- /dev/null +++ b/spec/services/boards/lists/update_service_spec.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Boards::Lists::UpdateService do + let(:user) { create(:user) } + let!(:list) { create(:list, board: board, position: 0) } + + shared_examples 'moving list' do + context 'when user can admin list' do + it 'calls Lists::MoveService to update list position' do + board.parent.add_developer(user) + service = described_class.new(board.parent, user, position: 1) + + expect(Boards::Lists::MoveService).to receive(:new).with(board.parent, user, { position: 1 }).and_call_original + expect_any_instance_of(Boards::Lists::MoveService).to receive(:execute).with(list) + + service.execute(list) + end + end + + context 'when user cannot admin list' do + it 'does not call Lists::MoveService to update list position' do + service = described_class.new(board.parent, user, position: 1) + + expect(Boards::Lists::MoveService).not_to receive(:new) + + service.execute(list) + end + end + end + + shared_examples 'updating list preferences' do + context 'when user can read list' do + it 'updates list preference for user' do + board.parent.add_guest(user) + service = described_class.new(board.parent, user, collapsed: true) + + service.execute(list) + + expect(list.preferences_for(user).collapsed).to eq(true) + end + end + + context 'when user cannot read list' do + it 'does not update list preference for user' do + service = described_class.new(board.parent, user, collapsed: true) + + service.execute(list) + + expect(list.preferences_for(user).collapsed).to be_nil + end + end + end + + describe '#execute' do + context 'when position parameter is present' do + context 'for projects' do + it_behaves_like 'moving list' do + let(:project) { create(:project, :private) } + let(:board) { create(:board, project: project) } + end + end + + context 'for groups' do + it_behaves_like 'moving list' do + let(:group) { create(:group, :private) } + let(:board) { create(:board, group: group) } + end + end + end + + context 'when collapsed parameter is present' do + context 'for projects' do + it_behaves_like 'updating list preferences' do + let(:project) { create(:project, :private) } + let(:board) { create(:board, project: project) } + end + end + + context 'for groups' do + it_behaves_like 'updating list preferences' do + let(:group) { create(:group, :private) } + let(:board) { create(:board, group: group) } + end + end + end + end +end diff --git a/spec/services/create_snippet_service_spec.rb b/spec/services/create_snippet_service_spec.rb index 9b83f65a17e..7d2491b3a49 100644 --- a/spec/services/create_snippet_service_spec.rb +++ b/spec/services/create_snippet_service_spec.rb @@ -34,6 +34,19 @@ describe CreateSnippetService do expect(snippet.errors.any?).to be_falsey expect(snippet.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) end + + describe "when visibility level is passed as a string" do + before do + @opts[:visibility] = 'internal' + @opts.delete(:visibility_level) + end + + it "assigns the correct visibility level" do + snippet = create_snippet(nil, @user, @opts) + expect(snippet.errors.any?).to be_falsey + expect(snippet.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL) + end + end end describe 'usage counter' do diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index f46f9633c1c..910fe3b50b7 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -212,6 +212,13 @@ describe SystemNoteService do expect(build_note([assignee, assignee1, assignee2], [assignee, assignee1])).to eq \ "unassigned @#{assignee2.username}" end + + it 'builds a correct phrase when the locale is different' do + Gitlab::I18n.with_locale('pt-BR') do + expect(build_note([assignee, assignee1, assignee2], [assignee3])).to eq \ + "assigned to @#{assignee3.username} and unassigned @#{assignee.username}, @#{assignee1.username}, and @#{assignee2.username}" + end + end end describe '.change_milestone' do diff --git a/spec/services/update_snippet_service_spec.rb b/spec/services/update_snippet_service_spec.rb index 0678f54c195..19b35dca6a7 100644 --- a/spec/services/update_snippet_service_spec.rb +++ b/spec/services/update_snippet_service_spec.rb @@ -32,12 +32,25 @@ describe UpdateSnippetService do expect(@snippet.visibility_level).to eq(old_visibility) end - it 'admins should be able to update to pubic visibility' do + it 'admins should be able to update to public visibility' do old_visibility = @snippet.visibility_level update_snippet(@project, @admin, @snippet, @opts) expect(@snippet.visibility_level).not_to eq(old_visibility) expect(@snippet.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) end + + describe "when visibility level is passed as a string" do + before do + @opts[:visibility] = 'internal' + @opts.delete(:visibility_level) + end + + it "assigns the correct visibility level" do + update_snippet(@project, @user, @snippet, @opts) + expect(@snippet.errors.any?).to be_falsey + expect(@snippet.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL) + end + end end describe 'usage counter' do diff --git a/spec/views/groups/edit.html.haml_spec.rb b/spec/views/groups/edit.html.haml_spec.rb index 47804411b9d..0da3470433c 100644 --- a/spec/views/groups/edit.html.haml_spec.rb +++ b/spec/views/groups/edit.html.haml_spec.rb @@ -23,7 +23,7 @@ describe 'groups/edit.html.haml' do render expect(rendered).to have_content("Prevent sharing a project within #{test_group.name} with other groups") - expect(rendered).to have_css('.descr', text: 'help text here') + expect(rendered).to have_css('.js-descr', text: 'help text here') expect(rendered).to have_field('group_share_with_group_lock', checkbox_options) end end diff --git a/vendor/licenses.csv b/vendor/licenses.csv index 0c445aeac88..20aacff75fd 100644 --- a/vendor/licenses.csv +++ b/vendor/licenses.csv @@ -67,7 +67,6 @@ @babel/template,7.1.2,MIT @babel/traverse,7.1.0,MIT @babel/types,7.1.2,MIT -@gitlab/csslab,1.8.0,MIT @gitlab/svgs,1.41.0,MIT @gitlab/ui,1.15.0,MIT @sindresorhus/is,0.7.0,MIT diff --git a/yarn.lock b/yarn.lock index 6d83f3f89ef..2a11e66e2b5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -964,13 +964,6 @@ exec-sh "^0.3.2" minimist "^1.2.0" -"@gitlab/csslab@^1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@gitlab/csslab/-/csslab-1.9.0.tgz#22fca5b1a30cbd9ca46fc6f9485ecbaba4dc300c" - integrity sha512-Zjayzokm7E2wgxUR/pxIMocdiBB5XHt2PEemdzD8qD+aQmMpMxSyIEMQk5Jq0Wgv+Rd5WXTolTw3kmb9l8ZeJg== - dependencies: - bootstrap "^4.1.3" - "@gitlab/eslint-config@^1.6.0": version "1.6.0" resolved "https://registry.yarnpkg.com/@gitlab/eslint-config/-/eslint-config-1.6.0.tgz#1fd247d6ab477d53d4c330e05f007e3afa303689" @@ -998,15 +991,15 @@ dependencies: vue-eslint-parser "^6.0.4" -"@gitlab/svgs@^1.70.0": - version "1.70.0" - resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.70.0.tgz#bdae478148c15d955fc06e69fd5d5ecae8298943" - integrity sha512-0uV9fgTwe17Fyy0hTcrsGX2jJuCrz3uRIe8yffuqc6pbQrSfYJyN66mfCCB45wq8lKTgOB5q0qcUyJx3RQEcKg== +"@gitlab/svgs@^1.71.0": + version "1.71.0" + resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.71.0.tgz#c8e6e8f500ea91e5cbba4ac08df533fb2e622a00" + integrity sha512-kkeNic/FFwaqKnzwio4NE7whBOZ/toRJ8cS0587DBotajAzSYhph5ij4TCY2GTjPa33zIJ5OUr/k90C0Kr71hQ== -"@gitlab/ui@5.19.0": - version "5.19.0" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-5.19.0.tgz#ef5431e0e91eb4009a30edcf49a40716cc2570d9" - integrity sha512-gCeTymtVzzO2uOWZGIweLM5JcHq3TN1QwP61CXw7b9Lp5A2MysRb8upehtR5d4JLOf/GQg8rHQB26uRvUc+AXQ== +"@gitlab/ui@5.20.1": + version "5.20.1" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-5.20.1.tgz#a7d479f2a19988eacd90a4864183f0c49ddd310f" + integrity sha512-fbqs5ncnqItgmXLlK4/ZoXwW+6DGBf+/nub2agoIT4EocwYGtrmAB8V+ybEcRrJVG8YpkCGt6R4lD24VrATWTw== dependencies: "@babel/standalone" "^7.0.0" "@gitlab/vue-toasted" "^1.2.1" @@ -2268,7 +2261,7 @@ bootstrap-vue@2.0.0-rc.27: portal-vue "^2.1.5" vue-functional-data-merge "^3.1.0" -bootstrap@4.3.1, bootstrap@^4.1.3, bootstrap@^4.3.1: +bootstrap@4.3.1, bootstrap@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.3.1.tgz#280ca8f610504d99d7b6b4bfc4b68cec601704ac" integrity sha512-rXqOmH1VilAt2DyPzluTi2blhk17bO7ef+zLLPlWvG494pDxcM234pJ8wTc/6R40UWizAIIMgxjvxZg5kmsbag== |