diff options
56 files changed, 1139 insertions, 293 deletions
diff --git a/.gitlab/merge_request_templates/Documentation.md b/.gitlab/merge_request_templates/Documentation.md index 303d3793ad8..66c1eff412b 100644 --- a/.gitlab/merge_request_templates/Documentation.md +++ b/.gitlab/merge_request_templates/Documentation.md @@ -8,13 +8,13 @@ ## Author's checklist -- Consider taking [the GitLab Technical Writing Fundamentals course](https://gitlab.edcast.com/pathways/ECL-02528ee2-c334-4e16-abf3-e9d8b8260de4) +- [ ] Consider taking [the GitLab Technical Writing Fundamentals course](https://gitlab.edcast.com/pathways/ECL-02528ee2-c334-4e16-abf3-e9d8b8260de4) - [ ] Follow the: - [Documentation process](https://docs.gitlab.com/ee/development/documentation/workflow.html). - [Documentation guidelines](https://docs.gitlab.com/ee/development/documentation/). - [Style Guide](https://docs.gitlab.com/ee/development/documentation/styleguide/). - [ ] Ensure that the [product tier badge](https://docs.gitlab.com/ee/development/documentation/styleguide/index.html#product-tier-badges) is added to topic's `h1`. -- [ ] [Request a review](https://docs.gitlab.com/ee/development/code_review.html#dogfooding-the-reviewers-feature) based on the: +- [ ] [Request a review](https://docs.gitlab.com/ee/development/code_review.html#dogfooding-the-reviewers-feature) based on: - The documentation page's [metadata](https://docs.gitlab.com/ee/development/documentation/#metadata). - The [associated Technical Writer](https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments). diff --git a/.rubocop.yml b/.rubocop.yml index a82910d89fb..6a460e30a95 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -712,3 +712,8 @@ QA/SelectorUsage: - 'ee/spec/**/*.rb' Exclude: - 'spec/rubocop/**/*_spec.rb' + +Performance/ActiveRecordSubtransactions: + Exclude: + - 'spec/**/*.rb' + - 'ee/spec/**/*.rb' diff --git a/CHANGELOG.md b/CHANGELOG.md index 98cecdc0558..07b76d3183f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 14.2.1 (2021-08-23) + +### Fixed (1 change) + +- [Drop un-used db/ci_migrate symlink](gitlab-org/gitlab@1154311625345e120407c0c397c7d4a27848a739) ([merge request](gitlab-org/gitlab!68723)) + +### Changed (2 changes) + +- [Reorder vuln check criteria](gitlab-org/gitlab@9bbb20db46362a859632e7bb88deba985318ca2c) ([merge request](gitlab-org/gitlab!68723)) **GitLab Enterprise Edition** +- [Don't override vulnerability feedback UUID anymore](gitlab-org/gitlab@5f8372fb782c9416ae5ab582009a4399cb7d3750) ([merge request](gitlab-org/gitlab!68723)) **GitLab Enterprise Edition** + ## 14.2.0 (2021-08-20) ### Added (128 changes) diff --git a/app/assets/javascripts/analytics/usage_trends/index.js b/app/assets/javascripts/analytics/usage_trends/index.js index d1880b09f15..3e85832edcf 100644 --- a/app/assets/javascripts/analytics/usage_trends/index.js +++ b/app/assets/javascripts/analytics/usage_trends/index.js @@ -6,7 +6,7 @@ import UsageTrendsApp from './components/app.vue'; Vue.use(VueApollo); const apolloProvider = new VueApollo({ - defaultClient: createDefaultClient(), + defaultClient: createDefaultClient({}, { assumeImmutableResults: true }), }); export default () => { diff --git a/app/assets/javascripts/content_editor/components/wrappers/image.vue b/app/assets/javascripts/content_editor/components/wrappers/image.vue index 3762324a431..5b81e5fddcc 100644 --- a/app/assets/javascripts/content_editor/components/wrappers/image.vue +++ b/app/assets/javascripts/content_editor/components/wrappers/image.vue @@ -22,6 +22,7 @@ export default { <img data-testid="image" class="gl-max-w-full gl-h-auto" + :title="node.attrs.title" :class="{ 'gl-opacity-5': node.attrs.uploading }" :src="node.attrs.src" /> diff --git a/app/assets/javascripts/content_editor/extensions/image.js b/app/assets/javascripts/content_editor/extensions/image.js index c9e8dfa4ad9..81b974af914 100644 --- a/app/assets/javascripts/content_editor/extensions/image.js +++ b/app/assets/javascripts/content_editor/extensions/image.js @@ -50,6 +50,16 @@ export default Image.extend({ }; }, }, + title: { + default: null, + parseHTML: (element) => { + const img = resolveImageEl(element); + + return { + title: img.getAttribute('title'), + }; + }, + }, }; }, parseHTML() { diff --git a/app/assets/javascripts/content_editor/extensions/link.js b/app/assets/javascripts/content_editor/extensions/link.js index 53104fe07a3..e51e6832972 100644 --- a/app/assets/javascripts/content_editor/extensions/link.js +++ b/app/assets/javascripts/content_editor/extensions/link.js @@ -42,6 +42,14 @@ export default Link.extend({ }; }, }, + title: { + title: null, + parseHTML: (element) => { + return { + title: element.getAttribute('title'), + }; + }, + }, canonicalSrc: { default: null, parseHTML: (element) => { diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index e9772232eaf..bca0e45d98d 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -418,43 +418,6 @@ export const urlParamsToArray = (path = '') => export const getUrlParamsArray = () => urlParamsToArray(window.location.search); /** - * Accepts encoding string which includes query params being - * sent to URL. - * - * @param {string} path Query param string - * - * @returns {object} Query params object containing key-value pairs - * with both key and values decoded into plain string. - * - * @deprecated Please use `queryToObject(query, { gatherArrays: true });` instead. See https://gitlab.com/gitlab-org/gitlab/-/issues/328845 - */ -export const urlParamsToObject = (path = '') => - splitPath(path).reduce((dataParam, filterParam) => { - if (filterParam === '') { - return dataParam; - } - - const data = dataParam; - let [key, value] = filterParam.split('='); - key = /%\w+/g.test(key) ? decodeURIComponent(key) : key; - const isArray = key.includes('[]'); - key = key.replace('[]', ''); - value = decodeURIComponent(value.replace(/\+/g, ' ')); - - if (isArray) { - if (!data[key]) { - data[key] = []; - } - - data[key].push(value); - } else { - data[key] = value; - } - - return data; - }, {}); - -/** * Convert search query into an object * * @param {String} query from "document.location.search" diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js index c6e767d5424..ee9560e36c4 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js +++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js @@ -1,16 +1,10 @@ -import Vue from 'vue'; import createFlash from '~/flash'; -import { parseBoolean } from '~/lib/utils/common_utils'; import { __ } from '~/locale'; -import Translate from '~/vue_shared/translate'; -import TestReports from './components/test_reports/test_reports.vue'; import createDagApp from './pipeline_details_dag'; import { createPipelinesDetailApp } from './pipeline_details_graph'; import { createPipelineHeaderApp } from './pipeline_details_header'; import { apolloProvider } from './pipeline_shared_client'; -import createTestReportsStore from './stores/test_reports'; - -Vue.use(Translate); +import { createTestDetails } from './pipeline_test_details'; const SELECTORS = { PIPELINE_DETAILS: '.js-pipeline-details-vue', @@ -19,33 +13,6 @@ const SELECTORS = { PIPELINE_TESTS: '#js-pipeline-tests-detail', }; -const createTestDetails = () => { - const el = document.querySelector(SELECTORS.PIPELINE_TESTS); - const { blobPath, emptyStateImagePath, hasTestReport, summaryEndpoint, suiteEndpoint } = - el?.dataset || {}; - const testReportsStore = createTestReportsStore({ - blobPath, - summaryEndpoint, - suiteEndpoint, - }); - - // eslint-disable-next-line no-new - new Vue({ - el, - components: { - TestReports, - }, - provide: { - emptyStateImagePath, - hasTestReport: parseBoolean(hasTestReport), - }, - store: testReportsStore, - render(createElement) { - return createElement('test-reports'); - }, - }); -}; - export default async function initPipelineDetailsBundle() { const { dataset } = document.querySelector(SELECTORS.PIPELINE_DETAILS); @@ -65,6 +32,27 @@ export default async function initPipelineDetailsBundle() { }); } - createDagApp(apolloProvider); - createTestDetails(); + try { + createPipelineHeaderApp(SELECTORS.PIPELINE_HEADER, apolloProvider, dataset.graphqlResourceEtag); + } catch { + createFlash({ + message: __('An error occurred while loading a section of this page.'), + }); + } + + try { + createDagApp(apolloProvider); + } catch { + createFlash({ + message: __('An error occurred while loading the Needs tab.'), + }); + } + + try { + createTestDetails(SELECTORS.PIPELINE_TESTS); + } catch { + createFlash({ + message: __('An error occurred while loading the Test Reports tab.'), + }); + } } diff --git a/app/assets/javascripts/pipelines/pipeline_test_details.js b/app/assets/javascripts/pipelines/pipeline_test_details.js new file mode 100644 index 00000000000..46c7ec07d03 --- /dev/null +++ b/app/assets/javascripts/pipelines/pipeline_test_details.js @@ -0,0 +1,34 @@ +import Vue from 'vue'; +import { parseBoolean } from '~/lib/utils/common_utils'; +import Translate from '~/vue_shared/translate'; +import TestReports from './components/test_reports/test_reports.vue'; +import createTestReportsStore from './stores/test_reports'; + +Vue.use(Translate); + +export const createTestDetails = (selector) => { + const el = document.querySelector(selector); + const { blobPath, emptyStateImagePath, hasTestReport, summaryEndpoint, suiteEndpoint } = + el?.dataset || {}; + const testReportsStore = createTestReportsStore({ + blobPath, + summaryEndpoint, + suiteEndpoint, + }); + + // eslint-disable-next-line no-new + new Vue({ + el, + components: { + TestReports, + }, + provide: { + emptyStateImagePath, + hasTestReport: parseBoolean(hasTestReport), + }, + store: testReportsStore, + render(createElement) { + return createElement('test-reports'); + }, + }); +}; diff --git a/app/finders/packages/npm/package_finder.rb b/app/finders/packages/npm/package_finder.rb index 92ceac297ee..1ec4fba8f29 100644 --- a/app/finders/packages/npm/package_finder.rb +++ b/app/finders/packages/npm/package_finder.rb @@ -12,11 +12,16 @@ module Packages end def execute - base.npm - .with_name(@package_name) - .installable - .last_of_each_version - .preload_files + results = base.npm + .with_name(@package_name) + .installable + .last_of_each_version + + unless Feature.enabled?(:npm_presenter_queries_tuning) + results = results.preload_files + end + + results end private diff --git a/app/models/application_record.rb b/app/models/application_record.rb index 29ef76547e0..2353a514097 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -31,7 +31,7 @@ class ApplicationRecord < ActiveRecord::Base end def self.safe_ensure_unique(retries: 0) - transaction(requires_new: true) do + transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions yield end rescue ActiveRecord::RecordNotUnique @@ -55,7 +55,7 @@ class ApplicationRecord < ActiveRecord::Base # currently one third of the default 15-second timeout def self.with_fast_read_statement_timeout(timeout_ms = 5000) ::Gitlab::Database::LoadBalancing::Session.current.fallback_to_replicas_for_ambiguous_queries do - transaction(requires_new: true) do + transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions connection.exec_query("SET LOCAL statement_timeout = #{timeout_ms}") yield @@ -80,7 +80,7 @@ class ApplicationRecord < ActiveRecord::Base # # When calling this method on an association, just calling `self.create` would call `ActiveRecord::Persistence.create` # and that skips some code that adds the newly created record to the association. - transaction(requires_new: true) { all.create(*args, &block) } + transaction(requires_new: true) { all.create(*args, &block) } # rubocop:disable Performance/ActiveRecordSubtransactions rescue ActiveRecord::RecordNotUnique find_by(*args) end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index c4b6bcb9395..8ed408d2c23 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -622,7 +622,7 @@ class ApplicationSetting < ApplicationRecord def self.create_from_defaults check_schema! - transaction(requires_new: true) do + transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions super end rescue ActiveRecord::RecordNotUnique diff --git a/app/models/internal_id.rb b/app/models/internal_id.rb index 107b1914af4..9cbb5f92bbd 100644 --- a/app/models/internal_id.rb +++ b/app/models/internal_id.rb @@ -239,7 +239,7 @@ class InternalId < ApplicationRecord lookup else begin - subject.transaction(requires_new: true) do + subject.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions InternalId.create!( **scope, usage: usage_value, @@ -362,7 +362,7 @@ class InternalId < ApplicationRecord value else begin - subject.transaction(requires_new: true) do + subject.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions internal_id = InternalId.create!(**scope, usage: usage, last_value: value) internal_id.last_value end diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb index 4ea127fc222..dcc3970c49f 100644 --- a/app/models/packages/package.rb +++ b/app/models/packages/package.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true class Packages::Package < ApplicationRecord + include EachBatch include Sortable include Gitlab::SQL::Pattern include UsageStatistics @@ -104,6 +105,7 @@ class Packages::Package < ApplicationRecord scope :including_build_info, -> { includes(pipelines: :user) } scope :including_project_route, -> { includes(project: { namespace: :route }) } scope :including_tags, -> { includes(:tags) } + scope :including_dependency_links, -> { includes(dependency_links: :dependency) } scope :with_conan_channel, ->(package_channel) do joins(:conan_metadatum).where(packages_conan_metadata: { package_channel: package_channel }) diff --git a/app/presenters/packages/npm/package_presenter.rb b/app/presenters/packages/npm/package_presenter.rb index 4e147b4739e..a99c93cb227 100644 --- a/app/presenters/packages/npm/package_presenter.rb +++ b/app/presenters/packages/npm/package_presenter.rb @@ -7,14 +7,26 @@ module Packages attr_reader :name, :packages - NPM_VALID_DEPENDENCY_TYPES = %i[dependencies devDependencies bundleDependencies peerDependencies].freeze - def initialize(name, packages) @name = name @packages = packages end def versions + if queries_tuning? + new_versions + else + legacy_versions + end + end + + def dist_tags + build_package_tags.tap { |t| t["latest"] ||= sorted_versions.last } + end + + private + + def legacy_versions package_versions = {} packages.each do |package| @@ -28,11 +40,23 @@ module Packages package_versions end - def dist_tags - build_package_tags.tap { |t| t["latest"] ||= sorted_versions.last } - end + def new_versions + package_versions = {} - private + packages.each_batch do |relation| + relation.including_dependency_links + .preload_files + .each do |package| + package_file = package.package_files.last + + next unless package_file + + package_versions[package.version] = build_package_version(package, package_file) + end + end + + package_versions + end def build_package_tags package_tags.to_h { |tag| [tag.name, tag.package.version] } @@ -59,26 +83,38 @@ module Packages def build_package_dependencies(package) dependencies = Hash.new { |h, key| h[key] = {} } - dependency_links = package.dependency_links - .with_dependency_type(NPM_VALID_DEPENDENCY_TYPES) - .includes_dependency - dependency_links.find_each do |dependency_link| - dependency = dependency_link.dependency - dependencies[dependency_link.dependency_type][dependency.name] = dependency.version_pattern + if queries_tuning? + package.dependency_links.each do |dependency_link| + dependency = dependency_link.dependency + dependencies[dependency_link.dependency_type][dependency.name] = dependency.version_pattern + end + else + dependency_links = package.dependency_links + .with_dependency_type(%i[dependencies devDependencies bundleDependencies peerDependencies]) + .includes_dependency + + dependency_links.find_each do |dependency_link| + dependency = dependency_link.dependency + dependencies[dependency_link.dependency_type][dependency.name] = dependency.version_pattern + end end dependencies end def sorted_versions - versions = packages.map(&:version).compact + versions = packages.pluck_versions.compact VersionSorter.sort(versions) end def package_tags Packages::Tag.for_packages(packages) - .preload_package + .preload_package + end + + def queries_tuning? + Feature.enabled?(:npm_presenter_queries_tuning) end end end diff --git a/app/services/projects/move_deploy_keys_projects_service.rb b/app/services/projects/move_deploy_keys_projects_service.rb index 51d84af249e..17513f0ba11 100644 --- a/app/services/projects/move_deploy_keys_projects_service.rb +++ b/app/services/projects/move_deploy_keys_projects_service.rb @@ -5,7 +5,7 @@ module Projects def execute(source_project, remove_remaining_elements: true) return unless super - Project.transaction(requires_new: true) do + Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions move_deploy_keys_projects remove_remaining_deploy_keys_projects if remove_remaining_elements diff --git a/app/services/projects/move_forks_service.rb b/app/services/projects/move_forks_service.rb index 33f0bab12c9..f61552f661f 100644 --- a/app/services/projects/move_forks_service.rb +++ b/app/services/projects/move_forks_service.rb @@ -5,7 +5,7 @@ module Projects def execute(source_project, remove_remaining_elements: true) return unless super && source_project.fork_network - Project.transaction(requires_new: true) do + Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions move_fork_network_members update_root_project refresh_forks_count diff --git a/app/services/projects/move_lfs_objects_projects_service.rb b/app/services/projects/move_lfs_objects_projects_service.rb index 57a8d3d69c6..ca8b4bfde68 100644 --- a/app/services/projects/move_lfs_objects_projects_service.rb +++ b/app/services/projects/move_lfs_objects_projects_service.rb @@ -5,7 +5,7 @@ module Projects def execute(source_project, remove_remaining_elements: true) return unless super - Project.transaction(requires_new: true) do + Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions move_lfs_objects_projects remove_remaining_lfs_objects_project if remove_remaining_elements diff --git a/app/services/projects/move_notification_settings_service.rb b/app/services/projects/move_notification_settings_service.rb index efe06f158cc..fc268f5762b 100644 --- a/app/services/projects/move_notification_settings_service.rb +++ b/app/services/projects/move_notification_settings_service.rb @@ -5,7 +5,7 @@ module Projects def execute(source_project, remove_remaining_elements: true) return unless super - Project.transaction(requires_new: true) do + Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions move_notification_settings remove_remaining_notification_settings if remove_remaining_elements diff --git a/app/services/projects/move_project_authorizations_service.rb b/app/services/projects/move_project_authorizations_service.rb index c95ad60ab5e..30f3b892131 100644 --- a/app/services/projects/move_project_authorizations_service.rb +++ b/app/services/projects/move_project_authorizations_service.rb @@ -9,7 +9,7 @@ module Projects def execute(source_project, remove_remaining_elements: true) return unless super - Project.transaction(requires_new: true) do + Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions move_project_authorizations remove_remaining_authorizations if remove_remaining_elements diff --git a/app/services/projects/move_project_group_links_service.rb b/app/services/projects/move_project_group_links_service.rb index 349953ff973..8344b0b404b 100644 --- a/app/services/projects/move_project_group_links_service.rb +++ b/app/services/projects/move_project_group_links_service.rb @@ -9,7 +9,7 @@ module Projects def execute(source_project, remove_remaining_elements: true) return unless super - Project.transaction(requires_new: true) do + Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions move_group_links remove_remaining_project_group_links if remove_remaining_elements diff --git a/app/services/projects/move_project_members_service.rb b/app/services/projects/move_project_members_service.rb index 9a1b7c6d1b6..11629c3fd7e 100644 --- a/app/services/projects/move_project_members_service.rb +++ b/app/services/projects/move_project_members_service.rb @@ -9,7 +9,7 @@ module Projects def execute(source_project, remove_remaining_elements: true) return unless super - Project.transaction(requires_new: true) do + Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions move_project_members remove_remaining_members if remove_remaining_elements diff --git a/app/services/projects/move_users_star_projects_service.rb b/app/services/projects/move_users_star_projects_service.rb index 20121d429e2..b8564a02301 100644 --- a/app/services/projects/move_users_star_projects_service.rb +++ b/app/services/projects/move_users_star_projects_service.rb @@ -9,7 +9,7 @@ module Projects return unless user_stars.any? - Project.transaction(requires_new: true) do + Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions user_stars.update_all(project_id: @project.id) Project.reset_counters @project.id, :users_star_projects diff --git a/app/services/users/migrate_to_ghost_user_service.rb b/app/services/users/migrate_to_ghost_user_service.rb index a471f55e644..09fd74e9818 100644 --- a/app/services/users/migrate_to_ghost_user_service.rb +++ b/app/services/users/migrate_to_ghost_user_service.rb @@ -39,7 +39,7 @@ module Users private def migrate_records_in_transaction - user.transaction(requires_new: true) do + user.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions @ghost_user = User.ghost migrate_records diff --git a/config/feature_flags/development/npm_presenter_queries_tuning.yml b/config/feature_flags/development/npm_presenter_queries_tuning.yml new file mode 100644 index 00000000000..d600385332c --- /dev/null +++ b/config/feature_flags/development/npm_presenter_queries_tuning.yml @@ -0,0 +1,8 @@ +--- +name: npm_presenter_queries_tuning +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68275 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338603 +milestone: '14.2' +type: development +group: group::package +default_enabled: false diff --git a/data/whats_new/202108190001_14_02.yml b/data/whats_new/202108190001_14_02.yml new file mode 100644 index 00000000000..99ba097b60c --- /dev/null +++ b/data/whats_new/202108190001_14_02.yml @@ -0,0 +1,85 @@ +- title: Add pronunciation to GitLab profile page + body: | + You can now add pronunciation to your user profile. In distributed teams where team members are from different countries, it can be difficult to determine how to say someone's name correctly. This will help others know how to pronounce your name. + stage: Manage + self-managed: true + gitlab-com: true + packages: [Free, Premium, Ultimate] + url: 'https://docs.gitlab.com/ee/user/profile/#add-your-pronunciation' + image_url: https://about.gitlab.com/images/14_2/pronounce.png + published_at: 2021-08-22 + release: 14.2 +- title: View historical CI pipeline minute usage + body: | + Before GitLab 14.2, the CI pipeline minutes usage on the [Usage Quotas](https://docs.gitlab.com/ee/user/admin_area/settings/continuous_integration.html#shared-runners-pipeline-minutes-quota) page only showed the current month's usage. This data would reset every month and there was no way to view activity from the past months for analyzing historical usage. + + Now there are two charts that show historical CI pipeline minutes usage by month or by project, so you can make informed decisions about your pipeline usage. + stage: Verify + self-managed: true + gitlab-com: true + packages: [Free, Premium, Ultimate] + url: 'https://docs.gitlab.com/ee/subscriptions/gitlab_com/index.html#ci-pipeline-minutes' + image_url: https://about.gitlab.com/images/14_2/CI_minutes_usage_graph.png + published_at: 2021-08-22 + release: 14.2 +- title: Edit issue title from an issue board + body: | + Editing an issue in an issue board currently requires many steps and takes you out of your workflow. We've added an easy way to edit an issue's title right in the issue board, without navigating to another page. To edit the title, in the right sidebar, select the issue, then select **Edit**. + stage: Plan + self-managed: true + gitlab-com: true + packages: [Free, Premium, Ultimate] + url: 'https://docs.gitlab.com/ee/user/project/issue_board.html#edit-an-issue' + image_url: https://about.gitlab.com/images/14_2/issue-board-edit-title.gif + published_at: 2021-08-22 + release: 14.2 +- title: Preview Markdown live while editing + body: | + Markdown is a fast and intuitive syntax for writing rich web content. Until it isn't. Luckily, it's easy to preview the rendered output of Markdown to ensure the accuracy of your markup from the **Preview** tab. Unfortunately, the context switch required to move between the raw source code and the preview can be tedious and disruptive to your flow. + + Now, in both the Web IDE and single file editor, Markdown files have a new live preview option available. Right-click the editor and select **Preview Markdown** or use `Command/Control + Shift + P` to toggle a split-screen live preview of your Markdown content. The preview refreshes as you type, so you can be confident that your markup is valid and will render as you intended. + stage: Create + self-managed: true + gitlab-com: true + packages: [Free, Premium, Ultimate] + url: 'https://docs.gitlab.com/ee/user/project/web_ide/#markdown-editing' + image_url: https://about.gitlab.com/images/14_2/create-markdown-live-preview.png + published_at: 2021-08-22 + release: 14.2 +- title: Stageless pipelines + body: | + Using the [`needs`](https://docs.gitlab.com/ee/ci/yaml/#needs) keyword in your pipeline configuration helps to reduce cycle times by ignoring stage ordering and running jobs without waiting for others to complete. Previously, `needs` could only be used between jobs on different stages. + + In this release, we've removed this limitation so you can define a `needs` relationship between any job you want. As a result, you can now create a complete CI/CD pipeline without using stages by including `needs` in every job to implicitly configure the execution order. This lets you define a less verbose pipeline that takes less time to create and can run even faster. + stage: Verify + self-managed: true + gitlab-com: true + packages: [Free, Premium, Ultimate] + url: 'https://docs.gitlab.com/ee/ci/yaml/#needs' + image_url: https://about.gitlab.com/images/14_2/need.png + published_at: 2021-08-22 + release: 14.2 +- title: New GitLab Kubernetes Agent UI + body: | + The GitLab Kubernetes Agent allows a secure bi-directional connection between GitLab and any Kubernetes cluster. Until now, registering a new Kubernetes Agent required writing GraphQL queries. + + As of GitLab 14.2, GitLab ships with a user-friendly user interface and a registration form to help you get started with the Kubernetes Agent with ease. + stage: Configure + self-managed: true + gitlab-com: true + packages: [Premium, Ultimate] + url: 'https://docs.gitlab.com/ee/user/clusters/agent/' + image_url: https://about.gitlab.com/images/14_2/k8s-agent-registration.png + published_at: 2021-08-22 + release: 14.2 +- title: Create a GitLab branch from a Jira issue + body: | + Users of the [GitLab.com for Jira Cloud](https://marketplace.atlassian.com/apps/1221011/gitlab-com-for-jira-cloud) application can now create GitLab branches directly from a Jira issue's [development panel](https://support.atlassian.com/jira-software-cloud/docs/view-development-information-for-an-issue/). This enables developers to begin work on issues without having to switch tools and lose context. + stage: Create + self-managed: true + gitlab-com: true + packages: [Free, Premium, Ultimate] + url: 'https://docs.gitlab.com/ee/integration/jira/connect-app.html' + image_url: https://about.gitlab.com/images/14_2/jira_dev_panel_jira_setup_3.png + published_at: 2021-08-22 + release: 14.2 diff --git a/db/migrate/20210817130415_add_project_id_name_version_id_to_npm_packages.rb b/db/migrate/20210817130415_add_project_id_name_version_id_to_npm_packages.rb new file mode 100644 index 00000000000..e4b681d66fb --- /dev/null +++ b/db/migrate/20210817130415_add_project_id_name_version_id_to_npm_packages.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddProjectIdNameVersionIdToNpmPackages < ActiveRecord::Migration[6.1] + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + INDEX_NAME = 'idx_installable_npm_pkgs_on_project_id_name_version_id' + + def up + add_concurrent_index :packages_packages, [:project_id, :name, :version, :id], where: 'package_type = 2 AND status = 0', name: INDEX_NAME + end + + def down + remove_concurrent_index :packages_packages, [:project_id, :name, :version, :id], where: 'package_type = 2 AND status = 0', name: INDEX_NAME + end +end diff --git a/db/post_migrate/20201106134950_deduplicate_epic_iids.rb b/db/post_migrate/20201106134950_deduplicate_epic_iids.rb index bc7daf9329d..8fddc81057b 100644 --- a/db/post_migrate/20201106134950_deduplicate_epic_iids.rb +++ b/db/post_migrate/20201106134950_deduplicate_epic_iids.rb @@ -85,7 +85,7 @@ class DeduplicateEpicIids < ActiveRecord::Migration[6.0] instance = subject.is_a?(::Class) ? nil : subject - subject.transaction(requires_new: true) do + subject.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions InternalId.create!( **scope, usage: usage_value, diff --git a/db/schema_migrations/20210817130415 b/db/schema_migrations/20210817130415 new file mode 100644 index 00000000000..e8481cb4019 --- /dev/null +++ b/db/schema_migrations/20210817130415 @@ -0,0 +1 @@ +8c1ec0dfc043861377786bd7731a1a1f994d6f03833f4dcc2ba94ab1ddc83acf
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 0710dc312c2..88b7693b749 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -23024,6 +23024,8 @@ CREATE UNIQUE INDEX idx_environment_merge_requests_unique_index ON deployment_me CREATE INDEX idx_geo_con_rep_updated_events_on_container_repository_id ON geo_container_repository_updated_events USING btree (container_repository_id); +CREATE INDEX idx_installable_npm_pkgs_on_project_id_name_version_id ON packages_packages USING btree (project_id, name, version, id) WHERE ((package_type = 2) AND (status = 0)); + CREATE INDEX idx_issues_on_health_status_not_null ON issues USING btree (health_status) WHERE (health_status IS NOT NULL); CREATE INDEX idx_issues_on_project_id_and_created_at_and_id_and_state_id ON issues USING btree (project_id, created_at, id, state_id); diff --git a/doc/administration/cicd.md b/doc/administration/cicd.md new file mode 100644 index 00000000000..89fc31822ee --- /dev/null +++ b/doc/administration/cicd.md @@ -0,0 +1,75 @@ +--- +stage: Verify +group: Pipeline Execution +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments +type: howto +--- + +# GitLab CI/CD instance configuration **(FREE SELF)** + +GitLab CI/CD is enabled by default in all new projects on an instance. You can configure +the instance to have [GitLab CI/CD disabled by default](#disable-gitlab-cicd-in-new-projects) +in new projects. + +You can still choose to [enable GitLab CI/CD in individual projects](../ci/enable_or_disable_ci.md#enable-cicd-in-a-project) +at any time. + +## Disable GitLab CI/CD in new projects + +You can set GitLab CI/CD to be disabled by default in all new projects by modifying the settings in: + +- `gitlab.yml` for source installations. +- `gitlab.rb` for Omnibus GitLab installations. + +Existing projects that already had CI/CD enabled are unchanged. Also, this setting only changes +the project default, so project owners can still enable CI/CD in the project settings. + +For installations from source: + +1. Open `gitlab.yml` with your editor and set `builds` to `false`: + + ```yaml + ## Default project features settings + default_projects_features: + issues: true + merge_requests: true + wiki: true + snippets: false + builds: false + ``` + +1. Save the `gitlab.yml` file. + +1. Restart GitLab: + + ```shell + sudo service gitlab restart + ``` + +For Omnibus GitLab installations: + +1. Edit `/etc/gitlab/gitlab.rb` and add this line: + + ```ruby + gitlab_rails['gitlab_default_projects_features_builds'] = false + ``` + +1. Save the `/etc/gitlab/gitlab.rb` file. + +1. Reconfigure GitLab: + + ```shell + sudo gitlab-ctl reconfigure + ``` + +<!-- ## Troubleshooting + +Include any troubleshooting steps that you can foresee. If you know beforehand what issues +one might have when setting this up, or when something is changed, or on upgrading, it's +important to describe those, too. Think of things that may go wrong and include them here. +This is important to minimize requests for support, and to avoid doc comments with +questions that you know someone might ask. + +Each scenario can be a third-level heading, e.g. `### Getting error message X`. +If you have none to add when creating a doc, leave this section in place +but commented out to help encourage others to add to it in the future. --> diff --git a/doc/administration/index.md b/doc/administration/index.md index 46624ab39a3..d3e018c9769 100644 --- a/doc/administration/index.md +++ b/doc/administration/index.md @@ -164,16 +164,16 @@ Learn how to install, configure, update, and maintain your GitLab instance. - [Limit repository size](../user/admin_area/settings/account_and_limit_settings.md): Set a hard limit for your repositories' size. - [Static objects external storage](static_objects_external_storage.md): Set external storage for static objects in a repository. -## Continuous Integration settings +## CI/CD settings -- [Enable/disable GitLab CI/CD](../ci/enable_or_disable_ci.md#make-gitlab-cicd-disabled-by-default-in-new-projects): Enable or disable GitLab CI/CD for your instance. +- [Enable or disable GitLab CI/CD](cicd.md#disable-gitlab-cicd-in-new-projects): Set new projects in your instance to have GitLab CI/CD enabled or disabled by default. - [GitLab CI/CD administration settings](../user/admin_area/settings/continuous_integration.md): Enable or disable Auto DevOps site-wide and define the artifacts' max size and expiration time. - [External Pipeline Validation](external_pipeline_validation.md): Enable, disable, and configure external pipeline validation. - [Job artifacts](job_artifacts.md): Enable, disable, and configure job artifacts (a set of files and directories which are outputted by a job when it completes successfully). - [Job logs](job_logs.md): Information about the job logs. - [Register runners](../ci/runners/runners_scope.md): Learn how to register and configure runners. - [Shared runners pipelines quota](../user/admin_area/settings/continuous_integration.md#shared-runners-pipeline-minutes-quota): Limit the usage of pipeline minutes for shared runners. -- [Enable/disable Auto DevOps](../topics/autodevops/index.md#enable-or-disable-auto-devops): Enable or disable Auto DevOps for your instance. +- [Enable or disable Auto DevOps](../topics/autodevops/index.md#enable-or-disable-auto-devops): Enable or disable Auto DevOps for your instance. ## Snippet settings diff --git a/doc/ci/enable_or_disable_ci.md b/doc/ci/enable_or_disable_ci.md index 421bca9e324..93b83fed253 100644 --- a/doc/ci/enable_or_disable_ci.md +++ b/doc/ci/enable_or_disable_ci.md @@ -15,8 +15,8 @@ To effectively use GitLab CI/CD, you need: You can read our [quick start guide](quick_start/index.md) to get you started. -If you are using an external CI/CD server like Jenkins or Drone CI, it is advised -to disable GitLab CI/CD in order to not have any conflicts with the commits status +If you use an external CI/CD server like Jenkins or Drone CI, you should +disable GitLab CI/CD to avoid conflicts with the commits status API. GitLab CI/CD is exposed by using the `/pipelines` and `/jobs` pages of a project. @@ -24,17 +24,15 @@ Disabling GitLab CI/CD in a project does not delete any previous jobs. In fact, the `/pipelines` and `/jobs` pages can still be accessed, although it's hidden from the left sidebar menu. -GitLab CI/CD is enabled by default on new installations and can be disabled -either: +GitLab CI/CD is enabled by default on all new projects. You can: -- Individually under each project's settings. -- Site-wide by modifying the settings in `gitlab.yml` and `gitlab.rb` for source - and Omnibus installations respectively. +- Disable GitLab CI/CD [under each project's settings](#enable-cicd-in-a-project). +- Set GitLab CI/CD to be [disabled in all new projects on an instance](../administration/cicd.md). This only applies to pipelines run as part of GitLab CI/CD. This doesn't enable or disable pipelines that are run from an [external integration](../user/project/integrations/overview.md#integrations-listing). -## Per-project user setting +## Enable CI/CD in a project To enable or disable GitLab CI/CD pipelines in your project: @@ -51,54 +49,6 @@ To enable or disable GitLab CI/CD pipelines in your project: Press **Save changes** for the settings to take effect. -## Make GitLab CI/CD disabled by default in new projects - -You can set GitLab CI/CD to be disabled by default in all new projects by modifying the settings in: - -- `gitlab.yml` for source installations. -- `gitlab.rb` for Omnibus GitLab installations. - -Existing projects that already had CI/CD enabled are unchanged. Also, this setting only changes -the project default, so project owners can still enable CI/CD in the project settings. - -For installations from source: - -1. Open `gitlab.yml` with your editor and set `builds` to `false`: - - ```yaml - ## Default project features settings - default_projects_features: - issues: true - merge_requests: true - wiki: true - snippets: false - builds: false - ``` - -1. Save the `gitlab.yml` file. - -1. Restart GitLab: - - ```shell - sudo service gitlab restart - ``` - -For Omnibus GitLab installations: - -1. Edit `/etc/gitlab/gitlab.rb` and add this line: - - ```ruby - gitlab_rails['gitlab_default_projects_features_builds'] = false - ``` - -1. Save the `/etc/gitlab/gitlab.rb` file. - -1. Reconfigure GitLab: - - ```shell - sudo gitlab-ctl reconfigure - ``` - <!-- ## Troubleshooting Include any troubleshooting steps that you can foresee. If you know beforehand what issues diff --git a/doc/ci/index.md b/doc/ci/index.md index 30c36d0a061..b9e56a016a8 100644 --- a/doc/ci/index.md +++ b/doc/ci/index.md @@ -135,8 +135,9 @@ of GitLab CI/CD for: See also: -- [How to enable or disable GitLab CI/CD](enable_or_disable_ci.md). -- Other [CI administration settings](../administration/index.md#continuous-integration-settings). +- [Enable or disable GitLab CI/CD in a project](enable_or_disable_ci.md). +- [Disable GitLab CI/CD by default in new projects](../administration/cicd.md). **(FREE SELF)** +- Other [CI administration settings](../administration/index.md#cicd-settings). ## References diff --git a/doc/development/i18n/proofreader.md b/doc/development/i18n/proofreader.md index 462c3fde7d6..50de1d253ba 100644 --- a/doc/development/i18n/proofreader.md +++ b/doc/development/i18n/proofreader.md @@ -43,6 +43,7 @@ are very appreciative of the work done by translators and proofreaders! - Jan Urbanec - [GitLab](https://gitlab.com/TatranskyMedved), [Crowdin](https://crowdin.com/profile/Tatranskymedved) - Danish - Saederup92 - [GitLab](https://gitlab.com/Saederup92), [Crowdin](https://crowdin.com/profile/Saederup92) + - scootergrisen - [GitLab](https://gitlab.com/scootergrisen), [Crowdin](https://crowdin.com/profile/scootergrisen) - Dutch - Emily Hendle - [GitLab](https://gitlab.com/pundachan), [Crowdin](https://crowdin.com/profile/pandachan) - Esperanto @@ -117,6 +118,7 @@ are very appreciative of the work done by translators and proofreaders! - Turkish - Ali Demirtaş - [GitLab](https://gitlab.com/alidemirtas), [Crowdin](https://crowdin.com/profile/alidemirtas) - Rıfat Ünalmış (Rifat Unalmis) - [GitLab](https://gitlab.com/runalmis), [Crowdin](https://crowdin.com/profile/runalmis) + - İsmail Arılık - [GitLab](https://gitlab.com/ismailarilik), [Crowdin](https://crowdin.com/profile/ismailarilik) - Ukrainian - Volodymyr Sobotovych - [GitLab](https://gitlab.com/wheleph), [Crowdin](https://crowdin.com/profile/wheleph) - Andrew Vityuk - [GitLab](https://gitlab.com/3_1_3_u), [Crowdin](https://crowdin.com/profile/andruwa13) diff --git a/doc/topics/autodevops/customize.md b/doc/topics/autodevops/customize.md index 72f688a0ed5..61cb29001dd 100644 --- a/doc/topics/autodevops/customize.md +++ b/doc/topics/autodevops/customize.md @@ -491,9 +491,9 @@ these prefixed variables available to the deployed application as environment va To configure your application variables: -1. Go to your project's **Settings > CI/CD**, then expand the - **Variables** section. - +1. On the top bar, select **Menu > Projects** and find your project. +1. On the left sidebar, select **Settings > CI/CD**. +1. Expand **Variables**. 1. Create a CI/CD variable, ensuring the key is prefixed with `K8S_SECRET_`. For example, you can create a variable with key `K8S_SECRET_RAILS_MASTER_KEY`. diff --git a/doc/topics/autodevops/requirements.md b/doc/topics/autodevops/requirements.md index 535ec18e5b6..ad00a1687a4 100644 --- a/doc/topics/autodevops/requirements.md +++ b/doc/topics/autodevops/requirements.md @@ -42,7 +42,9 @@ that works best for your needs: You can choose the deployment method when enabling Auto DevOps or later: -1. In GitLab, go to your project's **Settings > CI/CD > Auto DevOps**. +1. In GitLab, on the top bar, select **Menu > Projects** and find your project. +1. On the left sidebar, select **Settings > CI/CD**. +1. Expand **Auto DevOps**. 1. Choose the deployment strategy. 1. Select **Save changes**. @@ -181,9 +183,9 @@ You can choose to target [AWS ECS](../../ci/cloud_deployment/index.md) as a depl To get started on Auto DevOps to AWS ECS, you must add a specific CI/CD variable. To do so, follow these steps: -1. In your project, go to **Settings > CI/CD** and expand the **Variables** - section. - +1. In GitLab, on the top bar, select **Menu > Projects** and find your project. +1. On the left sidebar, select **Settings > CI/CD**. +1. Expand **Auto DevOps**. 1. Specify which AWS platform to target during the Auto DevOps deployment by adding the `AUTO_DEVOPS_PLATFORM_TARGET` variable with one of the following values: - `FARGATE` if the service you're targeting must be of launch type FARGATE. diff --git a/doc/topics/autodevops/stages.md b/doc/topics/autodevops/stages.md index 3b595cc7ea8..2f354ad9e66 100644 --- a/doc/topics/autodevops/stages.md +++ b/doc/topics/autodevops/stages.md @@ -425,9 +425,9 @@ including support for `Deployment` in the `extensions/v1beta1` version. To use Auto Deploy on a Kubernetes 1.16+ cluster: 1. If you are deploying your application for the first time in GitLab 13.0 or - newer, no configuration should be required. + later, no configuration should be required. -1. In GitLab 12.10 or older, set the following in the [`.gitlab/auto-deploy-values.yaml` file](customize.md#customize-values-for-helm-chart): +1. In GitLab 12.10 and earlier, set the following in the [`.gitlab/auto-deploy-values.yaml` file](customize.md#customize-values-for-helm-chart): ```yaml deploymentApiVersion: apps/v1 @@ -696,11 +696,12 @@ To use Auto Monitoring: 1. [Install and configure the Auto DevOps requirements](requirements.md). 1. [Enable Auto DevOps](index.md#enable-or-disable-auto-devops), if you haven't done already. -1. Navigate to your project's **{rocket}** **CI/CD > Pipelines** and click **Run pipeline**. +1. On the left sidebar, select **CI/CD > Pipelines**. +1. Select **Run pipeline**. 1. After the pipeline finishes successfully, open the [monitoring dashboard for a deployed environment](../../ci/environments/index.md#monitor-environments) to view the metrics of your deployed application. To view the metrics of the - whole Kubernetes cluster, navigate to **Operations > Metrics**. + whole Kubernetes cluster, on the left sidebar, select **Monitor > Metrics**. ![Auto Metrics](img/auto_monitoring.png) diff --git a/doc/user/project/repository/x509_signed_commits/index.md b/doc/user/project/repository/x509_signed_commits/index.md index 7c115734345..b19943b9ff0 100644 --- a/doc/user/project/repository/x509_signed_commits/index.md +++ b/doc/user/project/repository/x509_signed_commits/index.md @@ -23,12 +23,18 @@ GitLab uses its own certificate store and therefore defines the trust chain. For a commit or tag to be *verified* by GitLab: -- The signing certificate email must match a verified email address used by the committer in GitLab. -- The Certificate Authority has to be trusted by the GitLab instance, see also - [Omnibus install custom public certificates](https://docs.gitlab.com/omnibus/settings/ssl.html#install-custom-public-certificates). +- The signing certificate email must match a verified email address in GitLab. +- The GitLab instance must be able to establish a full trust chain from the certificate + in the signature to a trusted certificate in the GitLab certificate store. This chain + may include intermediate certificates supplied in the signature. Additional + certificates, such as the Certificate Authority root, + [may need to be added to the GitLab certificate store](https://docs.gitlab.com/omnibus/settings/ssl.html#install-custom-public-certificates). - The signing time has to be within the time range of the [certificate validity](https://www.rfc-editor.org/rfc/rfc5280.html#section-4.1.2.5) which is usually up to three years. - The signing time is equal or later than commit time. +- If the status of a commit has already been determined and stored in the database, + [use the Rake task to re-check the status](../../../../raketasks/x509_signatures.md). + [Read more about this and detailed troubleshooting steps](#troubleshooting). NOTE: Certificate revocation lists are checked on a daily basis via background worker. @@ -152,3 +158,196 @@ To verify that a tag is signed, you can use the `--verify` flag: ```shell git tag --verify v1.1.1 ``` + +## Troubleshooting + +### Re-verify commits + +GitLab stores the status of any checked commits in the database. You can use a +Rake task to [check the status of any previously checked commits](../../../../raketasks/x509_signatures.md). + +After you make any changes, run this command: + +```shell +sudo gitlab-rake gitlab:x509:update_signatures +``` + +### Main verification checks + +The code performs +[six key checks](https://gitlab.com/gitlab-org/gitlab/-/blob/v14.1.0-ee/lib/gitlab/x509/signature.rb#L33), +which all must return `verified`: + +- `x509_certificate.nil?` should be false. +- `x509_certificate.revoked?` should be false. +- `verified_signature` should be true. +- `user.nil?`should be false. +- `user.verified_emails.include?(@email)` should be true. +- `certificate_email == @email` should be true. + +To investigate why a commit shows as `Unverified`: + +1. [Start a Rails console](../../../../administration/operations/rails_console.md#starting-a-rails-console-session): + + ```shell + sudo gitlab-rails console + ``` + +1. Identify the project (either by path or ID) and full commit SHA that you're investigating. + Use this information to create `signature` to run other checks against: + + ```ruby + project = Project.find_by_full_path('group/subgroup/project') + project = Project.find_by_id('121') + commit = project.repository.commit_by(oid: '87fdbd0f9382781442053b0b76da729344e37653') + signedcommit=Gitlab::X509::Commit.new(commit) + signature=Gitlab::X509::Signature.new(signedcommit.signature_text, signedcommit.signed_text, commit.committer_email, commit.created_at) + ``` + + If you make changes to address issues identified running through the checks, restart the + Rails console and run though the checks again from the start. + +1. Check the certificate on the commit: + + ```ruby + signature.x509_certificate.nil? + signature.x509_certificate.revoked? + ``` + + Both checks should return `false`: + + ```ruby + > signature.x509_certificate.nil? + => false + > signature.x509_certificate.revoked? + => false + ``` + + A [known issue](https://gitlab.com/gitlab-org/gitlab/-/issues/332503) causes + these checks to fail with `Validation failed: Subject key identifier is invalid`. + +1. Run a cryptographic check on the signature. The code must return `true`: + + ```ruby + signature.verified_signature + ``` + + If it returns `false` then [investigate this check further](#cryptographic-verification-checks). + +1. Confirm the email addresses match on the commit and the signature: + + - The Rails console displays the email addresses being compared. + - The final command must return `true`: + + ```ruby + sigemail=signature.__send__:certificate_email + commitemail=commit.committer_email + sigemail == commitemail + ``` + + A [known issue](https://gitlab.com/gitlab-org/gitlab/-/issues/336677) exists: + only the first email in the `Subject Alternative Name` list is compared. To + display the `Subject Alternative Name` list, run: + + ```ruby + signature.__send__ :get_certificate_extension,'subjectAltName' + ``` + + If the developer's email address is not the first one in the list, this check + will not work, and the commit will be `unverified`. + +1. The email address on the commit must be associated with an account in GitLab. + This check should return `false`: + + ```ruby + signature.user.nil? + ``` + +1. Check the email address is associated with a user in GitLab. This check should + return a user, such as `#<User id:1234 @user_handle>`: + + ```ruby + User.find_by_any_email(commit.committer_email) + ``` + + If it returns `nil`, the email address is not associated with a user, and the check fails. + +1. Confirm the developer's email address is verified. This check must return true: + + ```ruby + signature.user.verified_emails.include?(commit.committer_email) + ``` + + If the previous check returned `nil`, this command displays an error: + + ```plaintext + NoMethodError (undefined method `verified_emails' for nil:NilClass) + ``` + +1. The verification status is stored in the database. To display the database record: + + ```ruby + pp X509CommitSignature.by_commit_sha(commit.sha);nil + ``` + + If all the previous checks returned the correct values: + + - `verification_status: "unverified"` indicates the database record needs + updating. [Use the Rake task](#re-verify-commits). + + - `[]` indicates the database doesn't have a record yet. Locate the commit + in GitLab to check the signature and store the result. + +#### Cryptographic verification checks + +If GitLab determines that `verified_signature` is `false`, investigate the reason +in the Rails console. These checks require `signature` to exist. Refer to the `signature` +step of the previous [main verification checks](#main-verification-checks). + +1. Check the signature, without checking the issuer, returns `true`: + + ```ruby + signature.__send__ :valid_signature? + ``` + +1. Check the signing time and date. This check must return `true`: + + ```ruby + signature.__send__ :valid_signing_time? + ``` + + - The code allows for code signing certificates to expire. + - A commit must be signed during the validity period of the certificate, + and at or after the commit's datestamp. Display the commit time and + certificate details including `not_before`, `not_after` with: + + ```ruby + commit.created_at + pp signature.__send__ :cert; nil + ``` + +1. Check the signature, including that TLS trust can be established. This check must return `true`: + + ```ruby + signature.__send__(:p7).verify([], signature.__send__(:cert_store), signature.__send__(:signed_text)) + ``` + + 1. If this fails, add the missing certificate(s) required to establish trust + [to the GitLab certificate store](https://docs.gitlab.com/omnibus/settings/ssl.html#install-custom-public-certificates). + + 1. After adding more certificates, (if these troubleshooting steps then pass) + run the Rake task to [re-verify commits](#re-verify-commits). + + 1. Display the certificates, including in the signature: + + ```ruby + pp signature.__send__(:p7).certificates ; nil + ``` + +Ensure any additional intermediate certificate(s) and the root certificate are added +to the certificate store. For consistency with how certificate chains are built on +web servers, Git clients that are signing commits should include the certificate +and all intermediate certificates in the signature, and the GitLab certificate +store should only contain the root. If you remove a root certificate from the GitLab +trust store, such as when it expires, commit signatures which chain back to that +root display as `unverified`. diff --git a/lib/gitlab/background_migration/backfill_design_internal_ids.rb b/lib/gitlab/background_migration/backfill_design_internal_ids.rb index 6d1df95c66d..236c6b6eb9a 100644 --- a/lib/gitlab/background_migration/backfill_design_internal_ids.rb +++ b/lib/gitlab/background_migration/backfill_design_internal_ids.rb @@ -73,7 +73,7 @@ module Gitlab # violation. We can safely roll-back the nested transaction and perform # a lookup instead to retrieve the record. def create_record - subject.transaction(requires_new: true) do + subject.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions InternalId.create!( **scope, usage: usage_value, diff --git a/lib/gitlab/background_migration/backfill_project_repositories.rb b/lib/gitlab/background_migration/backfill_project_repositories.rb index f5c8796bd18..a9eaeb0562d 100644 --- a/lib/gitlab/background_migration/backfill_project_repositories.rb +++ b/lib/gitlab/background_migration/backfill_project_repositories.rb @@ -21,7 +21,7 @@ module Gitlab shard_id = shards.fetch(name, nil) return shard_id if shard_id.present? - Shard.transaction(requires_new: true) do + Shard.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions create!(name) end rescue ActiveRecord::RecordNotUnique diff --git a/lib/gitlab/database/with_lock_retries.rb b/lib/gitlab/database/with_lock_retries.rb index bbf8f133f0f..70acbeac9e1 100644 --- a/lib/gitlab/database/with_lock_retries.rb +++ b/lib/gitlab/database/with_lock_retries.rb @@ -122,7 +122,7 @@ module Gitlab end def run_block_with_lock_timeout - ActiveRecord::Base.transaction(requires_new: true) do + ActiveRecord::Base.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions execute("SET LOCAL lock_timeout TO '#{current_lock_timeout_in_ms}ms'") log(message: 'Lock timeout is set', current_iteration: current_iteration, lock_timeout_in_ms: current_lock_timeout_in_ms) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index fc1af4f678f..e629b9d5ad8 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -3698,6 +3698,12 @@ msgstr "" msgid "An error occurred while loading merge requests." msgstr "" +msgid "An error occurred while loading the Needs tab." +msgstr "" + +msgid "An error occurred while loading the Test Reports tab." +msgstr "" + msgid "An error occurred while loading the access tokens form, please try again." msgstr "" @@ -10723,6 +10729,9 @@ msgstr "" msgid "Define environments in the deploy stage(s) in %{code_open}.gitlab-ci.yml%{code_close} to track deployments here." msgstr "" +msgid "Define how approval rules are applied as a merge request moves toward completion." +msgstr "" + msgid "Definition" msgstr "" @@ -19588,9 +19597,6 @@ msgstr "" msgid "Last repository check (%{last_check_timestamp}) failed. See the 'repocheck.log' file for error messages." msgstr "" -msgid "Last repository check run" -msgstr "" - msgid "Last seen" msgstr "" @@ -19609,6 +19615,9 @@ msgstr "" msgid "Last successful update" msgstr "" +msgid "Last time checked" +msgstr "" + msgid "Last time verified" msgstr "" diff --git a/rubocop/cop/performance/active_record_subtransactions.rb b/rubocop/cop/performance/active_record_subtransactions.rb new file mode 100644 index 00000000000..a550b558e52 --- /dev/null +++ b/rubocop/cop/performance/active_record_subtransactions.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module Performance + class ActiveRecordSubtransactions < RuboCop::Cop::Cop + MSG = 'Subtransactions should not be used. ' \ + 'For more information see: https://gitlab.com/gitlab-org/gitlab/-/issues/338346' + + def_node_matcher :match_transaction_with_options, <<~PATTERN + (send _ :transaction (hash $...)) + PATTERN + + def_node_matcher :subtransaction_option?, <<~PATTERN + (pair (:sym :requires_new) (true)) + PATTERN + + def on_send(node) + match_transaction_with_options(node) do |option_nodes| + option_nodes.each do |option_node| + next unless subtransaction_option?(option_node) + + add_offense(option_node) + end + end + end + end + end + end +end diff --git a/rubocop/rubocop-migrations.yml b/rubocop/rubocop-migrations.yml index 979b0fa2936..a98a059df1d 100644 --- a/rubocop/rubocop-migrations.yml +++ b/rubocop/rubocop-migrations.yml @@ -14,6 +14,7 @@ Migration/UpdateLargeTable: - :events - :gitlab_subscriptions - :issues + - :members - :merge_request_diff_commits - :merge_request_diff_files - :merge_request_diffs diff --git a/spec/factories/packages.rb b/spec/factories/packages.rb index cd9c8a8bfbb..d4993cc765a 100644 --- a/spec/factories/packages.rb +++ b/spec/factories/packages.rb @@ -112,7 +112,7 @@ FactoryBot.define do factory :npm_package do sequence(:name) { |n| "@#{project.root_namespace.path}/package-#{n}"} - version { '1.0.0' } + sequence(:version) { |n| "1.0.#{n}" } package_type { :npm } after :create do |package| diff --git a/spec/finders/packages/npm/package_finder_spec.rb b/spec/finders/packages/npm/package_finder_spec.rb index a995f3b96c4..99174e2dd3a 100644 --- a/spec/finders/packages/npm/package_finder_spec.rb +++ b/spec/finders/packages/npm/package_finder_spec.rb @@ -68,6 +68,20 @@ RSpec.describe ::Packages::Npm::PackageFinder do it { is_expected.to be_empty } end + + context 'with npm_presenter_queries_tuning disabled' do + before do + stub_feature_flags(npm_presenter_queries_tuning: false) + end + + it_behaves_like 'finding packages by name' + + context 'set to nil' do + let(:project) { nil } + + it { is_expected.to be_empty } + end + end end context 'with a namespace' do @@ -80,6 +94,20 @@ RSpec.describe ::Packages::Npm::PackageFinder do it { is_expected.to be_empty } end + + context 'with npm_presenter_queries_tuning disabled' do + before do + stub_feature_flags(npm_presenter_queries_tuning: false) + end + + it_behaves_like 'accepting a namespace for', 'finding packages by name' + + context 'set to nil' do + let_it_be(:namespace) { nil } + + it { is_expected.to be_empty } + end + end end end @@ -109,6 +137,24 @@ RSpec.describe ::Packages::Npm::PackageFinder do it_behaves_like 'accepting a namespace for', 'finding packages by version' end + + context 'with npm_presenter_queries_tuning disabled' do + before do + stub_feature_flags(npm_presenter_queries_tuning: false) + end + + context 'with a project' do + let(:finder) { described_class.new(package_name, project: project) } + + it_behaves_like 'finding packages by version' + end + + context 'with a namespace' do + let(:finder) { described_class.new(package_name, namespace: namespace) } + + it_behaves_like 'accepting a namespace for', 'finding packages by version' + end + end end describe '#last' do @@ -118,31 +164,43 @@ RSpec.describe ::Packages::Npm::PackageFinder do it { is_expected.to eq(package) } end - context 'with a project' do - let(:finder) { described_class.new(package_name, project: project) } + shared_examples 'handling project or namespace parameter' do + context 'with a project' do + let(:finder) { described_class.new(package_name, project: project) } - it_behaves_like 'finding package by last' - end + it_behaves_like 'finding package by last' + end - context 'with a namespace' do - let(:finder) { described_class.new(package_name, namespace: namespace) } + context 'with a namespace' do + let(:finder) { described_class.new(package_name, namespace: namespace) } - it_behaves_like 'accepting a namespace for', 'finding package by last' + it_behaves_like 'accepting a namespace for', 'finding package by last' - context 'with duplicate packages' do - let_it_be(:namespace) { create(:group) } - let_it_be(:subgroup1) { create(:group, parent: namespace) } - let_it_be(:subgroup2) { create(:group, parent: namespace) } - let_it_be(:project2) { create(:project, namespace: subgroup2) } - let_it_be(:package2) { create(:npm_package, name: package.name, project: project2) } + context 'with duplicate packages' do + let_it_be(:namespace) { create(:group) } + let_it_be(:subgroup1) { create(:group, parent: namespace) } + let_it_be(:subgroup2) { create(:group, parent: namespace) } + let_it_be(:project2) { create(:project, namespace: subgroup2) } + let_it_be(:package2) { create(:npm_package, name: package.name, project: project2) } - before do - project.update!(namespace: subgroup1) + before do + project.update!(namespace: subgroup1) + end + + # the most recent one is returned + it { is_expected.to eq(package2) } end + end + end - # the most recent one is returned - it { is_expected.to eq(package2) } + it_behaves_like 'handling project or namespace parameter' + + context 'with npm_presenter_queries_tuning disabled' do + before do + stub_feature_flags(npm_presenter_queries_tuning: false) end + + it_behaves_like 'handling project or namespace parameter' end end end diff --git a/spec/frontend/content_editor/services/markdown_serializer_spec.js b/spec/frontend/content_editor/services/markdown_serializer_spec.js index 24b3779057c..868473faa14 100644 --- a/spec/frontend/content_editor/services/markdown_serializer_spec.js +++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js @@ -115,6 +115,64 @@ describe('markdownSerializer', () => { expect(serialize(paragraph('hello', hardBreak(), 'world'))).toBe('hello\\\nworld'); }); + it('correctly serializes a link', () => { + expect(serialize(paragraph(link({ href: 'https://example.com' }, 'example url')))).toBe( + '[example url](https://example.com)', + ); + }); + + it('correctly serializes a link with a title', () => { + expect( + serialize( + paragraph(link({ href: 'https://example.com', title: 'click this link' }, 'example url')), + ), + ).toBe('[example url](https://example.com "click this link")'); + }); + + it('correctly serializes a link with a canonicalSrc', () => { + expect( + serialize( + paragraph( + link( + { + href: '/uploads/abcde/file.zip', + canonicalSrc: 'file.zip', + title: 'click here to download', + }, + 'download file', + ), + ), + ), + ).toBe('[download file](file.zip "click here to download")'); + }); + + it('correctly serializes an image', () => { + expect(serialize(paragraph(image({ src: 'img.jpg', alt: 'foo bar' })))).toBe( + '![foo bar](img.jpg)', + ); + }); + + it('correctly serializes an image with a title', () => { + expect(serialize(paragraph(image({ src: 'img.jpg', title: 'baz', alt: 'foo bar' })))).toBe( + '![foo bar](img.jpg "baz")', + ); + }); + + it('correctly serializes an image with a canonicalSrc', () => { + expect( + serialize( + paragraph( + image({ + src: '/uploads/abcde/file.png', + alt: 'this is an image', + canonicalSrc: 'file.png', + title: 'foo bar baz', + }), + ), + ), + ).toBe('![this is an image](file.png "foo bar baz")'); + }); + it('correctly serializes a table with inline content', () => { expect( serialize( diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js index c8ac7ffc9d9..6f186ba3227 100644 --- a/spec/frontend/lib/utils/url_utility_spec.js +++ b/spec/frontend/lib/utils/url_utility_spec.js @@ -645,29 +645,6 @@ describe('URL utility', () => { }); }); - describe('urlParamsToObject', () => { - it('parses path for label with trailing +', () => { - // eslint-disable-next-line import/no-deprecated - expect(urlUtils.urlParamsToObject('label_name[]=label%2B', {})).toEqual({ - label_name: ['label+'], - }); - }); - - it('parses path for milestone with trailing +', () => { - // eslint-disable-next-line import/no-deprecated - expect(urlUtils.urlParamsToObject('milestone_title=A%2B', {})).toEqual({ - milestone_title: 'A+', - }); - }); - - it('parses path for search terms with spaces', () => { - // eslint-disable-next-line import/no-deprecated - expect(urlUtils.urlParamsToObject('search=two+words', {})).toEqual({ - search: 'two words', - }); - }); - }); - describe('queryToObject', () => { it.each` case | query | options | result diff --git a/spec/presenters/packages/npm/package_presenter_spec.rb b/spec/presenters/packages/npm/package_presenter_spec.rb index e524edaadc6..a8ecc809a77 100644 --- a/spec/presenters/packages/npm/package_presenter_spec.rb +++ b/spec/presenters/packages/npm/package_presenter_spec.rb @@ -5,62 +5,131 @@ require 'spec_helper' RSpec.describe ::Packages::Npm::PackagePresenter do let_it_be(:project) { create(:project) } let_it_be(:package_name) { "@#{project.root_namespace.path}/test" } + let_it_be(:package1) { create(:npm_package, version: '2.0.4', project: project, name: package_name) } + let_it_be(:package2) { create(:npm_package, version: '2.0.6', project: project, name: package_name) } + let_it_be(:latest_package) { create(:npm_package, version: '2.0.11', project: project, name: package_name) } - let!(:package1) { create(:npm_package, version: '1.0.4', project: project, name: package_name) } - let!(:package2) { create(:npm_package, version: '1.0.6', project: project, name: package_name) } - let!(:latest_package) { create(:npm_package, version: '1.0.11', project: project, name: package_name) } let(:packages) { project.packages.npm.with_name(package_name).last_of_each_version } let(:presenter) { described_class.new(package_name, packages) } describe '#versions' do subject { presenter.versions } - context 'for packages without dependencies' do - it { is_expected.to be_a(Hash) } - it { expect(subject[package1.version].with_indifferent_access).to match_schema('public_api/v4/packages/npm_package_version') } - it { expect(subject[package2.version].with_indifferent_access).to match_schema('public_api/v4/packages/npm_package_version') } + shared_examples 'returning packages versions' do |expect_n_plus_one: false| + context 'for packages without dependencies' do + it { is_expected.to be_a(Hash) } + it { expect(subject[package1.version].with_indifferent_access).to match_schema('public_api/v4/packages/npm_package_version') } + it { expect(subject[package2.version].with_indifferent_access).to match_schema('public_api/v4/packages/npm_package_version') } - described_class::NPM_VALID_DEPENDENCY_TYPES.each do |dependency_type| - it { expect(subject.dig(package1.version, dependency_type)).to be nil } - it { expect(subject.dig(package2.version, dependency_type)).to be nil } + ::Packages::DependencyLink.dependency_types.keys.each do |dependency_type| + it { expect(subject.dig(package1.version, dependency_type)).to be nil } + it { expect(subject.dig(package2.version, dependency_type)).to be nil } + end + + it 'avoids N+1 database queries' do + check_n_plus_one(:versions, expect_it: expect_n_plus_one) do + create_list(:npm_package, 5, project: project, name: package_name) + end + end end - end - context 'for packages with dependencies' do - described_class::NPM_VALID_DEPENDENCY_TYPES.each do |dependency_type| - let!("package_dependency_link_for_#{dependency_type}") { create(:packages_dependency_link, package: package1, dependency_type: dependency_type) } + context 'for packages with dependencies' do + ::Packages::DependencyLink.dependency_types.keys.each do |dependency_type| + let_it_be("package_dependency_link_for_#{dependency_type}") { create(:packages_dependency_link, package: package1, dependency_type: dependency_type) } + end + + it { is_expected.to be_a(Hash) } + it { expect(subject[package1.version].with_indifferent_access).to match_schema('public_api/v4/packages/npm_package_version') } + it { expect(subject[package2.version].with_indifferent_access).to match_schema('public_api/v4/packages/npm_package_version') } + ::Packages::DependencyLink.dependency_types.keys.each do |dependency_type| + it { expect(subject.dig(package1.version, dependency_type.to_s)).to be_any } + end + + it 'avoids N+1 database queries' do + check_n_plus_one(:versions, expect_it: expect_n_plus_one) do + create_list(:npm_package, 5, project: project, name: package_name).each do |npm_package| + ::Packages::DependencyLink.dependency_types.keys.each do |dependency_type| + create(:packages_dependency_link, package: npm_package, dependency_type: dependency_type) + end + end + end + end end + end + + it_behaves_like 'returning packages versions' - it { is_expected.to be_a(Hash) } - it { expect(subject[package1.version].with_indifferent_access).to match_schema('public_api/v4/packages/npm_package_version') } - it { expect(subject[package2.version].with_indifferent_access).to match_schema('public_api/v4/packages/npm_package_version') } - described_class::NPM_VALID_DEPENDENCY_TYPES.each do |dependency_type| - it { expect(subject.dig(package1.version, dependency_type.to_s)).to be_any } + context 'with npm_presenter_queries_tuning disabled' do + before do + stub_feature_flags(npm_presenter_queries_tuning: false) end + + it_behaves_like 'returning packages versions', expect_n_plus_one: true end end describe '#dist_tags' do subject { presenter.dist_tags } - context 'for packages without tags' do - it { is_expected.to be_a(Hash) } - it { expect(subject["latest"]).to eq(latest_package.version) } + shared_examples 'returning packages tags' do + context 'for packages without tags' do + it { is_expected.to be_a(Hash) } + it { expect(subject["latest"]).to eq(latest_package.version) } + + it 'avoids N+1 database queries' do + check_n_plus_one(:dist_tags) do + create_list(:npm_package, 5, project: project, name: package_name) + end + end + end + + context 'for packages with tags' do + let_it_be(:package_tag1) { create(:packages_tag, package: package1, name: 'release_a') } + let_it_be(:package_tag2) { create(:packages_tag, package: package1, name: 'test_release') } + let_it_be(:package_tag3) { create(:packages_tag, package: package2, name: 'release_b') } + let_it_be(:package_tag4) { create(:packages_tag, package: latest_package, name: 'release_c') } + let_it_be(:package_tag5) { create(:packages_tag, package: latest_package, name: 'latest') } + + it { is_expected.to be_a(Hash) } + it { expect(subject[package_tag1.name]).to eq(package1.version) } + it { expect(subject[package_tag2.name]).to eq(package1.version) } + it { expect(subject[package_tag3.name]).to eq(package2.version) } + it { expect(subject[package_tag4.name]).to eq(latest_package.version) } + it { expect(subject[package_tag5.name]).to eq(latest_package.version) } + + it 'avoids N+1 database queries' do + check_n_plus_one(:dist_tags) do + create_list(:npm_package, 5, project: project, name: package_name).each_with_index do |npm_package, index| + create(:packages_tag, package: npm_package, name: "tag_#{index}") + end + end + end + end end - context 'for packages with tags' do - let!(:package_tag1) { create(:packages_tag, package: package1, name: 'release_a') } - let!(:package_tag2) { create(:packages_tag, package: package1, name: 'test_release') } - let!(:package_tag3) { create(:packages_tag, package: package2, name: 'release_b') } - let!(:package_tag4) { create(:packages_tag, package: latest_package, name: 'release_c') } - let!(:package_tag5) { create(:packages_tag, package: latest_package, name: 'latest') } - - it { is_expected.to be_a(Hash) } - it { expect(subject[package_tag1.name]).to eq(package1.version) } - it { expect(subject[package_tag2.name]).to eq(package1.version) } - it { expect(subject[package_tag3.name]).to eq(package2.version) } - it { expect(subject[package_tag4.name]).to eq(latest_package.version) } - it { expect(subject[package_tag5.name]).to eq(latest_package.version) } + it_behaves_like 'returning packages tags' + + context 'with npm_presenter_queries_tuning disabled' do + before do + stub_feature_flags(npm_presenter_queries_tuning: false) + end + + it_behaves_like 'returning packages tags' + end + end + + def check_n_plus_one(field, expect_it: false) + pkgs = project.packages.npm.with_name(package_name).last_of_each_version.preload_files + control = ActiveRecord::QueryRecorder.new { described_class.new(package_name, pkgs).public_send(field) } + + yield + + pkgs = project.packages.npm.with_name(package_name).last_of_each_version.preload_files + + if expect_it + expect { described_class.new(package_name, pkgs).public_send(field) }.to exceed_query_limit(control) + else + expect { described_class.new(package_name, pkgs).public_send(field) }.not_to exceed_query_limit(control) end end end diff --git a/spec/requests/api/issues/issues_spec.rb b/spec/requests/api/issues/issues_spec.rb index 125db58ed69..4f13a72e40c 100644 --- a/spec/requests/api/issues/issues_spec.rb +++ b/spec/requests/api/issues/issues_spec.rb @@ -9,15 +9,17 @@ RSpec.describe API::Issues do create(:project, :public, :repository, creator_id: user.id, namespace: user.namespace, merge_requests_access_level: ProjectFeature::PRIVATE) end - let(:user2) { create(:user) } - let(:non_member) { create(:user) } + let_it_be(:user2) { create(:user) } + let_it_be(:non_member) { create(:user) } let_it_be(:guest) { create(:user) } let_it_be(:author) { create(:author) } let_it_be(:assignee) { create(:assignee) } - let(:admin) { create(:user, :admin) } - let(:issue_title) { 'foo' } - let(:issue_description) { 'closed' } - let!(:closed_issue) do + let_it_be(:admin) { create(:user, :admin) } + + let_it_be(:milestone) { create(:milestone, title: '1.0.0', project: project) } + let_it_be(:empty_milestone) { create(:milestone, title: '2.0.0', project: project) } + + let_it_be(:closed_issue) do create :closed_issue, author: user, assignees: [user], @@ -29,7 +31,7 @@ RSpec.describe API::Issues do closed_at: 1.hour.ago end - let!(:confidential_issue) do + let_it_be(:confidential_issue) do create :issue, :confidential, project: project, @@ -39,7 +41,7 @@ RSpec.describe API::Issues do updated_at: 2.hours.ago end - let!(:issue) do + let_it_be(:issue) do create :issue, author: user, assignees: [user], @@ -47,21 +49,16 @@ RSpec.describe API::Issues do milestone: milestone, created_at: generate(:past_time), updated_at: 1.hour.ago, - title: issue_title, - description: issue_description + title: 'foo', + description: 'bar' end let_it_be(:label) do create(:label, title: 'label', color: '#FFAABB', project: project) end - let!(:label_link) { create(:label_link, label: label, target: issue) } - let(:milestone) { create(:milestone, title: '1.0.0', project: project) } - let_it_be(:empty_milestone) do - create(:milestone, title: '2.0.0', project: project) - end - - let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) } + let_it_be(:label_link) { create(:label_link, label: label, target: issue) } + let_it_be(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) } let(:no_milestone_title) { 'None' } let(:any_milestone_title) { 'Any' } @@ -711,8 +708,8 @@ RSpec.describe API::Issues do milestone: milestone, created_at: closed_issue.created_at, updated_at: 1.hour.ago, - title: issue_title, - description: issue_description + title: 'foo', + description: 'bar' end it 'page breaks first page correctly' do @@ -1001,13 +998,15 @@ RSpec.describe API::Issues do end describe 'DELETE /projects/:id/issues/:issue_iid' do + let(:issue_for_deletion) { create(:issue, author: user, assignees: [user], project: project) } + it 'rejects a non member from deleting an issue' do - delete api("/projects/#{project.id}/issues/#{issue.iid}", non_member) + delete api("/projects/#{project.id}/issues/#{issue_for_deletion.iid}", non_member) expect(response).to have_gitlab_http_status(:forbidden) end it 'rejects a developer from deleting an issue' do - delete api("/projects/#{project.id}/issues/#{issue.iid}", author) + delete api("/projects/#{project.id}/issues/#{issue_for_deletion.iid}", author) expect(response).to have_gitlab_http_status(:forbidden) end @@ -1016,13 +1015,13 @@ RSpec.describe API::Issues do let(:project) { create(:project, namespace: owner.namespace) } it 'deletes the issue if an admin requests it' do - delete api("/projects/#{project.id}/issues/#{issue.iid}", owner) + delete api("/projects/#{project.id}/issues/#{issue_for_deletion.iid}", owner) expect(response).to have_gitlab_http_status(:no_content) end it_behaves_like '412 response' do - let(:request) { api("/projects/#{project.id}/issues/#{issue.iid}", owner) } + let(:request) { api("/projects/#{project.id}/issues/#{issue_for_deletion.iid}", owner) } end end @@ -1035,7 +1034,7 @@ RSpec.describe API::Issues do end it 'returns 404 when using the issue ID instead of IID' do - delete api("/projects/#{project.id}/issues/#{issue.id}", user) + delete api("/projects/#{project.id}/issues/#{issue_for_deletion.id}", user) expect(response).to have_gitlab_http_status(:not_found) end diff --git a/spec/rubocop/cop/performance/active_record_subtransactions_spec.rb b/spec/rubocop/cop/performance/active_record_subtransactions_spec.rb new file mode 100644 index 00000000000..0da2e30062a --- /dev/null +++ b/spec/rubocop/cop/performance/active_record_subtransactions_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' +require_relative '../../../../rubocop/cop/performance/active_record_subtransactions' + +RSpec.describe RuboCop::Cop::Performance::ActiveRecordSubtransactions do + subject(:cop) { described_class.new } + + let(:message) { described_class::MSG } + + context 'when calling #transaction with only requires_new: true' do + it 'registers an offense' do + expect_offense(<<~RUBY) + ApplicationRecord.transaction(requires_new: true) do + ^^^^^^^^^^^^^^^^^^ #{message} + Project.create!(name: 'MyProject') + end + RUBY + end + end + + context 'when passing multiple arguments to #transaction, including requires_new: true' do + it 'registers an offense' do + expect_offense(<<~RUBY) + ApplicationRecord.transaction(isolation: :read_committed, requires_new: true) do + ^^^^^^^^^^^^^^^^^^ #{message} + Project.create!(name: 'MyProject') + end + RUBY + end + end + + context 'when calling #transaction with requires_new: false' do + it 'does not register an offense' do + expect_no_offenses(<<~RUBY) + ApplicationRecord.transaction(requires_new: false) do + Project.create!(name: 'MyProject') + end + RUBY + end + end + + context 'when calling #transaction with other options' do + it 'does not register an offense' do + expect_no_offenses(<<~RUBY) + ApplicationRecord.transaction(isolation: :read_committed) do + Project.create!(name: 'MyProject') + end + RUBY + end + end + + context 'when calling #transaction with no arguments' do + it 'does not register an offense' do + expect_no_offenses(<<~RUBY) + ApplicationRecord.transaction do + Project.create!(name: 'MyProject') + end + RUBY + end + end +end diff --git a/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb index 0390e60747f..2af7b616659 100644 --- a/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb @@ -21,11 +21,24 @@ RSpec.shared_examples 'handling get metadata requests' do |scope: :project| expect(response).to match_response_schema('public_api/v4/packages/npm_package') expect(json_response['name']).to eq(package.name) expect(json_response['versions'][package.version]).to match_schema('public_api/v4/packages/npm_package_version') - ::Packages::Npm::PackagePresenter::NPM_VALID_DEPENDENCY_TYPES.each do |dependency_type| + ::Packages::DependencyLink.dependency_types.keys.each do |dependency_type| expect(json_response.dig('versions', package.version, dependency_type.to_s)).to be_any end expect(json_response['dist-tags']).to match_schema('public_api/v4/packages/npm_package_tags') end + + it 'avoids N+1 database queries' do + control = ActiveRecord::QueryRecorder.new { get(url, headers: headers) } + + create_list(:npm_package, 5, project: project, name: package_name).each do |npm_package| + ::Packages::DependencyLink.dependency_types.keys.each do |dependency_type| + create(:packages_dependency_link, package: package, dependency_type: dependency_type) + end + end + + # query count can slightly change between the examples so we're using a custom threshold + expect { get(url, headers: headers) }.not_to exceed_query_limit(control).with_threshold(4) + end end shared_examples 'reject metadata request' do |status:| diff --git a/spec/views/groups/group_members/index.html.haml_spec.rb b/spec/views/groups/group_members/index.html.haml_spec.rb new file mode 100644 index 00000000000..8e190c24495 --- /dev/null +++ b/spec/views/groups/group_members/index.html.haml_spec.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'groups/group_members/index', :aggregate_failures do + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + + before do + allow(view).to receive(:group_members_app_data).and_return({}) + allow(view).to receive(:current_user).and_return(user) + assign(:group, group) + assign(:group_member, build(:group_member, group: group)) + end + + context 'when user can invite members for the group' do + before do + group.add_owner(user) + end + + context 'when modal is enabled' do + it 'renders as expected' do + render + + expect(rendered).to have_content('Group members') + expect(rendered).to have_content('You can invite a new member') + + expect(rendered).to have_selector('.js-invite-group-trigger') + expect(rendered).to have_selector('.js-invite-members-trigger') + expect(response).to render_template(partial: 'groups/_invite_members_modal') + + expect(rendered).not_to have_selector('#invite-member-tab') + expect(rendered).not_to have_selector('#invite-group-tab') + expect(response).not_to render_template(partial: 'shared/members/_invite_group') + end + end + + context 'when modal is not enabled' do + before do + stub_feature_flags(invite_members_group_modal: false) + end + + it 'renders as expected' do + render + + expect(rendered).to have_content('Group members') + expect(rendered).to have_content('You can invite a new member') + + expect(rendered).to have_selector('#invite-member-tab') + expect(rendered).to have_selector('#invite-group-tab') + expect(response).to render_template(partial: 'shared/members/_invite_group') + + expect(rendered).not_to have_selector('.js-invite-group-trigger') + expect(rendered).not_to have_selector('.js-invite-members-trigger') + expect(response).not_to render_template(partial: 'groups/_invite_members_modal') + end + end + end + + context 'when user can not invite members for the group' do + it 'renders as expected', :aggregate_failures do + render + + expect(rendered).not_to have_content('Group members') + expect(rendered).not_to have_content('You can invite a new member') + end + end +end diff --git a/spec/views/projects/project_members/index.html.haml_spec.rb b/spec/views/projects/project_members/index.html.haml_spec.rb new file mode 100644 index 00000000000..ba255a474e2 --- /dev/null +++ b/spec/views/projects/project_members/index.html.haml_spec.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'projects/project_members/index', :aggregate_failures do + let_it_be(:user) { create(:user) } + let_it_be(:source) { create(:project, :empty_repo) } + let_it_be(:project) { ProjectPresenter.new(source, current_user: user) } + + before do + allow(view).to receive(:project_members_app_data_json).and_return({}) + allow(view).to receive(:current_user).and_return(user) + assign(:project, project) + assign(:project_member, build(:project_member, project: source)) + end + + context 'when user can invite members for the project' do + before do + project.add_maintainer(user) + end + + context 'when modal is enabled' do + it 'renders as expected' do + render + + expect(rendered).to have_content('Project members') + expect(rendered).to have_content('You can invite a new member') + expect(rendered).to have_link('Import a project') + expect(rendered).to have_selector('.js-invite-group-trigger') + expect(rendered).to have_selector('.js-invite-members-trigger') + expect(rendered).not_to have_content('Members can be added by project') + expect(response).to render_template(partial: 'projects/_invite_members_modal') + end + + context 'when project is not allowed to share with group' do + before do + project.namespace.share_with_group_lock = true + end + + it 'renders as expected' do + render + + expect(rendered).not_to have_selector('.js-invite-group-trigger') + end + end + end + + context 'when modal is not enabled' do + before do + stub_feature_flags(invite_members_group_modal: false) + end + + it 'renders as expected' do + render + + expect(rendered).to have_content('Project members') + expect(rendered).to have_content('You can invite a new member') + expect(rendered).not_to have_selector('.js-invite-group-trigger') + expect(rendered).not_to have_selector('.js-invite-members-trigger') + expect(rendered).not_to have_content('Members can be added by project') + expect(response).not_to render_template(partial: 'projects/_invite_members_modal') + expect(response).to render_template(partial: 'shared/members/_invite_member') + end + + context 'when project can not be shared' do + before do + project.namespace.share_with_group_lock = true + end + + it 'renders as expected' do + render + + expect(rendered).to have_content('Project members') + expect(rendered).to have_content('You can invite a new member') + expect(response).not_to render_template(partial: 'projects/_invite_members_modal') + end + end + end + end + + context 'when user can not invite members or group for the project' do + context 'when project can be shared' do + it 'renders as expected', :aggregate_failures do + render + + expect(rendered).to have_content('Project members') + expect(rendered).not_to have_content('You can invite a new member') + expect(rendered).not_to have_link('Import a project') + expect(rendered).not_to have_selector('.js-invite-group-trigger') + expect(rendered).not_to have_selector('.js-invite-members-trigger') + expect(rendered).to have_content('Members can be added by project') + expect(response).not_to render_template(partial: 'projects/_invite_members_modal') + end + end + end +end |