diff options
102 files changed, 1178 insertions, 943 deletions
diff --git a/.gitlab/ci/releases.gitlab-ci.yml b/.gitlab/ci/releases.gitlab-ci.yml index 1ddc4e90fcf..d4e0236f3a8 100644 --- a/.gitlab/ci/releases.gitlab-ci.yml +++ b/.gitlab/ci/releases.gitlab-ci.yml @@ -1,22 +1,36 @@ --- -# Syncs any changes pushed to a stable branch to the corresponding CE stable -# branch. We run this prior to any tests so that random failures don't prevent a -# sync. -sync-stable-branch: +# Syncs any changes pushed to a stable branch to the corresponding +# gitlab-foss/CE stable branch. We run this prior to any tests so that random +# failures don't prevent a sync. +.merge-train-sync: # We don't need/want any global before/after commands, so we overwrite these # settings. image: alpine:edge stage: sync - # This job should only run on EE stable branches on the canonical GitLab.com - # repository. - only: - variables: - - $CI_SERVER_HOST == "gitlab.com" - refs: - - /^[\d-]+-stable-ee$/@gitlab-org/gitlab before_script: - apk add --no-cache --update curl bash after_script: [] script: - bash scripts/sync-stable-branch.sh + only: + variables: + - $CI_SERVER_HOST == "gitlab.com" + +sync-stable-branch: + extends: .merge-train-sync + variables: + SOURCE_PROJECT: gitlab-org/gitlab + TARGET_PROJECT: gitlab-org/gitlab-foss + only: + refs: + - /^[\d-]+-stable-ee$/@gitlab-org/gitlab + +sync-security-branch: + extends: .merge-train-sync + variables: + SOURCE_PROJECT: gitlab-org/security/gitlab + TARGET_PROJECT: gitlab-org/security/gitlab-foss + only: + refs: + - /^[\d-]+-stable-ee$/@gitlab-org/security/gitlab diff --git a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue index 23e251e4201..8e2128ac713 100644 --- a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue +++ b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue @@ -12,13 +12,18 @@ import { GlDropdownItem, GlDropdownDivider, GlTooltipDirective, + GlPagination, } from '@gitlab/ui'; import AccessorUtils from '~/lib/utils/accessor'; import Icon from '~/vue_shared/components/icon.vue'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; import { __ } from '~/locale'; +import _ from 'underscore'; export default { + FIRST_PAGE: 1, + PREV_PAGE: 1, + NEXT_PAGE: 2, fields: [ { key: 'error', label: __('Open errors'), thClass: 'w-70p' }, { key: 'events', label: __('Events') }, @@ -42,6 +47,7 @@ export default { GlTable, GlFormInput, Icon, + GlPagination, TimeAgo, }, directives: { @@ -73,10 +79,28 @@ export default { data() { return { errorSearchQuery: '', + pageValue: this.$options.FIRST_PAGE, }; }, computed: { - ...mapState('list', ['errors', 'loading', 'searchQuery', 'sortField', 'recentSearches']), + ...mapState('list', [ + 'errors', + 'loading', + 'searchQuery', + 'sortField', + 'recentSearches', + 'pagination', + ]), + paginationRequired() { + return !_.isEmpty(this.pagination); + }, + }, + watch: { + pagination() { + if (typeof this.pagination.previous === 'undefined') { + this.pageValue = this.$options.FIRST_PAGE; + } + }, }, created() { if (this.errorTrackingEnabled) { @@ -103,6 +127,17 @@ export default { getDetailsLink(errorId) { return `error_tracking/${errorId}/details`; }, + goToNextPage() { + this.pageValue = this.$options.NEXT_PAGE; + this.startPolling(`${this.indexPath}?cursor=${this.pagination.next.cursor}`); + }, + goToPrevPage() { + this.startPolling(`${this.indexPath}?cursor=${this.pagination.previous.cursor}`); + }, + goToPage(page) { + window.scrollTo(0, 0); + return page === this.$options.PREV_PAGE ? this.goToPrevPage() : this.goToNextPage(); + }, isCurrentSortField(field) { return field === this.sortField; }, @@ -217,7 +252,6 @@ export default { </span> </div> </template> - <template slot="events" slot-scope="errors"> <div class="text-md-right">{{ errors.item.count }}</div> </template> @@ -240,6 +274,15 @@ export default { </div> </template> </gl-table> + <gl-pagination + v-show="!loading" + v-if="paginationRequired" + :prev-page="$options.PREV_PAGE" + :next-page="$options.NEXT_PAGE" + :value="pageValue" + align="center" + @input="goToPage" + /> </div> <div v-else-if="userCanEnableErrorTracking"> <gl-empty-state diff --git a/app/assets/javascripts/error_tracking/store/list/actions.js b/app/assets/javascripts/error_tracking/store/list/actions.js index b1c81b55e58..c9e882c4ed2 100644 --- a/app/assets/javascripts/error_tracking/store/list/actions.js +++ b/app/assets/javascripts/error_tracking/store/list/actions.js @@ -23,6 +23,7 @@ export function startPolling({ state, commit, dispatch }) { if (!data) { return; } + commit(types.SET_PAGINATION, data.pagination); commit(types.SET_ERRORS, data.errors); commit(types.SET_LOADING, false); dispatch('stopPolling'); diff --git a/app/assets/javascripts/error_tracking/store/list/mutation_types.js b/app/assets/javascripts/error_tracking/store/list/mutation_types.js index 3ebfef76324..301984a1ee0 100644 --- a/app/assets/javascripts/error_tracking/store/list/mutation_types.js +++ b/app/assets/javascripts/error_tracking/store/list/mutation_types.js @@ -4,6 +4,7 @@ export const SET_LOADING = 'SET_LOADING'; export const ADD_RECENT_SEARCH = 'ADD_RECENT_SEARCH'; export const CLEAR_RECENT_SEARCHES = 'CLEAR_RECENT_SEARCHES'; export const LOAD_RECENT_SEARCHES = 'LOAD_RECENT_SEARCHES'; +export const SET_PAGINATION = 'SET_PAGINATION'; export const SET_ENDPOINT = 'SET_ENDPOINT'; export const SET_SORT_FIELD = 'SET_SORT_FIELD'; export const SET_SEARCH_QUERY = 'SET_SEARCH_QUERY'; diff --git a/app/assets/javascripts/error_tracking/store/list/mutations.js b/app/assets/javascripts/error_tracking/store/list/mutations.js index 048660eaeeb..5648013bb89 100644 --- a/app/assets/javascripts/error_tracking/store/list/mutations.js +++ b/app/assets/javascripts/error_tracking/store/list/mutations.js @@ -44,6 +44,9 @@ export default { throw e; } }, + [types.SET_PAGINATION](state, pagination) { + state.pagination = pagination; + }, [types.SET_SORT_FIELD](state, field) { state.sortField = field; }, diff --git a/app/assets/javascripts/error_tracking/store/list/state.js b/app/assets/javascripts/error_tracking/store/list/state.js index f20b707142e..93dc1040fde 100644 --- a/app/assets/javascripts/error_tracking/store/list/state.js +++ b/app/assets/javascripts/error_tracking/store/list/state.js @@ -6,4 +6,5 @@ export default () => ({ searchQuery: null, indexPath: '', recentSearches: [], + pagination: {}, }); diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js index be7ee80656f..a29d9bf3b40 100644 --- a/app/assets/javascripts/ide/stores/utils.js +++ b/app/assets/javascripts/ide/stores/utils.js @@ -162,7 +162,7 @@ export const createCommitPayload = ({ }); export const createNewMergeRequestUrl = (projectUrl, source, target) => - `${projectUrl}/merge_requests/new?merge_request[source_branch]=${source}&merge_request[target_branch]=${target}&nav_source=webide`; + `${projectUrl}/-/merge_requests/new?merge_request[source_branch]=${source}&merge_request[target_branch]=${target}&nav_source=webide`; const sortTreesByTypeAndName = (a, b) => { if (a.type === 'tree' && b.type === 'blob') { diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 52674107df2..8d09e88e772 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -32,17 +32,17 @@ import { __ } from './locale'; // // <ul class="nav-links merge-request-tabs"> // <li class="notes-tab active"> -// <a data-action="notes" data-target="#notes" data-toggle="tab" href="/foo/bar/merge_requests/1"> +// <a data-action="notes" data-target="#notes" data-toggle="tab" href="/foo/bar/-/merge_requests/1"> // Discussion // </a> // </li> // <li class="commits-tab"> -// <a data-action="commits" data-target="#commits" data-toggle="tab" href="/foo/bar/merge_requests/1/commits"> +// <a data-action="commits" data-target="#commits" data-toggle="tab" href="/foo/bar/-/merge_requests/1/commits"> // Commits // </a> // </li> // <li class="diffs-tab"> -// <a data-action="diffs" data-target="#diffs" data-toggle="tab" href="/foo/bar/merge_requests/1/diffs"> +// <a data-action="diffs" data-target="#diffs" data-toggle="tab" href="/foo/bar/-/merge_requests/1/diffs"> // Diffs // </a> // </li> @@ -260,17 +260,17 @@ export default class MergeRequestTabs { // // Examples: // - // location.pathname # => "/namespace/project/merge_requests/1" + // location.pathname # => "/namespace/project/-/merge_requests/1" // setCurrentAction('diffs') - // location.pathname # => "/namespace/project/merge_requests/1/diffs" + // location.pathname # => "/namespace/project/-/merge_requests/1/diffs" // - // location.pathname # => "/namespace/project/merge_requests/1/diffs" + // location.pathname # => "/namespace/project/-/merge_requests/1/diffs" // setCurrentAction('show') - // location.pathname # => "/namespace/project/merge_requests/1" + // location.pathname # => "/namespace/project/-/merge_requests/1" // - // location.pathname # => "/namespace/project/merge_requests/1/diffs" + // location.pathname # => "/namespace/project/-/merge_requests/1/diffs" // setCurrentAction('commits') - // location.pathname # => "/namespace/project/merge_requests/1/commits" + // location.pathname # => "/namespace/project/-/merge_requests/1/commits" // // Returns the new URL String setCurrentAction(action) { diff --git a/app/controllers/concerns/uploads_actions.rb b/app/controllers/concerns/uploads_actions.rb index 9b3b2c4a482..655575e0944 100644 --- a/app/controllers/concerns/uploads_actions.rb +++ b/app/controllers/concerns/uploads_actions.rb @@ -44,15 +44,14 @@ module UploadsActions expires_in ttl, directives - disposition = uploader.embeddable? ? 'inline' : 'attachment' - - uploaders = [uploader, *uploader.versions.values] - uploader = uploaders.find { |version| version.filename == params[:filename] } + file_uploader = [uploader, *uploader.versions.values].find do |version| + version.filename == params[:filename] + end - return render_404 unless uploader + return render_404 unless file_uploader workhorse_set_content_type! - send_upload(uploader, attachment: uploader.filename, disposition: disposition) + send_upload(file_uploader, attachment: file_uploader.filename, disposition: content_disposition) end def authorize @@ -83,6 +82,14 @@ module UploadsActions end end + def content_disposition + if uploader.embeddable? || uploader.pdf? + 'inline' + else + 'attachment' + end + end + def uploader_class raise NotImplementedError end diff --git a/app/graphql/types/permission_types/snippet.rb b/app/graphql/types/permission_types/snippet.rb index 1e21efe790a..0fc13c60983 100644 --- a/app/graphql/types/permission_types/snippet.rb +++ b/app/graphql/types/permission_types/snippet.rb @@ -10,6 +10,7 @@ module Types permission_field :read_snippet, method: :can_read_snippet? permission_field :update_snippet, method: :can_update_snippet? permission_field :admin_snippet, method: :can_admin_snippet? + permission_field :report_snippet, method: :can_report_as_spam? end end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index c7fdd009edb..d683faf6a20 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -686,6 +686,7 @@ module ProjectsHelper error_tracking user gcp + logs ] end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 4077a868373..b6c71f81a49 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -23,7 +23,6 @@ module Ci belongs_to :runner belongs_to :trigger_request belongs_to :erased_by, class_name: 'User' - belongs_to :resource_group, class_name: 'Ci::ResourceGroup', inverse_of: :builds RUNNER_FEATURES = { upload_multiple_artifacts: -> (build) { build.publishes_artifacts_reports? }, @@ -35,7 +34,6 @@ module Ci }.freeze has_one :deployment, as: :deployable, class_name: 'Deployment' - has_one :resource, class_name: 'Ci::Resource', inverse_of: :build has_many :trace_sections, class_name: 'Ci::BuildTraceSection' has_many :trace_chunks, class_name: 'Ci::BuildTraceChunk', foreign_key: :build_id @@ -443,15 +441,6 @@ module Ci environment.present? end - def requires_resource? - Feature.enabled?(:ci_resource_group, project) && - self.resource_group_id.present? && resource.nil? - end - - def retains_resource? - self.resource_group_id.present? && resource.present? - end - def starts_environment? has_environment? && self.environment_action == 'start' end diff --git a/app/models/ci/resource.rb b/app/models/ci/resource.rb deleted file mode 100644 index ee5b6546165..00000000000 --- a/app/models/ci/resource.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -module Ci - class Resource < ApplicationRecord - extend Gitlab::Ci::Model - - belongs_to :resource_group, class_name: 'Ci::ResourceGroup', inverse_of: :resources - belongs_to :build, class_name: 'Ci::Build', inverse_of: :resource - - scope :free, -> { where(build: nil) } - scope :retained_by, -> (build) { where(build: build) } - end -end diff --git a/app/models/ci/resource_group.rb b/app/models/ci/resource_group.rb deleted file mode 100644 index fb562783d3d..00000000000 --- a/app/models/ci/resource_group.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -module Ci - class ResourceGroup < ApplicationRecord - extend Gitlab::Ci::Model - - belongs_to :project, inverse_of: :resource_groups - - has_many :resources, class_name: 'Ci::Resource', inverse_of: :resource_group - has_many :builds, class_name: 'Ci::Build', inverse_of: :resource_group - - validates :key, - length: { maximum: 255 }, - format: { with: Gitlab::Regex.environment_name_regex, - message: Gitlab::Regex.environment_name_regex_message } - - before_create :ensure_resource - - def retain_resource_for(build) - resources.free.limit(1).update_all(build_id: build.id) > 0 - end - - def release_resource_from(build) - resources.retained_by(build).update_all(build_id: nil) > 0 - end - - private - - def ensure_resource - # Currently we only support one resource per group, which means - # maximum one build can be set to the resource group, thus builds - # belong to the same resource group are executed once at time. - self.resources.build if self.resources.empty? - end - end -end diff --git a/app/models/project.rb b/app/models/project.rb index bc7aebee9e6..5ed47032dab 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -285,7 +285,6 @@ class Project < ApplicationRecord has_many :pipeline_schedules, class_name: 'Ci::PipelineSchedule' has_many :project_deploy_tokens has_many :deploy_tokens, through: :project_deploy_tokens - has_many :resource_groups, class_name: 'Ci::ResourceGroup', inverse_of: :project has_one :auto_devops, class_name: 'ProjectAutoDevops', inverse_of: :project, autosave: true has_many :custom_attributes, class_name: 'ProjectCustomAttribute' @@ -741,7 +740,7 @@ class Project < ApplicationRecord end def unlink_forks_upon_visibility_decrease_enabled? - Feature.enabled?(:unlink_fork_network_upon_visibility_decrease, self) + Feature.enabled?(:unlink_fork_network_upon_visibility_decrease, self, default_enabled: true) end def empty_repo? diff --git a/app/models/project_services/chat_message/merge_message.rb b/app/models/project_services/chat_message/merge_message.rb index 46313ba7bec..dc62a4c8908 100644 --- a/app/models/project_services/chat_message/merge_message.rb +++ b/app/models/project_services/chat_message/merge_message.rb @@ -62,7 +62,7 @@ module ChatMessage end def merge_request_url - "#{project_url}/merge_requests/#{merge_request_iid}" + "#{project_url}/-/merge_requests/#{merge_request_iid}" end # overridden in EE diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index 019bd54f48c..c92e8ecb31c 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -184,7 +184,7 @@ class HipchatService < Service description = obj_attr[:description] title = render_line(obj_attr[:title]) - merge_request_url = "#{project_url}/merge_requests/#{merge_request_id}" + merge_request_url = "#{project_url}/-/merge_requests/#{merge_request_id}" merge_request_link = "<a href=\"#{merge_request_url}\">merge request !#{merge_request_id}</a>" message = ["#{user_name} #{state} #{merge_request_link} in " \ "#{project_link}: <b>#{title}</b>"] diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb index e1efd84e510..d092a2de882 100644 --- a/app/policies/user_policy.rb +++ b/app/policies/user_policy.rb @@ -10,6 +10,9 @@ class UserPolicy < BasePolicy desc "The profile is private" condition(:private_profile, scope: :subject, score: 0) { @subject.private_profile? } + desc "The user is blocked" + condition(:blocked_user, scope: :subject, score: 0) { @subject.blocked? } + rule { ~restricted_public_level }.enable :read_user rule { ~anonymous }.enable :read_user @@ -20,5 +23,5 @@ class UserPolicy < BasePolicy end rule { default }.enable :read_user_profile - rule { private_profile & ~(user_is_self | admin) }.prevent :read_user_profile + rule { (private_profile | blocked_user) & ~(user_is_self | admin) }.prevent :read_user_profile end diff --git a/app/presenters/snippet_presenter.rb b/app/presenters/snippet_presenter.rb index 37c9ebd3305..a453be18b95 100644 --- a/app/presenters/snippet_presenter.rb +++ b/app/presenters/snippet_presenter.rb @@ -23,6 +23,10 @@ class SnippetPresenter < Gitlab::View::Presenter::Delegated can_access_resource?("admin") end + def can_report_as_spam? + snippet.submittable_as_spam_by?(current_user) + end + private def can_access_resource?(ability_prefix) diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb index 5abfbd26641..7a5e33c61ba 100644 --- a/app/services/ci/retry_build_service.rb +++ b/app/services/ci/retry_build_service.rb @@ -5,7 +5,7 @@ module Ci CLONE_ACCESSORS = %i[pipeline project ref tag options name allow_failure stage stage_id stage_idx trigger_request yaml_variables when environment coverage_regex - description tag_list protected needs resource_group].freeze + description tag_list protected needs].freeze def execute(build) reprocess!(build).tap do |new_build| diff --git a/app/views/users/_profile_basic_info.html.haml b/app/views/users/_profile_basic_info.html.haml new file mode 100644 index 00000000000..af0a766bab0 --- /dev/null +++ b/app/views/users/_profile_basic_info.html.haml @@ -0,0 +1,6 @@ +%p + %span.middle-dot-divider + @#{@user.username} + - if can?(current_user, :read_user_profile, @user) + %span.middle-dot-divider + = s_('Member since %{date}') % { date: @user.created_at.to_date.to_s(:long) } diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index e1c75d5d0f4..e10dad8aa8d 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -1,7 +1,7 @@ - @hide_top_links = true - @hide_breadcrumbs = true - @no_container = true -- page_title @user.name +- page_title @user.blocked? ? s_('UserProfile|Blocked user') : @user.name - page_description @user.bio - header_title @user.name, user_path(@user) @@ -36,50 +36,48 @@ = link_to avatar_icon_for_user(@user, 400), target: '_blank', rel: 'noopener noreferrer' do = image_tag avatar_icon_for_user(@user, 90), class: "avatar s90", alt: '' - .user-info - .cover-title - = @user.name - - - if @user.status - .cover-status - = emoji_icon(@user.status.emoji) - = markdown_field(@user.status, :message) - - .cover-desc.member-date.cgray - %p - %span.middle-dot-divider - @#{@user.username} - - if can?(current_user, :read_user_profile, @user) - %span.middle-dot-divider - = s_('Member since %{date}') % { date: @user.created_at.to_date.to_s(:long) } - - .cover-desc.cgray - - unless @user.public_email.blank? - .profile-link-holder.middle-dot-divider - = link_to @user.public_email, "mailto:#{@user.public_email}", class: 'text-link' - - unless @user.skype.blank? - .profile-link-holder.middle-dot-divider - = link_to "skype:#{@user.skype}", title: "Skype" do - = icon('skype') - - unless @user.linkedin.blank? - .profile-link-holder.middle-dot-divider - = link_to linkedin_url(@user), title: "LinkedIn", target: '_blank', rel: 'noopener noreferrer nofollow' do - = icon('linkedin-square') - - unless @user.twitter.blank? - .profile-link-holder.middle-dot-divider - = link_to twitter_url(@user), title: "Twitter", target: '_blank', rel: 'noopener noreferrer nofollow' do - = icon('twitter-square') - - unless @user.website_url.blank? - .profile-link-holder.middle-dot-divider - = link_to @user.short_website_url, @user.full_website_url, class: 'text-link', target: '_blank', rel: 'me noopener noreferrer nofollow' - - unless @user.location.blank? - .profile-link-holder.middle-dot-divider - = sprite_icon('location', size: 16, css_class: 'vertical-align-sub') - = @user.location - - unless @user.organization.blank? - .profile-link-holder.middle-dot-divider - = sprite_icon('work', size: 16, css_class: 'vertical-align-sub') - = @user.organization + - if @user.blocked? + .user-info + .cover-title + = s_('UserProfile|Blocked user') + = render "users/profile_basic_info" + - else + .user-info + .cover-title + = @user.name + + - if @user.status + .cover-status + = emoji_icon(@user.status.emoji) + = markdown_field(@user.status, :message) + = render "users/profile_basic_info" + .cover-desc.cgray + - unless @user.public_email.blank? + .profile-link-holder.middle-dot-divider + = link_to @user.public_email, "mailto:#{@user.public_email}", class: 'text-link' + - unless @user.skype.blank? + .profile-link-holder.middle-dot-divider + = link_to "skype:#{@user.skype}", title: "Skype" do + = icon('skype') + - unless @user.linkedin.blank? + .profile-link-holder.middle-dot-divider + = link_to linkedin_url(@user), title: "LinkedIn", target: '_blank', rel: 'noopener noreferrer nofollow' do + = icon('linkedin-square') + - unless @user.twitter.blank? + .profile-link-holder.middle-dot-divider + = link_to twitter_url(@user), title: "Twitter", target: '_blank', rel: 'noopener noreferrer nofollow' do + = icon('twitter-square') + - unless @user.website_url.blank? + .profile-link-holder.middle-dot-divider + = link_to @user.short_website_url, @user.full_website_url, class: 'text-link', target: '_blank', rel: 'me noopener noreferrer nofollow' + - unless @user.location.blank? + .profile-link-holder.middle-dot-divider + = sprite_icon('location', size: 16, css_class: 'vertical-align-sub') + = @user.location + - unless @user.organization.blank? + .profile-link-holder.middle-dot-divider + = sprite_icon('work', size: 16, css_class: 'vertical-align-sub') + = @user.organization - if @user.bio.present? .cover-desc.cgray @@ -165,4 +163,8 @@ .col-12.text-center .text-content %h4 - = s_('UserProfile|This user has a private profile') + - if @user.blocked? + = s_('UserProfile|This user is blocked') + - else + = s_('UserProfile|This user has a private profile') + diff --git a/changelogs/unreleased/35122-sentry-error-search-pagination-implement-pagination-in-sentry-error.yml b/changelogs/unreleased/35122-sentry-error-search-pagination-implement-pagination-in-sentry-error.yml new file mode 100644 index 00000000000..326fd3bf66b --- /dev/null +++ b/changelogs/unreleased/35122-sentry-error-search-pagination-implement-pagination-in-sentry-error.yml @@ -0,0 +1,5 @@ +--- +title: Implement pagination for sentry errors +merge_request: 21136 +author: +type: added diff --git a/changelogs/unreleased/ci-resource-group-model.yml b/changelogs/unreleased/ci-resource-group-model.yml deleted file mode 100644 index 98bc0159626..00000000000 --- a/changelogs/unreleased/ci-resource-group-model.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add Ci Resource Group models -merge_request: 20950 -author: -type: other diff --git a/changelogs/unreleased/dz-move-project-routes.yml b/changelogs/unreleased/dz-move-project-routes.yml new file mode 100644 index 00000000000..97029f23907 --- /dev/null +++ b/changelogs/unreleased/dz-move-project-routes.yml @@ -0,0 +1,5 @@ +--- +title: Move merge request routes under /-/ scope +merge_request: 21126 +author: +type: deprecated diff --git a/changelogs/unreleased/fix-padding-design-comments.yml b/changelogs/unreleased/fix-padding-design-comments.yml new file mode 100644 index 00000000000..e8422c7598a --- /dev/null +++ b/changelogs/unreleased/fix-padding-design-comments.yml @@ -0,0 +1,5 @@ +--- +title: Fix padding on the design comments +merge_request: 21839 +author: +type: fixed diff --git a/changelogs/unreleased/fj-add-report-permission-to-graphql-snippet-endpoint.yml b/changelogs/unreleased/fj-add-report-permission-to-graphql-snippet-endpoint.yml new file mode 100644 index 00000000000..434ed0bb437 --- /dev/null +++ b/changelogs/unreleased/fj-add-report-permission-to-graphql-snippet-endpoint.yml @@ -0,0 +1,5 @@ +--- +title: Add reportSnippet permission to Snippet GraphQL +merge_request: 21836 +author: +type: other diff --git a/changelogs/unreleased/issue_7105.yml b/changelogs/unreleased/issue_7105.yml new file mode 100644 index 00000000000..b85ee761725 --- /dev/null +++ b/changelogs/unreleased/issue_7105.yml @@ -0,0 +1,5 @@ +--- +title: Allow PDF attachments to be opened on browser +merge_request: 21272 +author: +type: added diff --git a/changelogs/unreleased/nicolasdular-allow-broadcast-message-styling.yml b/changelogs/unreleased/nicolasdular-allow-broadcast-message-styling.yml new file mode 100644 index 00000000000..d9e683673fc --- /dev/null +++ b/changelogs/unreleased/nicolasdular-allow-broadcast-message-styling.yml @@ -0,0 +1,5 @@ +--- +title: Allow styling broadcast messages +merge_request: 21522 +author: +type: added diff --git a/changelogs/unreleased/xanf-blocked-profile-page.yml b/changelogs/unreleased/xanf-blocked-profile-page.yml new file mode 100644 index 00000000000..0b2e54469ec --- /dev/null +++ b/changelogs/unreleased/xanf-blocked-profile-page.yml @@ -0,0 +1,5 @@ +--- +title: Hide profile information when user is blocked +merge_request: 21706 +author: +type: added diff --git a/config/routes/project.rb b/config/routes/project.rb index 1a5c58105b2..c29d673f315 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -260,6 +260,82 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do end end + resources :merge_requests, concerns: :awardable, except: [:new, :create, :show], constraints: { id: /\d+/ } do + member do + get :show # Insert this first to ensure redirections using merge_requests#show match this route + get :commit_change_content + post :merge + post :cancel_auto_merge + get :pipeline_status + get :ci_environments_status + post :toggle_subscription + post :remove_wip + post :assign_related_issues + get :discussions, format: :json + post :rebase + get :test_reports + get :exposed_artifacts + + scope constraints: ->(req) { req.format == :json }, as: :json do + get :commits + get :pipelines + get :diffs, to: 'merge_requests/diffs#show' + get :diffs_batch, to: 'merge_requests/diffs#diffs_batch' + get :diffs_metadata, to: 'merge_requests/diffs#diffs_metadata' + get :widget, to: 'merge_requests/content#widget' + get :cached_widget, to: 'merge_requests/content#cached_widget' + end + + scope action: :show do + get :commits, defaults: { tab: 'commits' } + get :pipelines, defaults: { tab: 'pipelines' } + get :diffs, defaults: { tab: 'diffs' } + end + + get :diff_for_path, controller: 'merge_requests/diffs' + + scope controller: 'merge_requests/conflicts' do + get :conflicts, action: :show + get :conflict_for_path + post :resolve_conflicts + end + end + + collection do + get :diff_for_path + post :bulk_update + end + + resources :discussions, only: [:show], constraints: { id: /\h{40}/ } do + member do + post :resolve + delete :resolve, action: :unresolve + end + end + end + + scope path: 'merge_requests', controller: 'merge_requests/creations' do + post '', action: :create, as: nil + + scope path: 'new', as: :new_merge_request do + get '', action: :new + + scope constraints: ->(req) { req.format == :json }, as: :json do + get :diffs + get :pipelines + end + + scope action: :new do + get :diffs, defaults: { tab: 'diffs' } + get :pipelines, defaults: { tab: 'pipelines' } + end + + get :diff_for_path + get :branch_from + get :branch_to + end + end + # The wiki routing contains wildcard characters so # its preferable to keep it below all other project routes draw :wiki @@ -317,82 +393,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do end end - resources :merge_requests, concerns: :awardable, except: [:new, :create, :show], constraints: { id: /\d+/ } do - member do - get :show # Insert this first to ensure redirections using merge_requests#show match this route - get :commit_change_content - post :merge - post :cancel_auto_merge - get :pipeline_status - get :ci_environments_status - post :toggle_subscription - post :remove_wip - post :assign_related_issues - get :discussions, format: :json - post :rebase - get :test_reports - get :exposed_artifacts - - scope constraints: ->(req) { req.format == :json }, as: :json do - get :commits - get :pipelines - get :diffs, to: 'merge_requests/diffs#show' - get :diffs_batch, to: 'merge_requests/diffs#diffs_batch' - get :diffs_metadata, to: 'merge_requests/diffs#diffs_metadata' - get :widget, to: 'merge_requests/content#widget' - get :cached_widget, to: 'merge_requests/content#cached_widget' - end - - scope action: :show do - get :commits, defaults: { tab: 'commits' } - get :pipelines, defaults: { tab: 'pipelines' } - get :diffs, defaults: { tab: 'diffs' } - end - - get :diff_for_path, controller: 'merge_requests/diffs' - - scope controller: 'merge_requests/conflicts' do - get :conflicts, action: :show - get :conflict_for_path - post :resolve_conflicts - end - end - - collection do - get :diff_for_path - post :bulk_update - end - - resources :discussions, only: [:show], constraints: { id: /\h{40}/ } do - member do - post :resolve - delete :resolve, action: :unresolve - end - end - end - - scope path: 'merge_requests', controller: 'merge_requests/creations' do - post '', action: :create, as: nil - - scope path: 'new', as: :new_merge_request do - get '', action: :new - - scope constraints: ->(req) { req.format == :json }, as: :json do - get :diffs - get :pipelines - end - - scope action: :new do - get :diffs, defaults: { tab: 'diffs' } - get :pipelines, defaults: { tab: 'pipelines' } - end - - get :diff_for_path - get :branch_from - get :branch_to - end - end - resources :pipelines, only: [:index, :new, :create, :show] do collection do resource :pipelines_settings, path: 'settings', only: [:show, :update] @@ -546,7 +546,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do :forks, :group_links, :import, :avatar, :mirror, :cycle_analytics, :mattermost, :variables, :triggers, :environments, :protected_environments, :error_tracking, - :serverless, :clusters, :audit_events, :wikis) + :serverless, :clusters, :audit_events, :wikis, :merge_requests) end # rubocop: disable Cop/PutProjectRoutesUnderScope diff --git a/db/migrate/20191128145231_add_ci_resource_groups.rb b/db/migrate/20191128145231_add_ci_resource_groups.rb deleted file mode 100644 index 0b730e47dda..00000000000 --- a/db/migrate/20191128145231_add_ci_resource_groups.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -class AddCiResourceGroups < ActiveRecord::Migration[5.2] - DOWNTIME = false - - def change - create_table :ci_resource_groups do |t| - t.timestamps_with_timezone - t.references :project, null: false, index: false, foreign_key: { on_delete: :cascade } - t.string :key, null: false, limit: 255 - t.index %i[project_id key], unique: true - end - - create_table :ci_resources do |t| - t.timestamps_with_timezone - t.references :resource_group, null: false, index: false, foreign_key: { to_table: :ci_resource_groups, on_delete: :cascade } - t.references :build, null: true, index: true, foreign_key: { to_table: :ci_builds, on_delete: :nullify } - t.index %i[resource_group_id build_id], unique: true - end - - add_column :ci_builds, :resource_group_id, :bigint - add_column :ci_builds, :waiting_for_resource_at, :datetime_with_timezone - end -end diff --git a/db/migrate/20191129144631_add_index_to_resource_group_id.rb b/db/migrate/20191129144631_add_index_to_resource_group_id.rb deleted file mode 100644 index 0e5a84f094d..00000000000 --- a/db/migrate/20191129144631_add_index_to_resource_group_id.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -class AddIndexToResourceGroupId < ActiveRecord::Migration[5.2] - include Gitlab::Database::MigrationHelpers - - DOWNTIME = false - INDEX_NAME = 'index_for_resource_group'.freeze - - disable_ddl_transaction! - - def up - add_concurrent_index :ci_builds, %i[resource_group_id id], where: 'resource_group_id IS NOT NULL', name: INDEX_NAME - add_concurrent_foreign_key :ci_builds, :ci_resource_groups, column: :resource_group_id, on_delete: :nullify - end - - def down - remove_foreign_key_if_exists :ci_builds, column: :resource_group_id - remove_concurrent_index_by_name :ci_builds, INDEX_NAME - end -end diff --git a/db/schema.rb b/db/schema.rb index 88c824cb708..ede50e7ed06 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -684,8 +684,6 @@ ActiveRecord::Schema.define(version: 2019_12_08_071112) do t.datetime_with_timezone "scheduled_at" t.string "token_encrypted" t.integer "upstream_pipeline_id" - t.bigint "resource_group_id" - t.datetime_with_timezone "waiting_for_resource_at" t.index ["artifacts_expire_at"], name: "index_ci_builds_on_artifacts_expire_at", where: "(artifacts_file <> ''::text)" t.index ["auto_canceled_by_id"], name: "index_ci_builds_on_auto_canceled_by_id" t.index ["commit_id", "artifacts_expire_at", "id"], name: "index_ci_builds_on_commit_id_and_artifacts_expireatandidpartial", where: "(((type)::text = 'Ci::Build'::text) AND ((retried = false) OR (retried IS NULL)) AND ((name)::text = ANY (ARRAY[('sast'::character varying)::text, ('dependency_scanning'::character varying)::text, ('sast:container'::character varying)::text, ('container_scanning'::character varying)::text, ('dast'::character varying)::text])))" @@ -699,7 +697,6 @@ ActiveRecord::Schema.define(version: 2019_12_08_071112) do t.index ["project_id"], name: "index_ci_builds_on_project_id_for_successfull_pages_deploy", where: "(((type)::text = 'GenericCommitStatus'::text) AND ((stage)::text = 'deploy'::text) AND ((name)::text = 'pages:deploy'::text) AND ((status)::text = 'success'::text))" t.index ["protected"], name: "index_ci_builds_on_protected" t.index ["queued_at"], name: "index_ci_builds_on_queued_at" - t.index ["resource_group_id", "id"], name: "index_for_resource_group", where: "(resource_group_id IS NOT NULL)" t.index ["runner_id"], name: "index_ci_builds_on_runner_id" t.index ["scheduled_at"], name: "partial_index_ci_builds_on_scheduled_at_with_scheduled_jobs", where: "((scheduled_at IS NOT NULL) AND ((type)::text = 'Ci::Build'::text) AND ((status)::text = 'scheduled'::text))" t.index ["stage_id", "stage_idx"], name: "tmp_build_stage_position_index", where: "(stage_idx IS NOT NULL)" @@ -874,23 +871,6 @@ ActiveRecord::Schema.define(version: 2019_12_08_071112) do t.index ["user_id"], name: "index_ci_pipelines_on_user_id" end - create_table "ci_resource_groups", force: :cascade do |t| - t.datetime_with_timezone "created_at", null: false - t.datetime_with_timezone "updated_at", null: false - t.bigint "project_id", null: false - t.string "key", limit: 255, null: false - t.index ["project_id", "key"], name: "index_ci_resource_groups_on_project_id_and_key", unique: true - end - - create_table "ci_resources", force: :cascade do |t| - t.datetime_with_timezone "created_at", null: false - t.datetime_with_timezone "updated_at", null: false - t.bigint "resource_group_id", null: false - t.bigint "build_id" - t.index ["build_id"], name: "index_ci_resources_on_build_id" - t.index ["resource_group_id", "build_id"], name: "index_ci_resources_on_resource_group_id_and_build_id", unique: true - end - create_table "ci_runner_namespaces", id: :serial, force: :cascade do |t| t.integer "runner_id" t.integer "namespace_id" @@ -4409,7 +4389,6 @@ ActiveRecord::Schema.define(version: 2019_12_08_071112) do add_foreign_key "ci_builds", "ci_pipelines", column: "auto_canceled_by_id", name: "fk_a2141b1522", on_delete: :nullify add_foreign_key "ci_builds", "ci_pipelines", column: "commit_id", name: "fk_d3130c9a7f", on_delete: :cascade add_foreign_key "ci_builds", "ci_pipelines", column: "upstream_pipeline_id", name: "fk_87f4cefcda", on_delete: :cascade - add_foreign_key "ci_builds", "ci_resource_groups", column: "resource_group_id", name: "fk_6661f4f0e8", on_delete: :nullify add_foreign_key "ci_builds", "ci_stages", column: "stage_id", name: "fk_3a9eaa254d", on_delete: :cascade add_foreign_key "ci_builds", "projects", name: "fk_befce0568a", on_delete: :cascade add_foreign_key "ci_builds_metadata", "ci_builds", column: "build_id", on_delete: :cascade @@ -4430,9 +4409,6 @@ ActiveRecord::Schema.define(version: 2019_12_08_071112) do add_foreign_key "ci_pipelines", "external_pull_requests", name: "fk_190998ef09", on_delete: :nullify add_foreign_key "ci_pipelines", "merge_requests", name: "fk_a23be95014", on_delete: :cascade add_foreign_key "ci_pipelines", "projects", name: "fk_86635dbd80", on_delete: :cascade - add_foreign_key "ci_resource_groups", "projects", on_delete: :cascade - add_foreign_key "ci_resources", "ci_builds", column: "build_id", on_delete: :nullify - add_foreign_key "ci_resources", "ci_resource_groups", column: "resource_group_id", on_delete: :cascade add_foreign_key "ci_runner_namespaces", "ci_runners", column: "runner_id", on_delete: :cascade add_foreign_key "ci_runner_namespaces", "namespaces", on_delete: :cascade add_foreign_key "ci_runner_projects", "projects", name: "fk_4478a6f1e4", on_delete: :cascade diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index 3cbc489f464..484841fd712 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -2899,6 +2899,51 @@ type IssueSetDueDatePayload { } """ +Autogenerated input type of IssueSetWeight +""" +input IssueSetWeightInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The iid of the issue to mutate + """ + iid: String! + + """ + The project the issue to mutate is in + """ + projectPath: ID! + + """ + The desired weight for the issue + """ + weight: Int! +} + +""" +Autogenerated return type of IssueSetWeight +""" +type IssueSetWeightPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Reasons why the mutation failed. + """ + errors: [String!]! + + """ + The issue after mutation + """ + issue: Issue +} + +""" Values for sorting issues """ enum IssueSort { @@ -3895,6 +3940,7 @@ type Mutation { epicTreeReorder(input: EpicTreeReorderInput!): EpicTreeReorderPayload issueSetConfidential(input: IssueSetConfidentialInput!): IssueSetConfidentialPayload issueSetDueDate(input: IssueSetDueDateInput!): IssueSetDueDatePayload + issueSetWeight(input: IssueSetWeightInput!): IssueSetWeightPayload mergeRequestSetAssignees(input: MergeRequestSetAssigneesInput!): MergeRequestSetAssigneesPayload mergeRequestSetLabels(input: MergeRequestSetLabelsInput!): MergeRequestSetLabelsPayload mergeRequestSetLocked(input: MergeRequestSetLockedInput!): MergeRequestSetLockedPayload @@ -5644,6 +5690,11 @@ type SnippetPermissions { readSnippet: Boolean! """ + Whether or not a user can perform `report_snippet` on this resource + """ + reportSnippet: Boolean! + + """ Whether or not a user can perform `update_snippet` on this resource """ updateSnippet: Boolean! diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index e47e2c8056c..f3437a26f42 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -6803,6 +6803,24 @@ "deprecationReason": null }, { + "name": "reportSnippet", + "description": "Whether or not a user can perform `report_snippet` on this resource", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "updateSnippet", "description": "Whether or not a user can perform `update_snippet` on this resource", "args": [ @@ -16077,6 +16095,33 @@ "deprecationReason": null }, { + "name": "issueSetWeight", + "description": null, + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "IssueSetWeightInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "IssueSetWeightPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "mergeRequestSetAssignees", "description": null, "args": [ @@ -19943,6 +19988,136 @@ }, { "kind": "OBJECT", + "name": "IssueSetWeightPayload", + "description": "Autogenerated return type of IssueSetWeight", + "fields": [ + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": "Reasons why the mutation failed.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "issue", + "description": "The issue after mutation", + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "Issue", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "IssueSetWeightInput", + "description": "Autogenerated input type of IssueSetWeight", + "fields": null, + "inputFields": [ + { + "name": "projectPath", + "description": "The project the issue to mutate is in", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "iid", + "description": "The iid of the issue to mutate", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "weight", + "description": "The desired weight for the issue", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", "name": "EpicTreeReorderPayload", "description": "Autogenerated return type of EpicTreeReorder", "fields": [ diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 9ddbd08f32b..1371daa6453 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -410,6 +410,14 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph | `errors` | String! => Array | Reasons why the mutation failed. | | `issue` | Issue | The issue after mutation | +### IssueSetWeightPayload + +| Name | Type | Description | +| --- | ---- | ---------- | +| `clientMutationId` | String | A unique identifier for the client performing the mutation. | +| `errors` | String! => Array | Reasons why the mutation failed. | +| `issue` | Issue | The issue after mutation | + ### Label | Name | Type | Description | @@ -834,6 +842,7 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph | `readSnippet` | Boolean! | Whether or not a user can perform `read_snippet` on this resource | | `updateSnippet` | Boolean! | Whether or not a user can perform `update_snippet` on this resource | | `adminSnippet` | Boolean! | Whether or not a user can perform `admin_snippet` on this resource | +| `reportSnippet` | Boolean! | Whether or not a user can perform `report_snippet` on this resource | ### Submodule diff --git a/doc/user/project/clusters/serverless/index.md b/doc/user/project/clusters/serverless/index.md index 81aff05a32a..cfb12acb83b 100644 --- a/doc/user/project/clusters/serverless/index.md +++ b/doc/user/project/clusters/serverless/index.md @@ -863,3 +863,21 @@ The instructions below relate to installing and running Certbot on a Linux serve After your changes are running on your Knative cluster, you can begin using the HTTPS protocol for secure access your deployed Knative services. In the event a mistake is made during this process and you need to update the cert, you will need to edit the gateway `knative-ingress-gateway` to switch back to `PASSTHROUGH` mode. Once corrections are made, edit the file again so the gateway will use the new certificates. + +## Using an older version of `gitlabktl` + +There may be situations where you want to run an older version of `gitlabktl`. This +requires setting an older version of the `gitlabktl` image in the `.gitlab-ci.yml file.` + +To set an older version, add `image:` to the `functions:deploy` block. For example: + +```yaml +functions:deploy: + extends: .serverless:deploy:functions + environment: production + image: registry.gitlab.com/gitlab-org/gitlabktl:0.5.0 +``` + +Different versions are available by changing the version tag at the end of the registry URL: `registry.gitlab.com/gitlab-org/gitlabktl:VERSION` + +For a full inventory of available `gitlabktl` versions please see [this list](https://gitlab.com/gitlab-org/gitlabktl/container_registry) diff --git a/lib/banzai/filter/base_sanitization_filter.rb b/lib/banzai/filter/base_sanitization_filter.rb index 2dabca3552d..1b7af8aee45 100644 --- a/lib/banzai/filter/base_sanitization_filter.rb +++ b/lib/banzai/filter/base_sanitization_filter.rb @@ -7,6 +7,7 @@ module Banzai # # - Banzai::Filter::SanitizationFilter (Markdown) # - Banzai::Filter::AsciiDocSanitizationFilter (AsciiDoc/Asciidoctor) + # - Banzai::Filter::BroadcastMessageSanitizationFilter (Markdown with styled links and line breaks) # # Extends HTML::Pipeline::SanitizationFilter with common rules. class BaseSanitizationFilter < HTML::Pipeline::SanitizationFilter diff --git a/lib/banzai/filter/broadcast_message_sanitization_filter.rb b/lib/banzai/filter/broadcast_message_sanitization_filter.rb new file mode 100644 index 00000000000..042293170c8 --- /dev/null +++ b/lib/banzai/filter/broadcast_message_sanitization_filter.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Banzai + module Filter + # Sanitize HTML produced by Markdown. Allows styling of links and usage of line breaks. + # + # Extends Banzai::Filter::BaseSanitizationFilter with specific rules. + class BroadcastMessageSanitizationFilter < Banzai::Filter::BaseSanitizationFilter + def customize_whitelist(whitelist) + whitelist[:elements].push('br') + + whitelist[:attributes]['a'].push('class', 'style') + + whitelist[:css] = { properties: %w(color border background padding margin text-decoration) } + + whitelist + end + end + end +end diff --git a/lib/banzai/pipeline/broadcast_message_pipeline.rb b/lib/banzai/pipeline/broadcast_message_pipeline.rb index 580b5b72474..e31795e673c 100644 --- a/lib/banzai/pipeline/broadcast_message_pipeline.rb +++ b/lib/banzai/pipeline/broadcast_message_pipeline.rb @@ -6,7 +6,7 @@ module Banzai def self.filters @filters ||= FilterArray[ Filter::MarkdownFilter, - Filter::SanitizationFilter, + Filter::BroadcastMessageSanitizationFilter, Filter::EmojiFilter, Filter::ColorFilter, diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb index 5164223876a..517caabc79e 100644 --- a/lib/gitlab/ci/config/entry/job.rb +++ b/lib/gitlab/ci/config/entry/job.rb @@ -16,8 +16,7 @@ module Gitlab ALLOWED_KEYS = %i[tags script only except rules type image services allow_failure type stage when start_in artifacts cache dependencies before_script needs after_script variables - environment coverage retry parallel extends interruptible timeout - resource_group].freeze + environment coverage retry parallel extends interruptible timeout].freeze REQUIRED_BY_NEEDS = %i[stage].freeze @@ -49,7 +48,6 @@ module Gitlab validates :dependencies, array_of_strings: true validates :extends, array_of_strings_or_string: true validates :rules, array_of_hashes: true - validates :resource_group, type: String end validates :start_in, duration: { limit: '1 week' }, if: :delayed? @@ -158,7 +156,7 @@ module Gitlab attributes :script, :tags, :allow_failure, :when, :dependencies, :needs, :retry, :parallel, :extends, :start_in, :rules, - :interruptible, :timeout, :resource_group + :interruptible, :timeout def self.matching?(name, config) !name.to_s.start_with?('.') && @@ -245,8 +243,7 @@ module Gitlab artifacts: artifacts_value, after_script: after_script_value, ignore: ignored?, - needs: needs_defined? ? needs_value : nil, - resource_group: resource_group } + needs: needs_defined? ? needs_value : nil } end end end diff --git a/lib/gitlab/ci/pipeline/chain/validate/external.rb b/lib/gitlab/ci/pipeline/chain/validate/external.rb index 97af42b5fd6..44dc333a6a1 100644 --- a/lib/gitlab/ci/pipeline/chain/validate/external.rb +++ b/lib/gitlab/ci/pipeline/chain/validate/external.rb @@ -38,7 +38,7 @@ module Gitlab raise InvalidResponseCode, "Unsupported response code received from Validation Service: #{response_code}" end rescue => ex - Gitlab::Sentry.track_exception(ex) + Gitlab::ErrorTracking.track_exception(ex) true end diff --git a/lib/gitlab/ci/pipeline/seed/build.rb b/lib/gitlab/ci/pipeline/seed/build.rb index 98b4b4593e0..590c7f4d1dd 100644 --- a/lib/gitlab/ci/pipeline/seed/build.rb +++ b/lib/gitlab/ci/pipeline/seed/build.rb @@ -18,7 +18,6 @@ module Gitlab @seed_attributes = attributes @previous_stages = previous_stages @needs_attributes = dig(:needs_attributes) - @resource_group_key = attributes.delete(:resource_group_key) @using_rules = attributes.key?(:rules) @using_only = attributes.key?(:only) @@ -79,7 +78,6 @@ module Gitlab else ::Ci::Build.new(attributes).tap do |job| job.deployment = Seed::Deployment.new(job).to_resource - job.resource_group = Seed::Build::ResourceGroup.new(job, @resource_group_key).to_resource end end end diff --git a/lib/gitlab/ci/pipeline/seed/build/resource_group.rb b/lib/gitlab/ci/pipeline/seed/build/resource_group.rb deleted file mode 100644 index 100eb1d4084..00000000000 --- a/lib/gitlab/ci/pipeline/seed/build/resource_group.rb +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Ci - module Pipeline - module Seed - class Build - class ResourceGroup < Seed::Base - include Gitlab::Utils::StrongMemoize - - attr_reader :build, :resource_group_key - - def initialize(build, resource_group_key) - @build = build - @resource_group_key = resource_group_key - end - - def to_resource - return unless Feature.enabled?(:ci_resource_group, build.project) - return unless resource_group_key.present? - - resource_group = build.project.resource_groups - .safe_find_or_create_by(key: expanded_resource_group_key) - - resource_group if resource_group.persisted? - end - - private - - def expanded_resource_group_key - strong_memoize(:expanded_resource_group_key) do - ExpandVariables.expand(resource_group_key, -> { build.simple_variables }) - end - end - end - end - end - end - end -end diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb index 7f3f518dfd7..8ac864eb5cf 100644 --- a/lib/gitlab/ci/yaml_processor.rb +++ b/lib/gitlab/ci/yaml_processor.rb @@ -64,7 +64,6 @@ module Gitlab except: job[:except], rules: job[:rules], cache: job[:cache], - resource_group_key: job[:resource_group], options: { image: job[:image], services: job[:services], diff --git a/lib/gitlab/file_type_detection.rb b/lib/gitlab/file_type_detection.rb index ca78d49f99b..e052792675a 100644 --- a/lib/gitlab/file_type_detection.rb +++ b/lib/gitlab/file_type_detection.rb @@ -20,6 +20,7 @@ module Gitlab module FileTypeDetection SAFE_IMAGE_EXT = %w[png jpg jpeg gif bmp tiff ico].freeze + PDF_EXT = 'pdf' # We recommend using the .mp4 format over .mov. Videos in .mov format can # still be used but you really need to make sure they are served with the # proper MIME type video/mp4 and not video/quicktime or your videos won't play @@ -46,6 +47,10 @@ module Gitlab extension_match?(SAFE_AUDIO_EXT) end + def pdf? + extension_match?([PDF_EXT]) + end + def embeddable? image? || video? || audio? end diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index f822e931bdb..2fd4f18b756 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -203,8 +203,6 @@ excluded_attributes: - :artifacts_metadata_store - :artifacts_size - :commands - - :resource_group_id - - :waiting_for_resource_at push_event_payload: - :event_id project_badges: diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index 5d907300d68..1438a7db001 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -38,12 +38,12 @@ module Gitlab IMPORTED_OBJECT_MAX_RETRIES = 5.freeze - EXISTING_OBJECT_CHECK = %i[milestone milestones label labels project_label project_labels group_label group_labels project_feature merge_request ProjectCiCdSetting].freeze + EXISTING_OBJECT_CHECK = %i[milestone milestones label labels project_label project_labels group_label group_labels project_feature merge_request ProjectCiCdSetting container_expiration_policy].freeze TOKEN_RESET_MODELS = %i[Project Namespace Ci::Trigger Ci::Build Ci::Runner ProjectHook].freeze # This represents all relations that have unique key on `project_id` - UNIQUE_RELATIONS = %i[project_feature ProjectCiCdSetting].freeze + UNIQUE_RELATIONS = %i[project_feature ProjectCiCdSetting container_expiration_policy].freeze def self.create(*args) new(*args).create diff --git a/locale/gitlab.pot b/locale/gitlab.pot index f25810f23c8..d6e15fba348 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -19625,6 +19625,9 @@ msgstr "" msgid "UserProfile|Already reported for abuse" msgstr "" +msgid "UserProfile|Blocked user" +msgstr "" + msgid "UserProfile|Contributed projects" msgstr "" @@ -19685,6 +19688,9 @@ msgstr "" msgid "UserProfile|This user hasn't starred any projects" msgstr "" +msgid "UserProfile|This user is blocked" +msgstr "" + msgid "UserProfile|View all" msgstr "" diff --git a/scripts/frontend/check_no_partial_karma_jest.sh b/scripts/frontend/check_no_partial_karma_jest.sh new file mode 100755 index 00000000000..0d0c897bb18 --- /dev/null +++ b/scripts/frontend/check_no_partial_karma_jest.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +karma_files=$(find spec/javascripts ee/spec/javascripts -type f -name '*_spec.js' -not -path '*/helpers/*') +violations="" + +for karma_file in $karma_files; do + jest_file=${karma_file/spec\/javascripts/"spec/frontend"} + + if [ -f $jest_file ]; then + violations="$violations $jest_file" + fi +done + +if [[ -z "$violations" ]]; then + echo "All good!" + exit 0 +else + echo "Danger! The following Jest specs have corresponding files in the Karma spec directory (i.e. spec/javascripts):" + echo "" + echo "------------------------------" + for file in $violations; do + echo $file + done + echo "------------------------------" + echo "" + echo "For each of these files, please either:" + echo "" + echo "1. Fully migrate the file to Jest and remove the corresponding Karma file." + echo "2. Remove the Jest file for now, make any relevant changes in the corresponding Karma file, and handle the migration to Jest in a separate MR." + echo "" + echo "Why is this a problem?" + echo "" + echo "- It's nice to have a single source of truth for the unit tests of a subject." + echo "- This will cause conflicts if the remaining Karma spec is migrated using our automated tool." + echo " https://gitlab.com/gitlab-org/frontend/playground/migrate-karma-to-jest" + echo "" + exit 1 +fi diff --git a/scripts/static-analysis b/scripts/static-analysis index b7f7100c365..1392a4f6a23 100755 --- a/scripts/static-analysis +++ b/scripts/static-analysis @@ -35,7 +35,8 @@ def jobs_to_run(node_index, node_total) %w[yarn run prettier-all], %w[bundle exec rubocop --parallel], %w[scripts/lint-conflicts.sh], - %w[scripts/lint-rugged] + %w[scripts/lint-rugged], + %w[scripts/frontend/check_no_partial_karma_jest.sh] ] case node_total diff --git a/scripts/sync-stable-branch.sh b/scripts/sync-stable-branch.sh index fc62453d743..b44bf26a151 100644 --- a/scripts/sync-stable-branch.sh +++ b/scripts/sync-stable-branch.sh @@ -23,10 +23,24 @@ then exit 1 fi +if [[ "$SOURCE_PROJECT" == '' ]] +then + echo 'The variable SOURCE_PROJECT must be set to a non-empy value' + exit 1 +fi + +if [[ "$TARGET_PROJECT" == '' ]] +then + echo 'The variable TARGET_PROJECT must be set to a non-empy value' + exit 1 +fi + curl -X POST \ -F token="$MERGE_TRAIN_TRIGGER_TOKEN" \ -F ref=master \ -F "variables[MERGE_FOSS]=1" \ -F "variables[SOURCE_BRANCH]=$CI_COMMIT_REF_NAME" \ -F "variables[TARGET_BRANCH]=${CI_COMMIT_REF_NAME/-ee/}" \ + -F "variables[SOURCE_PROJECT]=$SOURCE_PROJECT" \ + -F "variables[TARGET_PROJECT]=$TARGET_PROJECT" \ "$MERGE_TRAIN_TRIGGER_URL" diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb index f35babc1b56..ff15e685007 100644 --- a/spec/controllers/uploads_controller_spec.rb +++ b/spec/controllers/uploads_controller_spec.rb @@ -196,24 +196,39 @@ describe UploadsController do describe "GET show" do context 'Content-Disposition security measures' do + let(:expected_disposition) { 'inline;' } let(:project) { create(:project, :public) } - context 'for PNG files' do - it 'returns Content-Disposition: inline' do - note = create(:note, :with_attachment, project: project) - get :show, params: { model: 'note', mounted_as: 'attachment', id: note.id, filename: 'dk.png' } + shared_examples_for 'uploaded file with disposition' do + it 'returns correct Content-Disposition' do + get :show, params: { model: 'note', mounted_as: 'attachment', id: note.id, filename: filename } - expect(response['Content-Disposition']).to start_with('inline;') + expect(response['Content-Disposition']).to start_with(expected_disposition) end end + context 'for PNG files' do + let(:filename) { 'dk.png' } + let(:expected_disposition) { 'inline;' } + let(:note) { create(:note, :with_attachment, project: project) } + + it_behaves_like 'uploaded file with disposition' + end + + context 'for PDF files' do + let(:filename) { 'git-cheat-sheet.pdf' } + let(:expected_disposition) { 'inline;' } + let(:note) { create(:note, :with_pdf_attachment, project: project) } + + it_behaves_like 'uploaded file with disposition' + end + context 'for SVG files' do - it 'returns Content-Disposition: attachment' do - note = create(:note, :with_svg_attachment, project: project) - get :show, params: { model: 'note', mounted_as: 'attachment', id: note.id, filename: 'unsanitized.svg' } + let(:filename) { 'unsanitized.svg' } + let(:expected_disposition) { 'attachment;' } + let(:note) { create(:note, :with_svg_attachment, project: project) } - expect(response['Content-Disposition']).to start_with('attachment;') - end + it_behaves_like 'uploaded file with disposition' end end diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index a38935c89ba..ecb1f1996d9 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -207,14 +207,6 @@ FactoryBot.define do trigger_request factory: :ci_trigger_request end - trait :resource_group do - waiting_for_resource_at { 5.minutes.ago } - - after(:build) do |build, evaluator| - build.resource_group = create(:ci_resource_group, project: build.project) - end - end - after(:build) do |build, evaluator| build.project ||= build.pipeline.project end diff --git a/spec/factories/ci/resource.rb b/spec/factories/ci/resource.rb deleted file mode 100644 index d47b3ba4635..00000000000 --- a/spec/factories/ci/resource.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -FactoryBot.define do - factory :ci_resource, class: Ci::Resource do - resource_group factory: :ci_resource_group - - trait(:retained) do - build factory: :ci_build - end - end -end diff --git a/spec/factories/ci/resource_group.rb b/spec/factories/ci/resource_group.rb deleted file mode 100644 index bdfc0740a45..00000000000 --- a/spec/factories/ci/resource_group.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -FactoryBot.define do - factory :ci_resource_group, class: Ci::ResourceGroup do - project - sequence(:key) { |n| "IOS_#{n}" } - end -end diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb index 2f02acca794..330f5276422 100644 --- a/spec/factories/notes.rb +++ b/spec/factories/notes.rb @@ -167,6 +167,10 @@ FactoryBot.define do attachment { fixture_file_upload("spec/fixtures/unsanitized.svg", "image/svg+xml") } end + trait :with_pdf_attachment do + attachment { fixture_file_upload("spec/fixtures/git-cheat-sheet.pdf", "application/pdf") } + end + transient do in_reply_to { nil } end diff --git a/spec/features/users/show_spec.rb b/spec/features/users/show_spec.rb index 932c1d8d4bd..8c2b555305a 100644 --- a/spec/features/users/show_spec.rb +++ b/spec/features/users/show_spec.rb @@ -59,6 +59,42 @@ describe 'User page' do end end + context 'with blocked profile' do + let(:user) { create(:user, state: :blocked) } + + it 'shows no tab' do + visit(user_path(user)) + + expect(page).to have_css("div.profile-header") + expect(page).not_to have_css("ul.nav-links") + end + + it 'shows blocked message' do + visit(user_path(user)) + + expect(page).to have_content("This user is blocked") + end + + it 'shows user name as blocked' do + visit(user_path(user)) + + expect(page).to have_css(".cover-title", text: 'Blocked user') + end + + it 'shows no additional fields' do + visit(user_path(user)) + + expect(page).not_to have_css(".profile-user-bio") + expect(page).not_to have_css(".profile-link-holder") + end + + it 'shows username' do + visit(user_path(user)) + + expect(page).to have_content("@#{user.username}") + end + end + it 'shows the status if there was one' do create(:user_status, user: user, message: "Working hard!") diff --git a/spec/fixtures/lib/gitlab/import_export/complex/project.json b/spec/fixtures/lib/gitlab/import_export/complex/project.json index acfd6a6924a..583d6c7b78a 100644 --- a/spec/fixtures/lib/gitlab/import_export/complex/project.json +++ b/spec/fixtures/lib/gitlab/import_export/complex/project.json @@ -6757,6 +6757,17 @@ "updated_at": "2017-01-16T15:25:29.637Z" } ], + "container_expiration_policy": { + "created_at": "2019-12-13 13:45:04 UTC", + "updated_at": "2019-12-13 13:45:04 UTC", + "next_run_at": null, + "project_id": 5, + "name_regex": null, + "cadence": "3month", + "older_than": null, + "keep_n": 100, + "enabled": false + }, "deploy_keys": [], "services": [ { diff --git a/spec/frontend/error_tracking/components/error_tracking_list_spec.js b/spec/frontend/error_tracking/components/error_tracking_list_spec.js index 9ec3d42f0d4..581581405b6 100644 --- a/spec/frontend/error_tracking/components/error_tracking_list_spec.js +++ b/spec/frontend/error_tracking/components/error_tracking_list_spec.js @@ -8,8 +8,8 @@ import { GlFormInput, GlDropdown, GlDropdownItem, + GlPagination, } from '@gitlab/ui'; -import createListState from '~/error_tracking/store/list/state'; import ErrorTrackingList from '~/error_tracking/components/error_tracking_list.vue'; import errorsList from './list_mock.json'; @@ -27,13 +27,16 @@ describe('ErrorTrackingList', () => { const findRecentSearchesDropdown = () => wrapper.find('.filtered-search-history-dropdown-wrapper'); const findLoadingIcon = () => wrapper.find(GlLoadingIcon); + const findPagination = () => wrapper.find(GlPagination); function mountComponent({ errorTrackingEnabled = true, userCanEnableErrorTracking = true, + sync = true, stubs = { 'gl-link': GlLink, 'gl-table': GlTable, + 'gl-pagination': GlPagination, 'gl-dropdown': GlDropdown, 'gl-dropdown-item': GlDropdownItem, }, @@ -41,6 +44,7 @@ describe('ErrorTrackingList', () => { wrapper = shallowMount(ErrorTrackingList, { localVue, store, + sync, propsData: { indexPath: '/path', enableErrorTrackingLink: '/link', @@ -69,7 +73,20 @@ describe('ErrorTrackingList', () => { sortByField: jest.fn(), }; - const state = createListState(); + const state = { + indexPath: '', + recentSearches: [], + errors: errorsList, + loading: true, + pagination: { + previous: { + cursor: 'previousCursor', + }, + next: { + cursor: 'nextCursor', + }, + }, + }; store = new Vuex.Store({ modules: { @@ -252,4 +269,65 @@ describe('ErrorTrackingList', () => { }); }); }); + + describe('When pagination is not required', () => { + beforeEach(() => { + store.state.list.pagination = {}; + mountComponent(); + }); + + it('should not render the pagination component', () => { + expect(findPagination().exists()).toBe(false); + }); + }); + + describe('When pagination is required', () => { + describe('and the user is on the first page', () => { + beforeEach(() => { + mountComponent({ sync: false }); + }); + + it('shows a disabled Prev button', () => { + expect(wrapper.find('.prev-page-item').attributes('aria-disabled')).toBe('true'); + }); + }); + + describe('and the user is not on the first page', () => { + describe('and the previous button is clicked', () => { + beforeEach(() => { + mountComponent({ sync: false }); + wrapper.setData({ pageValue: 2 }); + }); + + it('fetches the previous page of results', () => { + expect(wrapper.find('.prev-page-item').attributes('aria-disabled')).toBe(undefined); + wrapper.vm.goToPrevPage(); + expect(actions.startPolling).toHaveBeenCalledTimes(2); + expect(actions.startPolling).toHaveBeenLastCalledWith( + expect.anything(), + '/path?cursor=previousCursor', + undefined, + ); + }); + }); + + describe('and the next page button is clicked', () => { + beforeEach(() => { + mountComponent({ sync: false }); + }); + + it('fetches the next page of results', () => { + window.scrollTo = jest.fn(); + findPagination().vm.$emit('input', 2); + expect(window.scrollTo).toHaveBeenCalledWith(0, 0); + expect(actions.startPolling).toHaveBeenCalledTimes(2); + expect(actions.startPolling).toHaveBeenLastCalledWith( + expect.anything(), + '/path?cursor=nextCursor', + undefined, + ); + }); + }); + }); + }); }); diff --git a/spec/frontend/error_tracking/store/list/actions_spec.js b/spec/frontend/error_tracking/store/list/actions_spec.js index fb659db9ab5..7906738f5b0 100644 --- a/spec/frontend/error_tracking/store/list/actions_spec.js +++ b/spec/frontend/error_tracking/store/list/actions_spec.js @@ -30,6 +30,7 @@ describe('error tracking actions', () => { {}, [ { type: types.SET_LOADING, payload: true }, + { type: types.SET_PAGINATION, payload: payload.pagination }, { type: types.SET_ERRORS, payload: payload.errors }, { type: types.SET_LOADING, payload: false }, ], diff --git a/spec/frontend/notes/mock_data.js b/spec/frontend/notes/mock_data.js index 01cb70d395c..9ed79c61c22 100644 --- a/spec/frontend/notes/mock_data.js +++ b/spec/frontend/notes/mock_data.js @@ -52,7 +52,7 @@ export const noteableDataMock = { time_estimate: 0, title: '14', total_time_spent: 0, - noteable_note_url: '/group/project/merge_requests/1#note_1', + noteable_note_url: '/group/project/-/merge_requests/1#note_1', updated_at: '2017-08-04T09:53:01.226Z', updated_by_id: 1, web_url: '/gitlab-org/gitlab-foss/issues/26', @@ -101,8 +101,8 @@ export const individualNote = { { name: 'art', user: { id: 1, name: 'Root', username: 'root' } }, ], toggle_award_path: '/gitlab-org/gitlab-foss/notes/1390/toggle_award_emoji', - noteable_note_url: '/group/project/merge_requests/1#note_1', - note_url: '/group/project/merge_requests/1#note_1', + noteable_note_url: '/group/project/-/merge_requests/1#note_1', + note_url: '/group/project/-/merge_requests/1#note_1', report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1390&user_id=1', path: '/gitlab-org/gitlab-foss/notes/1390', @@ -161,8 +161,8 @@ export const note = { }, ], toggle_award_path: '/gitlab-org/gitlab-foss/notes/546/toggle_award_emoji', - note_url: '/group/project/merge_requests/1#note_1', - noteable_note_url: '/group/project/merge_requests/1#note_1', + note_url: '/group/project/-/merge_requests/1#note_1', + noteable_note_url: '/group/project/-/merge_requests/1#note_1', report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_546&user_id=1', path: '/gitlab-org/gitlab-foss/notes/546', @@ -205,7 +205,7 @@ export const discussionMock = { discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1', emoji_awardable: true, award_emoji: [], - noteable_note_url: '/group/project/merge_requests/1#note_1', + noteable_note_url: '/group/project/-/merge_requests/1#note_1', toggle_award_path: '/gitlab-org/gitlab-foss/notes/1395/toggle_award_emoji', report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1395&user_id=1', @@ -253,7 +253,7 @@ export const discussionMock = { emoji_awardable: true, award_emoji: [], toggle_award_path: '/gitlab-org/gitlab-foss/notes/1396/toggle_award_emoji', - noteable_note_url: '/group/project/merge_requests/1#note_1', + noteable_note_url: '/group/project/-/merge_requests/1#note_1', report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1396&user_id=1', path: '/gitlab-org/gitlab-foss/notes/1396', @@ -299,7 +299,7 @@ export const discussionMock = { discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1', emoji_awardable: true, award_emoji: [], - noteable_note_url: '/group/project/merge_requests/1#note_1', + noteable_note_url: '/group/project/-/merge_requests/1#note_1', toggle_award_path: '/gitlab-org/gitlab-foss/notes/1437/toggle_award_emoji', report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1437&user_id=1', @@ -349,7 +349,7 @@ export const loggedOutnoteableData = { can_create_note: false, can_update: false, }, - noteable_note_url: '/group/project/merge_requests/1#note_1', + noteable_note_url: '/group/project/-/merge_requests/1#note_1', create_note_path: '/gitlab-org/gitlab-foss/notes?target_id=98&target_type=issue', preview_note_path: '/gitlab-org/gitlab-foss/preview_markdown?target_id=98&target_type=Issue', }; @@ -483,7 +483,7 @@ export const INDIVIDUAL_NOTE_RESPONSE_MAP = { }, }, ], - noteable_note_url: '/group/project/merge_requests/1#note_1', + noteable_note_url: '/group/project/-/merge_requests/1#note_1', toggle_award_path: '/gitlab-org/gitlab-foss/notes/1390/toggle_award_emoji', report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1390\u0026user_id=1', @@ -528,7 +528,7 @@ export const INDIVIDUAL_NOTE_RESPONSE_MAP = { discussion_id: '70d5c92a4039a36c70100c6691c18c27e4b0a790', emoji_awardable: true, award_emoji: [], - noteable_note_url: '/group/project/merge_requests/1#note_1', + noteable_note_url: '/group/project/-/merge_requests/1#note_1', toggle_award_path: '/gitlab-org/gitlab-foss/notes/1391/toggle_award_emoji', report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1391\u0026user_id=1', @@ -583,7 +583,7 @@ export const INDIVIDUAL_NOTE_RESPONSE_MAP = { discussion_id: 'a3ed36e29b1957efb3b68c53e2d7a2b24b1df052', emoji_awardable: true, award_emoji: [], - noteable_note_url: '/group/project/merge_requests/1#note_1', + noteable_note_url: '/group/project/-/merge_requests/1#note_1', toggle_award_path: '/gitlab-org/gitlab-foss/notes/1471/toggle_award_emoji', report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F29%23note_1471\u0026user_id=1', @@ -635,7 +635,7 @@ export const DISCUSSION_NOTE_RESPONSE_MAP = { emoji_awardable: true, award_emoji: [], toggle_award_path: '/gitlab-org/gitlab-foss/notes/1471/toggle_award_emoji', - noteable_note_url: '/group/project/merge_requests/1#note_1', + noteable_note_url: '/group/project/-/merge_requests/1#note_1', report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F29%23note_1471\u0026user_id=1', path: '/gitlab-org/gitlab-foss/notes/1471', diff --git a/spec/graphql/types/permission_types/snippet_spec.rb b/spec/graphql/types/permission_types/snippet_spec.rb index 71843153d43..66e9fa6dfdb 100644 --- a/spec/graphql/types/permission_types/snippet_spec.rb +++ b/spec/graphql/types/permission_types/snippet_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' describe Types::PermissionTypes::Snippet do it 'returns the snippets permissions' do expected_permissions = [ - :create_note, :award_emoji, :read_snippet, :update_snippet, :admin_snippet + :create_note, :award_emoji, :read_snippet, :update_snippet, :admin_snippet, :report_snippet ] expected_permissions.each do |permission| diff --git a/spec/helpers/award_emoji_helper_spec.rb b/spec/helpers/award_emoji_helper_spec.rb index 2ad6b68a34c..975f32edd42 100644 --- a/spec/helpers/award_emoji_helper_spec.rb +++ b/spec/helpers/award_emoji_helper_spec.rb @@ -51,7 +51,7 @@ describe AwardEmojiHelper do it 'returns correct url' do @project = merge_request.project - expected_url = "/#{@project.namespace.path}/#{@project.path}/merge_requests/#{merge_request.iid}/toggle_award_emoji" + expected_url = "/#{@project.namespace.path}/#{@project.path}/-/merge_requests/#{merge_request.iid}/toggle_award_emoji" expect(subject).to eq(expected_url) end diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb index 7853617c3ed..63a37a1f113 100644 --- a/spec/helpers/events_helper_spec.rb +++ b/spec/helpers/events_helper_spec.rb @@ -115,7 +115,7 @@ describe EventsHelper do it 'returns a merge request url' do event.target = create(:note_on_merge_request, note: 'LGTM!') - expect(subject).to eq("#{project_base_url}/merge_requests/#{event.note_target.iid}#note_#{event.target.id}") + expect(subject).to eq("#{project_base_url}/-/merge_requests/#{event.note_target.iid}#note_#{event.target.id}") end end end diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb index 3238743ee26..e2dff05cfaa 100644 --- a/spec/helpers/labels_helper_spec.rb +++ b/spec/helpers/labels_helper_spec.rb @@ -78,13 +78,21 @@ describe LabelsHelper do end context 'with a type argument' do - ['issue', :issue, 'merge_request', :merge_request].each do |type| + ['issue', :issue].each do |type| context "set to #{type}" do it 'links to correct page' do expect(link_to_label(label_presenter, type: type)).to match %r{<a href="/#{label.project.full_path}/#{type.to_s.pluralize}\?label_name%5B%5D=#{label.name}">.*</a>} end end end + + ['merge_request', :merge_request].each do |type| + context "set to #{type}" do + it 'links to correct page' do + expect(link_to_label(label_presenter, type: type)).to match %r{<a href="/#{label.project.full_path}/-/#{type.to_s.pluralize}\?label_name%5B%5D=#{label.name}">.*</a>} + end + end + end end context 'with a tooltip argument' do diff --git a/spec/javascripts/ide/stores/modules/commit/actions_spec.js b/spec/javascripts/ide/stores/modules/commit/actions_spec.js index cbc2401262f..557244e237e 100644 --- a/spec/javascripts/ide/stores/modules/commit/actions_spec.js +++ b/spec/javascripts/ide/stores/modules/commit/actions_spec.js @@ -461,7 +461,7 @@ describe('IDE commit module actions', () => { .dispatch('commit/commitChanges') .then(() => { expect(visitUrl).toHaveBeenCalledWith( - `webUrl/merge_requests/new?merge_request[source_branch]=${ + `webUrl/-/merge_requests/new?merge_request[source_branch]=${ store.getters['commit/placeholderBranchName'] }&merge_request[target_branch]=master&nav_source=webide`, ); diff --git a/spec/javascripts/jobs/components/stages_dropdown_spec.js b/spec/javascripts/jobs/components/stages_dropdown_spec.js index e091aece564..f1a01530104 100644 --- a/spec/javascripts/jobs/components/stages_dropdown_spec.js +++ b/spec/javascripts/jobs/components/stages_dropdown_spec.js @@ -27,7 +27,7 @@ describe('Stages Dropdown', () => { }, merge_request: { iid: 1234, - path: '/root/detached-merge-request-pipelines/merge_requests/1', + path: '/root/detached-merge-request-pipelines/-/merge_requests/1', title: 'Update README.md', source_branch: 'feature-1234', source_branch_path: '/root/detached-merge-request-pipelines/branches/feature-1234', diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js index dc61482fdf3..b6173b9b171 100644 --- a/spec/javascripts/merge_request_spec.js +++ b/spec/javascripts/merge_request_spec.js @@ -17,7 +17,7 @@ describe('MergeRequest', function() { mock = new MockAdapter(axios); mock - .onPatch(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`) + .onPatch(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/-/merge_requests/1.json`) .reply(200, {}); this.merge = new MergeRequest(); @@ -75,7 +75,7 @@ describe('MergeRequest', function() { setTimeout(() => { expect(axios.patch).toHaveBeenCalledWith( - `${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`, + `${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/-/merge_requests/1.json`, { merge_request: { description: '- [ ] Task List Item\n- [ ] \n- [ ] Task List Item 2\n', @@ -93,7 +93,9 @@ describe('MergeRequest', function() { // eslint-disable-next-line jasmine/no-disabled-tests xit('shows an error notification when tasklist update failed', done => { mock - .onPatch(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`) + .onPatch( + `${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/-/merge_requests/1.json`, + ) .reply(409, {}); $('.js-task-list-field').trigger({ diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index 73b1ea4d36f..1672cf69485 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -147,53 +147,53 @@ describe('MergeRequestTabs', function() { it('changes from commits', function() { setLocation({ - pathname: '/foo/bar/merge_requests/1/commits', + pathname: '/foo/bar/-/merge_requests/1/commits', }); - expect(this.subject('show')).toBe('/foo/bar/merge_requests/1'); - expect(this.subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs'); + expect(this.subject('show')).toBe('/foo/bar/-/merge_requests/1'); + expect(this.subject('diffs')).toBe('/foo/bar/-/merge_requests/1/diffs'); }); it('changes from diffs', function() { setLocation({ - pathname: '/foo/bar/merge_requests/1/diffs', + pathname: '/foo/bar/-/merge_requests/1/diffs', }); - expect(this.subject('show')).toBe('/foo/bar/merge_requests/1'); - expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits'); + expect(this.subject('show')).toBe('/foo/bar/-/merge_requests/1'); + expect(this.subject('commits')).toBe('/foo/bar/-/merge_requests/1/commits'); }); it('changes from diffs.html', function() { setLocation({ - pathname: '/foo/bar/merge_requests/1/diffs.html', + pathname: '/foo/bar/-/merge_requests/1/diffs.html', }); - expect(this.subject('show')).toBe('/foo/bar/merge_requests/1'); - expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits'); + expect(this.subject('show')).toBe('/foo/bar/-/merge_requests/1'); + expect(this.subject('commits')).toBe('/foo/bar/-/merge_requests/1/commits'); }); it('changes from notes', function() { setLocation({ - pathname: '/foo/bar/merge_requests/1', + pathname: '/foo/bar/-/merge_requests/1', }); - expect(this.subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs'); - expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits'); + expect(this.subject('diffs')).toBe('/foo/bar/-/merge_requests/1/diffs'); + expect(this.subject('commits')).toBe('/foo/bar/-/merge_requests/1/commits'); }); it('includes search parameters and hash string', function() { setLocation({ - pathname: '/foo/bar/merge_requests/1/diffs', + pathname: '/foo/bar/-/merge_requests/1/diffs', search: '?view=parallel', hash: '#L15-35', }); - expect(this.subject('show')).toBe('/foo/bar/merge_requests/1?view=parallel#L15-35'); + expect(this.subject('show')).toBe('/foo/bar/-/merge_requests/1?view=parallel#L15-35'); }); it('replaces the current history state', function() { setLocation({ - pathname: '/foo/bar/merge_requests/1', + pathname: '/foo/bar/-/merge_requests/1', }); const newState = this.subject('commits'); @@ -208,10 +208,10 @@ describe('MergeRequestTabs', function() { it('treats "show" like "notes"', function() { setLocation({ - pathname: '/foo/bar/merge_requests/1/commits', + pathname: '/foo/bar/-/merge_requests/1/commits', }); - expect(this.subject('show')).toBe('/foo/bar/merge_requests/1'); + expect(this.subject('show')).toBe('/foo/bar/-/merge_requests/1'); }); }); diff --git a/spec/javascripts/notes/components/note_actions_spec.js b/spec/javascripts/notes/components/note_actions_spec.js index 2e0694869ba..a65e2fc31ad 100644 --- a/spec/javascripts/notes/components/note_actions_spec.js +++ b/spec/javascripts/notes/components/note_actions_spec.js @@ -30,7 +30,7 @@ describe('noteActions', () => { canAwardEmoji: true, canReportAsAbuse: true, noteId: '539', - noteUrl: `${TEST_HOST}/group/project/merge_requests/1#note_1`, + noteUrl: `${TEST_HOST}/group/project/-/merge_requests/1#note_1`, reportAbusePath: `${TEST_HOST}/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26`, showReply: false, }; diff --git a/spec/lib/banzai/filter/broadcast_message_sanitization_filter_spec.rb b/spec/lib/banzai/filter/broadcast_message_sanitization_filter_spec.rb new file mode 100644 index 00000000000..317ac7ef854 --- /dev/null +++ b/spec/lib/banzai/filter/broadcast_message_sanitization_filter_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Banzai::Filter::BroadcastMessageSanitizationFilter do + include FilterSpecHelper + + it_behaves_like 'default whitelist' + + describe 'custom whitelist' do + it_behaves_like 'XSS prevention' + it_behaves_like 'sanitize link' + + subject { filter(exp).to_html } + + context 'allows `a` elements' do + let(:exp) { %q{<a href="/">Link</a>} } + + it { is_expected.to eq(exp) } + end + + context 'allows `br` elements' do + let(:exp) { %q{Hello<br>World} } + + it { is_expected.to eq(exp) } + end + + context 'when `a` elements have `style` attribute' do + let(:whitelisted_style) { 'color: red; border: blue; background: green; padding: 10px; margin: 10px; text-decoration: underline;' } + + context 'allows specific properties' do + let(:exp) { %{<a href="#" style="#{whitelisted_style}">Stylish Link</a>} } + + it { is_expected.to eq(exp) } + end + + it 'disallows other properties in `style` attribute on `a` elements' do + style = [whitelisted_style, 'position: fixed'].join(';') + doc = filter(%{<a href="#" style="#{style}">Stylish Link</a>}) + + expect(doc.at_css('a')['style']).to eq(whitelisted_style) + end + end + + context 'allows `class` on `a` elements' do + let(:exp) { %q{<a href="#" class="btn">Button Link</a>} } + + it { is_expected.to eq(exp) } + end + end +end diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb index a17a645d4d0..1efca647b8b 100644 --- a/spec/lib/banzai/filter/relative_link_filter_spec.rb +++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb @@ -157,7 +157,7 @@ describe Banzai::Filter::RelativeLinkFilter do end it 'does not modify relative URLs in system notes' do - path = "#{project_path}/merge_requests/1/diffs" + path = "#{project_path}/-/merge_requests/1/diffs" doc = filter(link(path), system_note: true) expect(doc.at_css('a')['href']).to eq path diff --git a/spec/lib/banzai/filter/sanitization_filter_spec.rb b/spec/lib/banzai/filter/sanitization_filter_spec.rb index 8a4b819e4d6..607dc3fda47 100644 --- a/spec/lib/banzai/filter/sanitization_filter_spec.rb +++ b/spec/lib/banzai/filter/sanitization_filter_spec.rb @@ -5,48 +5,12 @@ require 'spec_helper' describe Banzai::Filter::SanitizationFilter do include FilterSpecHelper - describe 'default whitelist' do - it 'sanitizes tags that are not whitelisted' do - act = %q{<textarea>no inputs</textarea> and <blink>no blinks</blink>} - exp = 'no inputs and no blinks' - expect(filter(act).to_html).to eq exp - end - - it 'sanitizes tag attributes' do - act = %q{<a href="http://example.com/bar.html" onclick="bar">Text</a>} - exp = %q{<a href="http://example.com/bar.html">Text</a>} - expect(filter(act).to_html).to eq exp - end - - it 'sanitizes javascript in attributes' do - act = %q(<a href="javascript:alert('foo')">Text</a>) - exp = '<a>Text</a>' - expect(filter(act).to_html).to eq exp - end - - it 'sanitizes mixed-cased javascript in attributes' do - act = %q(<a href="javaScript:alert('foo')">Text</a>) - exp = '<a>Text</a>' - expect(filter(act).to_html).to eq exp - end - - it 'allows whitelisted HTML tags from the user' do - exp = act = "<dl>\n<dt>Term</dt>\n<dd>Definition</dd>\n</dl>" - expect(filter(act).to_html).to eq exp - end - - it 'sanitizes `class` attribute on any element' do - act = %q{<strong class="foo">Strong</strong>} - expect(filter(act).to_html).to eq %q{<strong>Strong</strong>} - end - - it 'sanitizes `id` attribute on any element' do - act = %q{<em id="foo">Emphasis</em>} - expect(filter(act).to_html).to eq %q{<em>Emphasis</em>} - end - end + it_behaves_like 'default whitelist' describe 'custom whitelist' do + it_behaves_like 'XSS prevention' + it_behaves_like 'sanitize link' + it 'customizes the whitelist only once' do instance = described_class.new('Foo') control_count = instance.whitelist[:transformers].size @@ -167,142 +131,6 @@ describe Banzai::Filter::SanitizationFilter do expect(filter(html).to_html).to eq(output) end - it 'removes `rel` attribute from `a` elements' do - act = %q{<a href="#" rel="nofollow">Link</a>} - exp = %q{<a href="#">Link</a>} - - expect(filter(act).to_html).to eq exp - end - - # Adapted from the Sanitize test suite: http://git.io/vczrM - protocols = { - 'protocol-based JS injection: simple, no spaces' => { - input: '<a href="javascript:alert(\'XSS\');">foo</a>', - output: '<a>foo</a>' - }, - - 'protocol-based JS injection: simple, spaces before' => { - input: '<a href="javascript :alert(\'XSS\');">foo</a>', - output: '<a>foo</a>' - }, - - 'protocol-based JS injection: simple, spaces after' => { - input: '<a href="javascript: alert(\'XSS\');">foo</a>', - output: '<a>foo</a>' - }, - - 'protocol-based JS injection: simple, spaces before and after' => { - input: '<a href="javascript : alert(\'XSS\');">foo</a>', - output: '<a>foo</a>' - }, - - 'protocol-based JS injection: preceding colon' => { - input: '<a href=":javascript:alert(\'XSS\');">foo</a>', - output: '<a>foo</a>' - }, - - 'protocol-based JS injection: UTF-8 encoding' => { - input: '<a href="javascript:">foo</a>', - output: '<a>foo</a>' - }, - - 'protocol-based JS injection: long UTF-8 encoding' => { - input: '<a href="javascript:">foo</a>', - output: '<a>foo</a>' - }, - - 'protocol-based JS injection: long UTF-8 encoding without semicolons' => { - input: '<a href=javascript:alert('XSS')>foo</a>', - output: '<a>foo</a>' - }, - - 'protocol-based JS injection: hex encoding' => { - input: '<a href="javascript:">foo</a>', - output: '<a>foo</a>' - }, - - 'protocol-based JS injection: long hex encoding' => { - input: '<a href="javascript:">foo</a>', - output: '<a>foo</a>' - }, - - 'protocol-based JS injection: hex encoding without semicolons' => { - input: '<a href=javascript:alert('XSS')>foo</a>', - output: '<a>foo</a>' - }, - - 'protocol-based JS injection: null char' => { - input: "<a href=java\0script:alert(\"XSS\")>foo</a>", - output: '<a href="java"></a>' - }, - - 'protocol-based JS injection: invalid URL char' => { - input: '<img src=java\script:alert("XSS")>', - output: '<img>' - }, - - 'protocol-based JS injection: Unicode' => { - input: %Q(<a href="\u0001java\u0003script:alert('XSS')">foo</a>), - output: '<a>foo</a>' - }, - - 'protocol-based JS injection: spaces and entities' => { - input: '<a href="  javascript:alert(\'XSS\');">foo</a>', - output: '<a href="">foo</a>' - }, - - 'protocol whitespace' => { - input: '<a href=" http://example.com/"></a>', - output: '<a href="http://example.com/"></a>' - } - } - - protocols.each do |name, data| - it "disallows #{name}" do - doc = filter(data[:input]) - - expect(doc.to_html).to eq data[:output] - end - end - - it 'disallows data links' do - input = '<a href="data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K">XSS</a>' - output = filter(input) - - expect(output.to_html).to eq '<a>XSS</a>' - end - - it 'disallows vbscript links' do - input = '<a href="vbscript:alert(document.domain)">XSS</a>' - output = filter(input) - - expect(output.to_html).to eq '<a>XSS</a>' - end - - it 'disallows invalid URIs' do - expect(Addressable::URI).to receive(:parse).with('foo://example.com') - .and_raise(Addressable::URI::InvalidURIError) - - input = '<a href="foo://example.com">Foo</a>' - output = filter(input) - - expect(output.to_html).to eq '<a>Foo</a>' - end - - it 'allows non-standard anchor schemes' do - exp = %q{<a href="irc://irc.freenode.net/git">IRC</a>} - act = filter(exp) - - expect(act.to_html).to eq exp - end - - it 'allows relative links' do - exp = %q{<a href="foo/bar.md">foo/bar.md</a>} - act = filter(exp) - - expect(act.to_html).to eq exp - end - it 'allows the `data-sourcepos` attribute globally' do exp = %q{<p data-sourcepos="1:1-1:10">foo/bar.md</p>} act = filter(exp) diff --git a/spec/lib/banzai/pipeline/broadcast_message_pipeline_spec.rb b/spec/lib/banzai/pipeline/broadcast_message_pipeline_spec.rb new file mode 100644 index 00000000000..9832b132b58 --- /dev/null +++ b/spec/lib/banzai/pipeline/broadcast_message_pipeline_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Banzai::Pipeline::BroadcastMessagePipeline do + before do + stub_commonmark_sourcepos_disabled + end + + subject { described_class.to_html(exp, project: spy) } + + context "allows `a` elements" do + let(:exp) { "<a>Link</a>" } + + it { is_expected.to eq("<p>#{exp}</p>") } + end + + context "allows `br` elements" do + let(:exp) { "Hello<br>World" } + + it { is_expected.to eq("<p>#{exp}</p>") } + end +end diff --git a/spec/lib/gitlab/ci/pipeline/seed/build/resource_group_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build/resource_group_spec.rb deleted file mode 100644 index bf6985156d3..00000000000 --- a/spec/lib/gitlab/ci/pipeline/seed/build/resource_group_spec.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe Gitlab::Ci::Pipeline::Seed::Build::ResourceGroup do - let_it_be(:project) { create(:project) } - let(:job) { build(:ci_build, project: project) } - let(:seed) { described_class.new(job, resource_group_key) } - - describe '#to_resource' do - subject { seed.to_resource } - - context 'when resource group key is specified' do - let(:resource_group_key) { 'iOS' } - - it 'returns a resource group object' do - is_expected.to be_a(Ci::ResourceGroup) - expect(subject.key).to eq('iOS') - end - - context 'when environment has an invalid URL' do - let(:resource_group_key) { ':::' } - - it 'returns nothing' do - is_expected.to be_nil - end - end - - context 'when there is a resource group already' do - let!(:resource_group) { create(:ci_resource_group, project: project, key: 'iOS') } - - it 'does not create a new resource group' do - expect { subject }.not_to change { Ci::ResourceGroup.count } - end - end - end - - context 'when resource group key is nil' do - let(:resource_group_key) { nil } - - it 'returns nothing' do - is_expected.to be_nil - end - end - end -end diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb index 5526ec9e16f..2ae513aea1b 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb @@ -231,15 +231,6 @@ describe Gitlab::Ci::Pipeline::Seed::Build do end end end - - context 'when job belongs to a resource group' do - let(:attributes) { { name: 'rspec', ref: 'master', resource_group_key: 'iOS' } } - - it 'returns a job with resource group' do - expect(subject.resource_group).not_to be_nil - expect(subject.resource_group.key).to eq('iOS') - end - end end context 'when job is a bridge' do diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index f61b28b06c8..8f9c5c74260 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -241,21 +241,6 @@ module Gitlab end end end - - describe 'resource group' do - context 'when resource group is defined' do - let(:config) do - YAML.dump(rspec: { - script: 'test', - resource_group: 'iOS' - }) - end - - it 'has the attributes' do - expect(subject[:resource_group_key]).to eq 'iOS' - end - end - end end describe '#stages_attributes' do diff --git a/spec/lib/gitlab/data_builder/pipeline_spec.rb b/spec/lib/gitlab/data_builder/pipeline_spec.rb index 635bf56b72e..86ab7f888ca 100644 --- a/spec/lib/gitlab/data_builder/pipeline_spec.rb +++ b/spec/lib/gitlab/data_builder/pipeline_spec.rb @@ -77,7 +77,7 @@ describe Gitlab::DataBuilder::Pipeline do expect(merge_request_attrs[:target_project_id]).to eq(merge_request.target_project_id) expect(merge_request_attrs[:state]).to eq(merge_request.state) expect(merge_request_attrs[:merge_status]).to eq(merge_request.merge_status) - expect(merge_request_attrs[:url]).to eq("http://localhost/#{merge_request.target_project.full_path}/merge_requests/#{merge_request.iid}") + expect(merge_request_attrs[:url]).to eq("http://localhost/#{merge_request.target_project.full_path}/-/merge_requests/#{merge_request.iid}") end end end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 16fe5f23d14..8d436fb28e0 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -444,7 +444,6 @@ project: - service_desk_setting - import_failures - container_expiration_policy -- resource_groups award_emoji: - awardable - user 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 f549216ccb0..ec1b935ad63 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -240,6 +240,16 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do expect(sentry_issue.sentry_issue_identifier).to eq(1234567891) end + it 'restores container_expiration_policy' do + policy = Project.find_by_path('project').container_expiration_policy + + aggregate_failures do + expect(policy).to be_an_instance_of(ContainerExpirationPolicy) + expect(policy).to be_persisted + expect(policy.cadence).to eq('3month') + end + end + context 'Merge requests' do it 'always has the new project as a target' do expect(MergeRequest.find_by_title('MR1').target_project).to eq(@project) diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb index 0aab02b6c4c..d349c2928b0 100644 --- a/spec/lib/gitlab/url_builder_spec.rb +++ b/spec/lib/gitlab/url_builder_spec.rb @@ -55,7 +55,7 @@ describe Gitlab::UrlBuilder do url = described_class.build(merge_request) - expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.full_path}/merge_requests/#{merge_request.iid}" + expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.full_path}/-/merge_requests/#{merge_request.iid}" end end @@ -118,7 +118,7 @@ describe Gitlab::UrlBuilder do url = described_class.build(note) - expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.full_path}/merge_requests/#{merge_request.iid}#note_#{note.id}" + expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.full_path}/-/merge_requests/#{merge_request.iid}#note_#{note.id}" end end @@ -129,7 +129,7 @@ describe Gitlab::UrlBuilder do url = described_class.build(note) - expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.full_path}/merge_requests/#{merge_request.iid}#note_#{note.id}" + expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.full_path}/-/merge_requests/#{merge_request.iid}#note_#{note.id}" end end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 371982df2bb..8a701a461c0 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1275,68 +1275,6 @@ describe Ci::Build do end end - describe '#requires_resource?' do - subject { build.requires_resource? } - - context 'when build needs a resource from a resource group' do - let(:resource_group) { create(:ci_resource_group, project: project) } - let(:build) { create(:ci_build, resource_group: resource_group, project: project) } - - context 'when build has not retained a resource' do - it { is_expected.to eq(true) } - end - - context 'when build has retained a resource' do - before do - resource_group.retain_resource_for(build) - end - - it { is_expected.to eq(false) } - - context 'when ci_resource_group feature flag is disabled' do - before do - stub_feature_flags(ci_resource_group: false) - end - - it { is_expected.to eq(false) } - end - end - end - - context 'when build does not need a resource from a resource group' do - let(:build) { create(:ci_build, project: project) } - - it { is_expected.to eq(false) } - end - end - - describe '#retains_resource?' do - subject { build.retains_resource? } - - context 'when build needs a resource from a resource group' do - let(:resource_group) { create(:ci_resource_group, project: project) } - let(:build) { create(:ci_build, resource_group: resource_group, project: project) } - - context 'when build has retained a resource' do - before do - resource_group.retain_resource_for(build) - end - - it { is_expected.to eq(true) } - end - - context 'when build has not retained a resource' do - it { is_expected.to eq(false) } - end - end - - context 'when build does not need a resource from a resource group' do - let(:build) { create(:ci_build, project: project) } - - it { is_expected.to eq(false) } - end - end - describe '#stops_environment?' do subject { build.stops_environment? } diff --git a/spec/models/ci/resource_group_spec.rb b/spec/models/ci/resource_group_spec.rb deleted file mode 100644 index 213a57c2d78..00000000000 --- a/spec/models/ci/resource_group_spec.rb +++ /dev/null @@ -1,88 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe Ci::ResourceGroup do - describe 'validation' do - it 'valids when key includes allowed character' do - resource_group = build(:ci_resource_group, key: 'test') - - expect(resource_group).to be_valid - end - - it 'invalids when key includes invalid character' do - resource_group = build(:ci_resource_group, key: ':::') - - expect(resource_group).not_to be_valid - end - end - - describe '#ensure_resource' do - it 'creates one resource when resource group is created' do - resource_group = create(:ci_resource_group) - - expect(resource_group.resources.count).to eq(1) - expect(resource_group.resources.all?(&:persisted?)).to eq(true) - end - end - - describe '#retain_resource_for' do - subject { resource_group.retain_resource_for(build) } - - let(:build) { create(:ci_build) } - let(:resource_group) { create(:ci_resource_group) } - - it 'retains resource for the build' do - expect(resource_group.resources.first.build).to be_nil - - is_expected.to eq(true) - - expect(resource_group.resources.first.build).to eq(build) - end - - context 'when there are no free resources' do - before do - resource_group.retain_resource_for(create(:ci_build)) - end - - it 'fails to retain resource' do - is_expected.to eq(false) - end - end - - context 'when the build has already retained a resource' do - let!(:another_resource) { create(:ci_resource, resource_group: resource_group, build: build) } - - it 'fails to retain resource' do - expect { subject }.to raise_error(ActiveRecord::RecordNotUnique) - end - end - end - - describe '#release_resource_from' do - subject { resource_group.release_resource_from(build) } - - let(:build) { create(:ci_build) } - let(:resource_group) { create(:ci_resource_group) } - - context 'when the build has already retained a resource' do - before do - resource_group.retain_resource_for(build) - end - - it 'releases resource from the build' do - expect(resource_group.resources.first.build).to eq(build) - - is_expected.to eq(true) - - expect(resource_group.resources.first.build).to be_nil - end - end - - context 'when the build has already released a resource' do - it 'fails to release resource' do - is_expected.to eq(false) - end - end - end -end diff --git a/spec/models/ci/resource_spec.rb b/spec/models/ci/resource_spec.rb deleted file mode 100644 index 27e512e2c45..00000000000 --- a/spec/models/ci/resource_spec.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe Ci::Resource do - describe '.free' do - subject { described_class.free } - - let(:resource_group) { create(:ci_resource_group) } - let!(:free_resource) { resource_group.resources.take } - let!(:retained_resource) { create(:ci_resource, :retained, resource_group: resource_group) } - - it 'returns free resources' do - is_expected.to eq([free_resource]) - end - end - - describe '.retained_by' do - subject { described_class.retained_by(build) } - - let(:build) { create(:ci_build) } - let!(:resource) { create(:ci_resource, build: build) } - - it 'returns retained resources' do - is_expected.to eq([resource]) - end - end -end diff --git a/spec/models/project_services/chat_message/merge_message_spec.rb b/spec/models/project_services/chat_message/merge_message_spec.rb index b56eb19dd55..150ee6f7472 100644 --- a/spec/models/project_services/chat_message/merge_message_spec.rb +++ b/spec/models/project_services/chat_message/merge_message_spec.rb @@ -52,7 +52,7 @@ describe ChatMessage::MergeMessage do context 'open' do it 'returns a message regarding opening of merge requests' do expect(subject.pretext).to eq( - 'Test User (test.user) opened <http://somewhere.com/merge_requests/100|!100 *Merge Request title*> in <http://somewhere.com|project_name>') + 'Test User (test.user) opened <http://somewhere.com/-/merge_requests/100|!100 *Merge Request title*> in <http://somewhere.com|project_name>') expect(subject.attachments).to be_empty end end @@ -63,7 +63,7 @@ describe ChatMessage::MergeMessage do end it 'returns a message regarding closing of merge requests' do expect(subject.pretext).to eq( - 'Test User (test.user) closed <http://somewhere.com/merge_requests/100|!100 *Merge Request title*> in <http://somewhere.com|project_name>') + 'Test User (test.user) closed <http://somewhere.com/-/merge_requests/100|!100 *Merge Request title*> in <http://somewhere.com|project_name>') expect(subject.attachments).to be_empty end end @@ -77,12 +77,12 @@ describe ChatMessage::MergeMessage do context 'open' do it 'returns a message regarding opening of merge requests' do expect(subject.pretext).to eq( - 'Test User (test.user) opened [!100 *Merge Request title*](http://somewhere.com/merge_requests/100) in [project_name](http://somewhere.com)') + 'Test User (test.user) opened [!100 *Merge Request title*](http://somewhere.com/-/merge_requests/100) in [project_name](http://somewhere.com)') expect(subject.attachments).to be_empty expect(subject.activity).to eq({ title: 'Merge Request opened by Test User (test.user)', subtitle: 'in [project_name](http://somewhere.com)', - text: '[!100 *Merge Request title*](http://somewhere.com/merge_requests/100)', + text: '[!100 *Merge Request title*](http://somewhere.com/-/merge_requests/100)', image: 'http://someavatar.com' }) end @@ -95,12 +95,12 @@ describe ChatMessage::MergeMessage do it 'returns a message regarding closing of merge requests' do expect(subject.pretext).to eq( - 'Test User (test.user) closed [!100 *Merge Request title*](http://somewhere.com/merge_requests/100) in [project_name](http://somewhere.com)') + 'Test User (test.user) closed [!100 *Merge Request title*](http://somewhere.com/-/merge_requests/100) in [project_name](http://somewhere.com)') expect(subject.attachments).to be_empty expect(subject.activity).to eq({ title: 'Merge Request closed by Test User (test.user)', subtitle: 'in [project_name](http://somewhere.com)', - text: '[!100 *Merge Request title*](http://somewhere.com/merge_requests/100)', + text: '[!100 *Merge Request title*](http://somewhere.com/-/merge_requests/100)', image: 'http://someavatar.com' }) end diff --git a/spec/presenters/merge_request_presenter_spec.rb b/spec/presenters/merge_request_presenter_spec.rb index ce437090d43..6ce6f84cf61 100644 --- a/spec/presenters/merge_request_presenter_spec.rb +++ b/spec/presenters/merge_request_presenter_spec.rb @@ -101,7 +101,7 @@ describe MergeRequestPresenter do allow(presenter).to receive_message_chain(:conflicts, :can_be_resolved_by?).with(user) { true } expect(path) - .to eq("/#{project.full_path}/merge_requests/#{resource.iid}/conflicts") + .to eq("/#{project.full_path}/-/merge_requests/#{resource.iid}/conflicts") end end end @@ -179,7 +179,7 @@ describe MergeRequestPresenter do it 'returns correct link with correct text' do is_expected - .to match("#{project.full_path}/merge_requests/#{resource.iid}/assign_related_issues") + .to match("#{project.full_path}/-/merge_requests/#{resource.iid}/assign_related_issues") is_expected .to match("Assign yourself to this issue") @@ -192,7 +192,7 @@ describe MergeRequestPresenter do it 'returns correct link with correct text' do is_expected - .to match("#{project.full_path}/merge_requests/#{resource.iid}/assign_related_issues") + .to match("#{project.full_path}/-/merge_requests/#{resource.iid}/assign_related_issues") is_expected .to match("Assign yourself to these issues") @@ -221,7 +221,7 @@ describe MergeRequestPresenter do .with(user) .and_return(true) - is_expected.to eq("/#{resource.project.full_path}/merge_requests/#{resource.iid}/cancel_auto_merge") + is_expected.to eq("/#{resource.project.full_path}/-/merge_requests/#{resource.iid}/cancel_auto_merge") end end @@ -248,7 +248,7 @@ describe MergeRequestPresenter do .and_return(true) is_expected - .to eq("/#{resource.project.full_path}/merge_requests/#{resource.iid}/merge") + .to eq("/#{resource.project.full_path}/-/merge_requests/#{resource.iid}/merge") end end @@ -312,7 +312,7 @@ describe MergeRequestPresenter do project.add_maintainer(user) is_expected - .to eq("/#{resource.project.full_path}/merge_requests/#{resource.iid}/remove_wip") + .to eq("/#{resource.project.full_path}/-/merge_requests/#{resource.iid}/remove_wip") end end @@ -535,7 +535,7 @@ describe MergeRequestPresenter do it 'returns path' do is_expected - .to eq("/#{project.full_path}/merge_requests/#{resource.iid}/rebase") + .to eq("/#{project.full_path}/-/merge_requests/#{resource.iid}/rebase") end end diff --git a/spec/presenters/snippet_presenter_spec.rb b/spec/presenters/snippet_presenter_spec.rb index d874dbcc279..87f2220979c 100644 --- a/spec/presenters/snippet_presenter_spec.rb +++ b/spec/presenters/snippet_presenter_spec.rb @@ -127,4 +127,20 @@ describe SnippetPresenter do end end end + + describe '#can_report_as_spam' do + let(:snippet) { personal_snippet } + + subject { presenter.can_report_as_spam? } + + it 'returns false if the user cannot submit the snippet as spam' do + expect(subject).to be_falsey + end + + it 'returns true if the user can submit the snippet as spam' do + allow(snippet).to receive(:submittable_as_spam_by?).and_return(true) + + expect(subject).to be_truthy + end + end end diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb index ecbb81294a0..d38b7eafe97 100644 --- a/spec/requests/api/internal/base_spec.rb +++ b/spec/requests/api/internal/base_spec.rb @@ -852,7 +852,7 @@ describe API::Internal::Base do 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} + 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)) @@ -909,7 +909,7 @@ describe API::Internal::Base do message = <<~MESSAGE.strip View merge request for #{branch_name}: - http://#{Gitlab.config.gitlab.host}/#{project.full_path}/merge_requests/1 + http://#{Gitlab.config.gitlab.host}/#{project.full_path}/-/merge_requests/1 MESSAGE expect(json_response['messages']).to include(build_basic_message(message)) diff --git a/spec/requests/api/releases_spec.rb b/spec/requests/api/releases_spec.rb index da04e852795..534dced16bf 100644 --- a/spec/requests/api/releases_spec.rb +++ b/spec/requests/api/releases_spec.rb @@ -76,7 +76,7 @@ describe API::Releases do mr_uri = URI.parse(links['merge_requests_url']) issue_uri = URI.parse(links['issues_url']) - expect(mr_uri.path).to eq("#{path_base}/merge_requests") + expect(mr_uri.path).to eq("#{path_base}/-/merge_requests") expect(issue_uri.path).to eq("#{path_base}/issues") expect(mr_uri.query).to eq(expected_query) expect(issue_uri.query).to eq(expected_query) diff --git a/spec/requests/projects/merge_requests_discussions_spec.rb b/spec/requests/projects/merge_requests_discussions_spec.rb index 5945561aa7b..ffc98d09e5c 100644 --- a/spec/requests/projects/merge_requests_discussions_spec.rb +++ b/spec/requests/projects/merge_requests_discussions_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' describe 'merge requests discussions' do # Further tests can be found at merge_requests_controller_spec.rb - describe 'GET /:namespace/:project/merge_requests/:iid/discussions' do + describe 'GET /:namespace/:project/-/merge_requests/:iid/discussions' do let(:project) { create(:project, :repository) } let(:user) { project.owner } let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) } diff --git a/spec/requests/user_activity_spec.rb b/spec/requests/user_activity_spec.rb index 15666e00b9f..3cd4911098a 100644 --- a/spec/requests/user_activity_spec.rb +++ b/spec/requests/user_activity_spec.rb @@ -26,8 +26,8 @@ describe 'Update of user activity' do '/dashboard/todos', '/group/project/issues', '/group/project/issues/10', - '/group/project/merge_requests', - '/group/project/merge_requests/15' + '/group/project/-/merge_requests', + '/group/project/-/merge_requests/15' ] context 'without an authenticated user' do diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 287db20448a..96956d85de4 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -292,71 +292,77 @@ describe 'project routing' do describe Projects::MergeRequestsController, 'routing' do it 'to #commits' do - expect(get('/gitlab/gitlabhq/merge_requests/1/commits.json')).to route_to('projects/merge_requests#commits', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', format: 'json') + expect(get('/gitlab/gitlabhq/-/merge_requests/1/commits.json')).to route_to('projects/merge_requests#commits', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', format: 'json') end it 'to #pipelines' do - expect(get('/gitlab/gitlabhq/merge_requests/1/pipelines.json')).to route_to('projects/merge_requests#pipelines', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', format: 'json') + expect(get('/gitlab/gitlabhq/-/merge_requests/1/pipelines.json')).to route_to('projects/merge_requests#pipelines', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', format: 'json') end it 'to #merge' do - expect(post('/gitlab/gitlabhq/merge_requests/1/merge')).to route_to( + expect(post('/gitlab/gitlabhq/-/merge_requests/1/merge')).to route_to( 'projects/merge_requests#merge', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1' ) end it 'to #show' do - expect(get('/gitlab/gitlabhq/merge_requests/1.diff')).to route_to('projects/merge_requests#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', format: 'diff') - expect(get('/gitlab/gitlabhq/merge_requests/1.patch')).to route_to('projects/merge_requests#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', format: 'patch') - expect(get('/gitlab/gitlabhq/merge_requests/1/diffs')).to route_to('projects/merge_requests#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', tab: 'diffs') - expect(get('/gitlab/gitlabhq/merge_requests/1/commits')).to route_to('projects/merge_requests#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', tab: 'commits') - expect(get('/gitlab/gitlabhq/merge_requests/1/pipelines')).to route_to('projects/merge_requests#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', tab: 'pipelines') + expect(get('/gitlab/gitlabhq/-/merge_requests/1.diff')).to route_to('projects/merge_requests#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', format: 'diff') + expect(get('/gitlab/gitlabhq/-/merge_requests/1.patch')).to route_to('projects/merge_requests#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', format: 'patch') + expect(get('/gitlab/gitlabhq/-/merge_requests/1/diffs')).to route_to('projects/merge_requests#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', tab: 'diffs') + expect(get('/gitlab/gitlabhq/-/merge_requests/1/commits')).to route_to('projects/merge_requests#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', tab: 'commits') + expect(get('/gitlab/gitlabhq/-/merge_requests/1/pipelines')).to route_to('projects/merge_requests#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', tab: 'pipelines') end it_behaves_like 'RESTful project resources' do let(:controller) { 'merge_requests' } let(:actions) { [:index, :edit, :show, :update] } + let(:controller_path) { '/-/merge_requests' } end + + it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/merge_requests", "/gitlab/gitlabhq/-/merge_requests" + it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/merge_requests/1/diffs", "/gitlab/gitlabhq/-/merge_requests/1/diffs" end describe Projects::MergeRequests::CreationsController, 'routing' do it 'to #new' do - expect(get('/gitlab/gitlabhq/merge_requests/new')).to route_to('projects/merge_requests/creations#new', namespace_id: 'gitlab', project_id: 'gitlabhq') - expect(get('/gitlab/gitlabhq/merge_requests/new/diffs')).to route_to('projects/merge_requests/creations#new', namespace_id: 'gitlab', project_id: 'gitlabhq', tab: 'diffs') - expect(get('/gitlab/gitlabhq/merge_requests/new/pipelines')).to route_to('projects/merge_requests/creations#new', namespace_id: 'gitlab', project_id: 'gitlabhq', tab: 'pipelines') + expect(get('/gitlab/gitlabhq/-/merge_requests/new')).to route_to('projects/merge_requests/creations#new', namespace_id: 'gitlab', project_id: 'gitlabhq') + expect(get('/gitlab/gitlabhq/-/merge_requests/new/diffs')).to route_to('projects/merge_requests/creations#new', namespace_id: 'gitlab', project_id: 'gitlabhq', tab: 'diffs') + expect(get('/gitlab/gitlabhq/-/merge_requests/new/pipelines')).to route_to('projects/merge_requests/creations#new', namespace_id: 'gitlab', project_id: 'gitlabhq', tab: 'pipelines') end it 'to #create' do - expect(post('/gitlab/gitlabhq/merge_requests')).to route_to('projects/merge_requests/creations#create', namespace_id: 'gitlab', project_id: 'gitlabhq') + expect(post('/gitlab/gitlabhq/-/merge_requests')).to route_to('projects/merge_requests/creations#create', namespace_id: 'gitlab', project_id: 'gitlabhq') end it 'to #branch_from' do - expect(get('/gitlab/gitlabhq/merge_requests/new/branch_from')).to route_to('projects/merge_requests/creations#branch_from', namespace_id: 'gitlab', project_id: 'gitlabhq') + expect(get('/gitlab/gitlabhq/-/merge_requests/new/branch_from')).to route_to('projects/merge_requests/creations#branch_from', namespace_id: 'gitlab', project_id: 'gitlabhq') end it 'to #branch_to' do - expect(get('/gitlab/gitlabhq/merge_requests/new/branch_to')).to route_to('projects/merge_requests/creations#branch_to', namespace_id: 'gitlab', project_id: 'gitlabhq') + expect(get('/gitlab/gitlabhq/-/merge_requests/new/branch_to')).to route_to('projects/merge_requests/creations#branch_to', namespace_id: 'gitlab', project_id: 'gitlabhq') end it 'to #pipelines' do - expect(get('/gitlab/gitlabhq/merge_requests/new/pipelines.json')).to route_to('projects/merge_requests/creations#pipelines', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'json') + expect(get('/gitlab/gitlabhq/-/merge_requests/new/pipelines.json')).to route_to('projects/merge_requests/creations#pipelines', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'json') end it 'to #diffs' do - expect(get('/gitlab/gitlabhq/merge_requests/new/diffs.json')).to route_to('projects/merge_requests/creations#diffs', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'json') + expect(get('/gitlab/gitlabhq/-/merge_requests/new/diffs.json')).to route_to('projects/merge_requests/creations#diffs', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'json') end + + it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/merge_requests/new", "/gitlab/gitlabhq/-/merge_requests/new" end describe Projects::MergeRequests::DiffsController, 'routing' do it 'to #show' do - expect(get('/gitlab/gitlabhq/merge_requests/1/diffs.json')).to route_to('projects/merge_requests/diffs#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', format: 'json') + expect(get('/gitlab/gitlabhq/-/merge_requests/1/diffs.json')).to route_to('projects/merge_requests/diffs#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', format: 'json') end end describe Projects::MergeRequests::ConflictsController, 'routing' do it 'to #show' do - expect(get('/gitlab/gitlabhq/merge_requests/1/conflicts')).to route_to('projects/merge_requests/conflicts#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') + expect(get('/gitlab/gitlabhq/-/merge_requests/1/conflicts')).to route_to('projects/merge_requests/conflicts#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') end end # raw_project_snippet GET /:project_id/snippets/:id/raw(.:format) snippets#raw diff --git a/spec/serializers/merge_request_widget_entity_spec.rb b/spec/serializers/merge_request_widget_entity_spec.rb index 22232682be1..80f59ef90ca 100644 --- a/spec/serializers/merge_request_widget_entity_spec.rb +++ b/spec/serializers/merge_request_widget_entity_spec.rb @@ -45,12 +45,12 @@ describe MergeRequestWidgetEntity do it 'has email_patches_path' do expect(subject[:email_patches_path]) - .to eq("/#{resource.project.full_path}/merge_requests/#{resource.iid}.patch") + .to eq("/#{resource.project.full_path}/-/merge_requests/#{resource.iid}.patch") end it 'has plain_diff_path' do expect(subject[:plain_diff_path]) - .to eq("/#{resource.project.full_path}/merge_requests/#{resource.iid}.diff") + .to eq("/#{resource.project.full_path}/-/merge_requests/#{resource.iid}.diff") end describe 'when source project is deleted' do diff --git a/spec/serializers/paginated_diff_entity_spec.rb b/spec/serializers/paginated_diff_entity_spec.rb index 7432e072318..77569aaa4bc 100644 --- a/spec/serializers/paginated_diff_entity_spec.rb +++ b/spec/serializers/paginated_diff_entity_spec.rb @@ -26,7 +26,7 @@ describe PaginatedDiffEntity do expect(subject[:pagination]).to eq( current_page: 2, next_page: 3, - next_page_href: "/#{merge_request.project.full_path}/merge_requests/#{merge_request.iid}/diffs_batch.json?page=3", + next_page_href: "/#{merge_request.project.full_path}/-/merge_requests/#{merge_request.iid}/diffs_batch.json?page=3", total_pages: 7 ) end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 4f624368215..04e57b1a2d4 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -914,44 +914,6 @@ describe Ci::CreatePipelineService do end end - context 'with resource group' do - context 'when resource group is defined' do - before do - config = YAML.dump( - test: { stage: 'test', script: 'ls', resource_group: resource_group_key } - ) - - stub_ci_pipeline_yaml_file(config) - end - - let(:resource_group_key) { 'iOS' } - - it 'persists the association correctly' do - result = execute_service - deploy_job = result.builds.find_by_name!(:test) - resource_group = project.resource_groups.find_by_key!(resource_group_key) - - expect(result).to be_persisted - expect(deploy_job.resource_group.key).to eq(resource_group_key) - expect(project.resource_groups.count).to eq(1) - expect(resource_group.builds.count).to eq(1) - expect(resource_group.resources.count).to eq(1) - expect(resource_group.resources.first.build).to eq(nil) - end - - context 'when resourc group key includes predefined variables' do - let(:resource_group_key) { '$CI_COMMIT_REF_NAME-$CI_JOB_NAME' } - - it 'interpolates the variables into the key correctly' do - result = execute_service - - expect(result).to be_persisted - expect(project.resource_groups.exists?(key: 'master-test')).to eq(true) - end - end - end - end - context 'with timeout' do context 'when builds with custom timeouts are configured' do before do diff --git a/spec/services/ci/expire_pipeline_cache_service_spec.rb b/spec/services/ci/expire_pipeline_cache_service_spec.rb index ff2d286465a..f7fc73d9f9c 100644 --- a/spec/services/ci/expire_pipeline_cache_service_spec.rb +++ b/spec/services/ci/expire_pipeline_cache_service_spec.rb @@ -11,7 +11,7 @@ describe Ci::ExpirePipelineCacheService do describe '#execute' do it 'invalidates Etag caching for project pipelines path' do pipelines_path = "/#{project.full_path}/pipelines.json" - new_mr_pipelines_path = "/#{project.full_path}/merge_requests/new.json" + new_mr_pipelines_path = "/#{project.full_path}/-/merge_requests/new.json" pipeline_path = "/#{project.full_path}/pipelines/#{pipeline.id}.json" expect_any_instance_of(Gitlab::EtagCaching::Store).to receive(:touch).with(pipelines_path) @@ -24,7 +24,7 @@ describe Ci::ExpirePipelineCacheService do it 'invalidates Etag caching for merge request pipelines if pipeline runs on any commit of that source branch' do pipeline = create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master') merge_request = create(:merge_request, source_project: project, source_branch: pipeline.ref) - merge_request_pipelines_path = "/#{project.full_path}/merge_requests/#{merge_request.iid}/pipelines.json" + merge_request_pipelines_path = "/#{project.full_path}/-/merge_requests/#{merge_request.iid}/pipelines.json" allow_any_instance_of(Gitlab::EtagCaching::Store).to receive(:touch) expect_any_instance_of(Gitlab::EtagCaching::Store).to receive(:touch).with(merge_request_pipelines_path) diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb index 76fe6f53a11..b1368f7776b 100644 --- a/spec/services/ci/retry_build_service_spec.rb +++ b/spec/services/ci/retry_build_service_spec.rb @@ -31,7 +31,7 @@ describe Ci::RetryBuildService do job_artifacts_container_scanning job_artifacts_dast job_artifacts_license_management job_artifacts_performance job_artifacts_codequality job_artifacts_metrics scheduled_at - job_variables waiting_for_resource_at].freeze + job_variables].freeze IGNORE_ACCESSORS = %i[type lock_version target_url base_tags trace_sections @@ -40,14 +40,14 @@ describe Ci::RetryBuildService do user_id auto_canceled_by_id retried failure_reason sourced_pipelines artifacts_file_store artifacts_metadata_store metadata runner_session trace_chunks upstream_pipeline_id - artifacts_file artifacts_metadata artifacts_size commands resource resource_group_id].freeze + artifacts_file artifacts_metadata artifacts_size commands].freeze shared_examples 'build duplication' do let(:another_pipeline) { create(:ci_empty_pipeline, project: project) } let(:build) do create(:ci_build, :failed, :expired, :erased, :queued, :coverage, :tags, - :allowed_to_fail, :on_tag, :triggered, :teardown_environment, :resource_group, + :allowed_to_fail, :on_tag, :triggered, :teardown_environment, description: 'my-job', stage: 'test', stage_id: stage.id, pipeline: pipeline, auto_canceled_by: another_pipeline, scheduled_at: 10.seconds.since) diff --git a/spec/services/merge_requests/get_urls_service_spec.rb b/spec/services/merge_requests/get_urls_service_spec.rb index dcb8c8080a1..8500aa2b852 100644 --- a/spec/services/merge_requests/get_urls_service_spec.rb +++ b/spec/services/merge_requests/get_urls_service_spec.rb @@ -8,8 +8,8 @@ describe MergeRequests::GetUrlsService do let(:project) { create(:project, :public, :repository) } let(:service) { described_class.new(project) } let(:source_branch) { "merge-test" } - let(:new_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.full_path}/merge_requests/new?merge_request%5Bsource_branch%5D=#{source_branch}" } - let(:show_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.full_path}/merge_requests/#{merge_request.iid}" } + let(:new_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.full_path}/-/merge_requests/new?merge_request%5Bsource_branch%5D=#{source_branch}" } + let(:show_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.full_path}/-/merge_requests/#{merge_request.iid}" } let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" } let(:deleted_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 #{Gitlab::Git::BLANK_SHA} refs/heads/#{source_branch}" } let(:existing_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" } @@ -127,7 +127,7 @@ describe MergeRequests::GetUrlsService do let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/new_branch" } let(:existing_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/markdown" } let(:changes) { "#{new_branch_changes}\n#{existing_branch_changes}" } - let(:new_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.full_path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch" } + let(:new_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.full_path}/-/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch" } it 'returns 2 urls for both creating new and showing merge request' do result = service.execute(changes) diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index a952e26e338..21bf4545f34 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -328,7 +328,7 @@ describe SystemNoteService do url = if type == 'commit' "#{Settings.gitlab.base_url}/#{project.namespace.path}/#{project.path}/commit/#{commit.id}" else - "#{Settings.gitlab.base_url}/#{project.namespace.path}/#{project.path}/merge_requests/#{merge_request.iid}" + "#{Settings.gitlab.base_url}/#{project.namespace.path}/#{project.path}/-/merge_requests/#{merge_request.iid}" end link = double(object: { 'url' => url }) diff --git a/spec/support/shared_examples/lib/banzai/filters/sanitization_filter_shared_examples.rb b/spec/support/shared_examples/lib/banzai/filters/sanitization_filter_shared_examples.rb new file mode 100644 index 00000000000..134e38833cf --- /dev/null +++ b/spec/support/shared_examples/lib/banzai/filters/sanitization_filter_shared_examples.rb @@ -0,0 +1,182 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'default whitelist' do + it 'sanitizes tags that are not whitelisted' do + act = %q{<textarea>no inputs</textarea> and <blink>no blinks</blink>} + exp = 'no inputs and no blinks' + expect(filter(act).to_html).to eq exp + end + + it 'sanitizes tag attributes' do + act = %q{<a href="http://example.com/bar.html" onclick="bar">Text</a>} + exp = %q{<a href="http://example.com/bar.html">Text</a>} + expect(filter(act).to_html).to eq exp + end + + it 'sanitizes javascript in attributes' do + act = %q(<a href="javascript:alert('foo')">Text</a>) + exp = '<a>Text</a>' + expect(filter(act).to_html).to eq exp + end + + it 'sanitizes mixed-cased javascript in attributes' do + act = %q(<a href="javaScript:alert('foo')">Text</a>) + exp = '<a>Text</a>' + expect(filter(act).to_html).to eq exp + end + + it 'allows whitelisted HTML tags from the user' do + exp = act = "<dl>\n<dt>Term</dt>\n<dd>Definition</dd>\n</dl>" + expect(filter(act).to_html).to eq exp + end + + it 'sanitizes `class` attribute on any element' do + act = %q{<strong class="foo">Strong</strong>} + expect(filter(act).to_html).to eq %q{<strong>Strong</strong>} + end + + it 'sanitizes `id` attribute on any element' do + act = %q{<em id="foo">Emphasis</em>} + expect(filter(act).to_html).to eq %q{<em>Emphasis</em>} + end +end + +RSpec.shared_examples 'XSS prevention' do + # Adapted from the Sanitize test suite: http://git.io/vczrM + protocols = { + 'protocol-based JS injection: simple, no spaces' => { + input: '<a href="javascript:alert(\'XSS\');">foo</a>', + output: '<a>foo</a>' + }, + + 'protocol-based JS injection: simple, spaces before' => { + input: '<a href="javascript :alert(\'XSS\');">foo</a>', + output: '<a>foo</a>' + }, + + 'protocol-based JS injection: simple, spaces after' => { + input: '<a href="javascript: alert(\'XSS\');">foo</a>', + output: '<a>foo</a>' + }, + + 'protocol-based JS injection: simple, spaces before and after' => { + input: '<a href="javascript : alert(\'XSS\');">foo</a>', + output: '<a>foo</a>' + }, + + 'protocol-based JS injection: preceding colon' => { + input: '<a href=":javascript:alert(\'XSS\');">foo</a>', + output: '<a>foo</a>' + }, + + 'protocol-based JS injection: UTF-8 encoding' => { + input: '<a href="javascript:">foo</a>', + output: '<a>foo</a>' + }, + + 'protocol-based JS injection: long UTF-8 encoding' => { + input: '<a href="javascript:">foo</a>', + output: '<a>foo</a>' + }, + + 'protocol-based JS injection: long UTF-8 encoding without semicolons' => { + input: '<a href=javascript:alert('XSS')>foo</a>', + output: '<a>foo</a>' + }, + + 'protocol-based JS injection: hex encoding' => { + input: '<a href="javascript:">foo</a>', + output: '<a>foo</a>' + }, + + 'protocol-based JS injection: long hex encoding' => { + input: '<a href="javascript:">foo</a>', + output: '<a>foo</a>' + }, + + 'protocol-based JS injection: hex encoding without semicolons' => { + input: '<a href=javascript:alert('XSS')>foo</a>', + output: '<a>foo</a>' + }, + + 'protocol-based JS injection: null char' => { + input: "<a href=java\0script:alert(\"XSS\")>foo</a>", + output: '<a href="java"></a>' + }, + + 'protocol-based JS injection: invalid URL char' => { + input: '<img src=java\script:alert("XSS")>', + output: '<img>' + }, + + 'protocol-based JS injection: Unicode' => { + input: %Q(<a href="\u0001java\u0003script:alert('XSS')">foo</a>), + output: '<a>foo</a>' + }, + + 'protocol-based JS injection: spaces and entities' => { + input: '<a href="  javascript:alert(\'XSS\');">foo</a>', + output: '<a href="">foo</a>' + }, + + 'protocol whitespace' => { + input: '<a href=" http://example.com/"></a>', + output: '<a href="http://example.com/"></a>' + } + } + + protocols.each do |name, data| + it "disallows #{name}" do + doc = filter(data[:input]) + + expect(doc.to_html).to eq data[:output] + end + end + + it 'disallows data links' do + input = '<a href="data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K">XSS</a>' + output = filter(input) + + expect(output.to_html).to eq '<a>XSS</a>' + end + + it 'disallows vbscript links' do + input = '<a href="vbscript:alert(document.domain)">XSS</a>' + output = filter(input) + + expect(output.to_html).to eq '<a>XSS</a>' + end +end + +RSpec.shared_examples 'sanitize link' do + it 'removes `rel` attribute from `a` elements' do + act = %q{<a href="#" rel="nofollow">Link</a>} + exp = %q{<a href="#">Link</a>} + + expect(filter(act).to_html).to eq exp + end + + it 'disallows invalid URIs' do + expect(Addressable::URI).to receive(:parse).with('foo://example.com') + .and_raise(Addressable::URI::InvalidURIError) + + input = '<a href="foo://example.com">Foo</a>' + output = filter(input) + + expect(output.to_html).to eq '<a>Foo</a>' + end + + it 'allows non-standard anchor schemes' do + exp = %q{<a href="irc://irc.freenode.net/git">IRC</a>} + act = filter(exp) + + expect(act.to_html).to eq exp + end + + it 'allows relative links' do + exp = %q{<a href="foo/bar.md">foo/bar.md</a>} + act = filter(exp) + + expect(act.to_html).to eq exp + end +end |