diff options
48 files changed, 1044 insertions, 358 deletions
diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS index a24fef5e44d..7aa3bb6fd4a 100644 --- a/.gitlab/CODEOWNERS +++ b/.gitlab/CODEOWNERS @@ -194,12 +194,17 @@ Dangerfile @gl-quality/eng-prod # Secure & Threat Management ownership delineation # https://about.gitlab.com/handbook/engineering/development/threat-management/delineate-secure-threat-management.html#technical-boundaries -[Secure] +[Threat Insights] /ee/app/finders/security/ @gitlab-org/secure/threat-insights-backend-team /ee/app/models/security/ @gitlab-org/secure/threat-insights-backend-team /ee/app/models/vulnerabilities/ @gitlab-org/secure/threat-insights-backend-team /ee/app/models/vulnerability.rb @gitlab-org/secure/threat-insights-backend-team +/ee/app/policies/vulnerabilities/ @gitlab-org/secure/threat-insights-backend-team +/ee/app/policies/vulnerability*.rb @gitlab-org/secure/threat-insights-backend-team /ee/lib/api/vulnerabilit*.rb @gitlab-org/secure/threat-insights-backend-team +/ee/spec/policies/vulnerabilities/ @gitlab-org/secure/threat-insights-backend-team +/ee/spec/policies/vulnerabilities/vulnerability*.rb @gitlab-org/secure/threat-insights-backend-team +[Secure] /ee/lib/gitlab/ci/parsers/license_compliance/ @gitlab-org/secure/composition-analysis-be /ee/lib/gitlab/ci/parsers/security/ @gitlab-org/secure/composition-analysis-be @gitlab-org/secure/dynamic-analysis-be @gitlab-org/secure/static-analysis-be @gitlab-org/secure/fuzzing-be /ee/lib/gitlab/ci/reports/coverage_fuzzing/ @gitlab-org/secure/fuzzing-be diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue index 2df03fbc679..87c59e5ece9 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue @@ -1,20 +1,48 @@ <script> import $ from 'jquery'; import { escape } from 'lodash'; -import { GlButton, GlModalDirective } from '@gitlab/ui'; +import { GlButton, GlModalDirective, GlSkeletonLoader } from '@gitlab/ui'; import { s__, sprintf } from '~/locale'; import { mouseenter, debouncedMouseleave, togglePopover } from '~/shared/popover'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables'; import StatusIcon from '../mr_widget_status_icon.vue'; +import userPermissionsQuery from '../../queries/permissions.query.graphql'; +import conflictsStateQuery from '../../queries/states/conflicts.query.graphql'; export default { name: 'MRWidgetConflicts', components: { + GlSkeletonLoader, StatusIcon, GlButton, }, directives: { GlModalDirective, }, + mixins: [glFeatureFlagMixin(), mergeRequestQueryVariablesMixin], + apollo: { + userPermissions: { + query: userPermissionsQuery, + skip() { + return !this.glFeatures.mergeRequestWidgetGraphql; + }, + variables() { + return this.mergeRequestQueryVariables; + }, + update: data => data.project.mergeRequest.userPermissions, + }, + stateData: { + query: conflictsStateQuery, + skip() { + return !this.glFeatures.mergeRequestWidgetGraphql; + }, + variables() { + return this.mergeRequestQueryVariables; + }, + update: data => data.project.mergeRequest, + }, + }, props: { /* TODO: This is providing all store and service down when it only needs a few props */ @@ -24,21 +52,72 @@ export default { default: () => ({}), }, }, + data() { + return { + userPermissions: {}, + stateData: {}, + }; + }, computed: { + isLoading() { + return ( + this.glFeatures.mergeRequestWidgetGraphql && + this.$apollo.queries.userPermissions.loading && + this.$apollo.queries.stateData.loading + ); + }, + canPushToSourceBranch() { + if (this.glFeatures.mergeRequestWidgetGraphql) { + return this.userPermissions.pushToSourceBranch; + } + + return this.mr.canPushToSourceBranch; + }, + canMerge() { + if (this.glFeatures.mergeRequestWidgetGraphql) { + return this.userPermissions.canMerge; + } + + return this.mr.canMerge; + }, + shouldBeRebased() { + if (this.glFeatures.mergeRequestWidgetGraphql) { + return this.stateData.shouldBeRebased; + } + + return this.mr.shouldBeRebased; + }, + sourceBranchProtected() { + if (this.glFeatures.mergeRequestWidgetGraphql) { + return this.stateData.sourceBranchProtected; + } + + return this.mr.sourceBranchProtected; + }, popoverTitle() { return s__( 'mrWidget|This feature merges changes from the target branch to the source branch. You cannot use this feature since the source branch is protected.', ); }, showResolveButton() { - return this.mr.conflictResolutionPath && this.mr.canPushToSourceBranch; + return this.mr.conflictResolutionPath && this.canPushToSourceBranch; }, showPopover() { - return this.showResolveButton && this.mr.sourceBranchProtected; + return this.showResolveButton && this.sourceBranchProtected; }, }, - mounted() { - if (this.showPopover) { + watch: { + showPopover: { + handler(newVal) { + if (newVal) { + this.$nextTick(this.initPopover); + } + }, + immediate: true, + }, + }, + methods: { + initPopover() { const $el = $(this.$refs.popover); $el @@ -68,7 +147,7 @@ export default { .on('show.bs.popover', () => { window.addEventListener('scroll', togglePopover.bind($el, false), { once: true }); }); - } + }, }, }; </script> @@ -76,34 +155,41 @@ export default { <div class="mr-widget-body media"> <status-icon :show-disabled-button="true" status="warning" /> - <div class="media-body space-children"> - <span v-if="mr.shouldBeRebased" class="bold"> + <div v-if="isLoading" class="gl-ml-4 gl-w-full mr-conflict-loader"> + <gl-skeleton-loader :width="334" :height="30"> + <rect x="0" y="7" width="150" height="16" rx="4" /> + <rect x="158" y="7" width="84" height="16" rx="4" /> + <rect x="250" y="7" width="84" height="16" rx="4" /> + </gl-skeleton-loader> + </div> + <div v-else class="media-body space-children"> + <span v-if="shouldBeRebased" class="bold"> {{ s__(`mrWidget|Fast-forward merge is not possible. -To merge this request, first rebase locally.`) + To merge this request, first rebase locally.`) }} </span> <template v-else> <span class="bold"> - {{ s__('mrWidget|There are merge conflicts') }}<span v-if="!mr.canMerge">.</span> - <span v-if="!mr.canMerge"> + {{ s__('mrWidget|There are merge conflicts') }}<span v-if="!canMerge">.</span> + <span v-if="!canMerge"> {{ s__(`mrWidget|Resolve these conflicts or ask someone - with write access to this repository to merge it locally`) + with write access to this repository to merge it locally`) }} </span> </span> <span v-if="showResolveButton" ref="popover"> <gl-button - :href="mr.conflictResolutionPath" - :disabled="mr.sourceBranchProtected" + :href="!sourceBranchProtected && mr.conflictResolutionPath" + :disabled="sourceBranchProtected" class="js-resolve-conflicts-button" > {{ s__('mrWidget|Resolve conflicts') }} </gl-button> </span> <gl-button - v-if="mr.canMerge" + v-if="canMerge" v-gl-modal-directive="'modal-merge-info'" class="js-merge-locally-button" > diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/permissions.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/permissions.query.graphql new file mode 100644 index 00000000000..ae2a67440fe --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/queries/permissions.query.graphql @@ -0,0 +1,10 @@ +query userPermissionsQuery($projectPath: ID!, $iid: String!) { + project(fullPath: $projectPath) { + mergeRequest(iid: $iid) { + userPermissions { + canMerge + pushToSourceBranch + } + } + } +} diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/conflicts.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/conflicts.query.graphql new file mode 100644 index 00000000000..186c0e64561 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/conflicts.query.graphql @@ -0,0 +1,8 @@ +query workInProgressQuery($projectPath: ID!, $iid: String!) { + project(fullPath: $projectPath) { + mergeRequest(iid: $iid) { + shouldBeRebased + sourceBranchProtected + } + } +} diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index f0b1e859139..808813599c5 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -468,7 +468,6 @@ $gl-line-height-20: 20px; $gl-line-height-24: 24px; $gl-line-height-14: 14px; -$issue-box-upcoming-bg: #8f8f8f; $pages-group-name-color: #4c4e54; /* diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index aa849e1b17b..3893962cd48 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -693,10 +693,6 @@ .issuable-list { li { - .issue-box { - display: flex; - } - .issuable-info-container { flex: 1; display: flex; diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index a0ac55e4c6c..98eee2c3a02 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -1039,3 +1039,11 @@ $mr-widget-min-height: 69px; .diff-file-row.is-active { background-color: $gray-50; } + +.mr-conflict-loader { + max-width: 334px; + + > svg { + vertical-align: middle; + } +} diff --git a/app/controllers/whats_new_controller.rb b/app/controllers/whats_new_controller.rb index 384c984089a..6ed15d9b127 100644 --- a/app/controllers/whats_new_controller.rb +++ b/app/controllers/whats_new_controller.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class WhatsNewController < ApplicationController - include Gitlab::WhatsNew - skip_before_action :authenticate_user! before_action :check_feature_flag, :check_valid_page_param, :set_pagination_headers @@ -12,7 +10,7 @@ class WhatsNewController < ApplicationController def index respond_to do |format| format.js do - render json: whats_new_release_items(page: current_page) + render json: most_recent_items end end end @@ -27,18 +25,19 @@ class WhatsNewController < ApplicationController render_404 if current_page < 1 end - def set_pagination_headers - response.set_header('X-Next-Page', next_page) - end - def current_page params[:page]&.to_i || 1 end - def next_page - next_page = current_page + 1 - next_index = next_page - 1 + def most_recent + @most_recent ||= ReleaseHighlight.paginated(page: current_page) + end + + def most_recent_items + most_recent[:items].map {|item| Gitlab::WhatsNew::ItemPresenter.present(item) } + end - next_page if whats_new_file_paths[next_index] + def set_pagination_headers + response.set_header('X-Next-Page', most_recent[:next_page]) end end diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb index e68d6706c43..6d1e15742e7 100644 --- a/app/graphql/types/merge_request_type.rb +++ b/app/graphql/types/merge_request_type.rb @@ -49,6 +49,8 @@ module Types description: 'ID of the merge request target project' field :source_branch, GraphQL::STRING_TYPE, null: false, description: 'Source branch of the merge request' + field :source_branch_protected, GraphQL::BOOLEAN_TYPE, null: false, calls_gitaly: true, + description: 'Indicates if the source branch is protected' field :target_branch, GraphQL::STRING_TYPE, null: false, description: 'Target branch of the merge request' field :work_in_progress, GraphQL::BOOLEAN_TYPE, method: :work_in_progress?, null: false, @@ -194,6 +196,10 @@ module Types def commit_count object&.metrics&.commits_count end + + def source_branch_protected + object.source_project.present? && ProtectedBranch.protected?(object.source_project, object.source_branch) + end end end Types::MergeRequestType.prepend_if_ee('::EE::Types::MergeRequestType') diff --git a/app/helpers/whats_new_helper.rb b/app/helpers/whats_new_helper.rb index 283d443f51b..f267ede3153 100644 --- a/app/helpers/whats_new_helper.rb +++ b/app/helpers/whats_new_helper.rb @@ -1,25 +1,15 @@ # frozen_string_literal: true module WhatsNewHelper - include Gitlab::WhatsNew - def whats_new_most_recent_release_items_count - Gitlab::ProcessMemoryCache.cache_backend.fetch('whats_new:release_items_count', expires_in: CACHE_DURATION) do - whats_new_release_items&.count - end + ReleaseHighlight.most_recent_item_count end def whats_new_storage_key - return unless whats_new_most_recent_version - - ['display-whats-new-notification', whats_new_most_recent_version].join('-') - end + most_recent_version = ReleaseHighlight.most_recent_version - private + return unless most_recent_version - def whats_new_most_recent_version - Gitlab::ProcessMemoryCache.cache_backend.fetch('whats_new:release_version', expires_in: CACHE_DURATION) do - whats_new_release_items&.first&.[]('release') - end + ['display-whats-new-notification', most_recent_version].join('-') end end diff --git a/app/models/release_highlight.rb b/app/models/release_highlight.rb new file mode 100644 index 00000000000..436314de3a3 --- /dev/null +++ b/app/models/release_highlight.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +class ReleaseHighlight + CACHE_DURATION = 1.hour + FILES_PATH = Rails.root.join('data', 'whats_new', '*.yml') + + def self.paginated(page: 1) + Rails.cache.fetch(cache_key(page), expires_in: CACHE_DURATION) do + items = self.load_items(page: page) + + next if items.nil? + + { + items: items, + next_page: next_page(current_page: page) + } + end + end + + def self.load_items(page:) + index = page - 1 + file_path = file_paths[index] + + return if file_path.nil? + + file = File.read(file_path) + + items = YAML.safe_load(file, permitted_classes: [Date]) + + platform = Gitlab.com? ? 'gitlab-com' : 'self-managed' + items&.select {|item| item[platform] } + rescue Psych::Exception => e + Gitlab::ErrorTracking.track_exception(e, file_path: file_path) + + nil + end + + def self.file_paths + @file_paths ||= Rails.cache.fetch('release_highlight:file_paths', expires_in: CACHE_DURATION) do + Dir.glob(FILES_PATH).sort.reverse + end + end + + def self.cache_key(page) + filename = /\d*\_\d*\_\d*/.match(self.file_paths&.first) + "release_highlight:items:file-#{filename}:page-#{page}" + end + + def self.next_page(current_page: 1) + next_page = current_page + 1 + next_index = next_page - 1 + + next_page if self.file_paths[next_index] + end + + def self.most_recent_version + Gitlab::ProcessMemoryCache.cache_backend.fetch('release_highlight:release_version', expires_in: CACHE_DURATION) do + self.paginated&.[](:items)&.first&.[]('release') + end + end + + def self.most_recent_item_count + Gitlab::ProcessMemoryCache.cache_backend.fetch('release_highlight:recent_item_count', expires_in: CACHE_DURATION) do + self.paginated&.[](:items)&.count + end + end +end diff --git a/app/presenters/gitlab/whats_new/item_presenter.rb b/app/presenters/gitlab/whats_new/item_presenter.rb new file mode 100644 index 00000000000..26a5b9aab02 --- /dev/null +++ b/app/presenters/gitlab/whats_new/item_presenter.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Gitlab + module WhatsNew + class ItemPresenter + DICTIONARY = { + free: 'Free', + starter: 'Bronze', + premium: 'Silver', + ultimate: 'Gold' + }.freeze + + def self.present(item) + if Gitlab.com? + item['packages'] = item['packages'].map { |p| DICTIONARY[p.downcase.to_sym] } + end + + item + end + end + end +end diff --git a/changelogs/unreleased/284602-remove-issue-box-css.yml b/changelogs/unreleased/284602-remove-issue-box-css.yml new file mode 100644 index 00000000000..8bfa9283724 --- /dev/null +++ b/changelogs/unreleased/284602-remove-issue-box-css.yml @@ -0,0 +1,5 @@ +--- +title: Remove unused .issue-box CSS +merge_request: 48002 +author: Takuya Noguchi +type: other diff --git a/changelogs/unreleased/feat-add-packages_size-to-project-statistics.yml b/changelogs/unreleased/feat-add-packages_size-to-project-statistics.yml new file mode 100644 index 00000000000..d488b455651 --- /dev/null +++ b/changelogs/unreleased/feat-add-packages_size-to-project-statistics.yml @@ -0,0 +1,5 @@ +--- +title: Add packages_size to ProjectStatistics API entity +merge_request: 47156 +author: Roger Meier +type: added diff --git a/db/migrate/20201117153333_add_index_on_package_size_and_project_id_to_project_statistics.rb b/db/migrate/20201117153333_add_index_on_package_size_and_project_id_to_project_statistics.rb new file mode 100644 index 00000000000..efb5cf14d3c --- /dev/null +++ b/db/migrate/20201117153333_add_index_on_package_size_and_project_id_to_project_statistics.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddIndexOnPackageSizeAndProjectIdToProjectStatistics < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + INDEX_NAME = 'index_project_statistics_on_packages_size_and_project_id' + + disable_ddl_transaction! + + def up + add_concurrent_index :project_statistics, [:packages_size, :project_id], + name: INDEX_NAME + end + + def down + remove_concurrent_index_by_name :project_statistics, INDEX_NAME + end +end diff --git a/db/schema_migrations/20201117153333 b/db/schema_migrations/20201117153333 new file mode 100644 index 00000000000..ee496f91777 --- /dev/null +++ b/db/schema_migrations/20201117153333 @@ -0,0 +1 @@ +008f3a69d23abbd513336c5a48b2448e470a9413920beeb6a1684d0c6840d6a4
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 5cb98ddbd19..a579f2aacd7 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -21779,6 +21779,8 @@ CREATE UNIQUE INDEX index_project_settings_on_push_rule_id ON project_settings U CREATE INDEX index_project_statistics_on_namespace_id ON project_statistics USING btree (namespace_id); +CREATE INDEX index_project_statistics_on_packages_size_and_project_id ON project_statistics USING btree (packages_size, project_id); + CREATE UNIQUE INDEX index_project_statistics_on_project_id ON project_statistics USING btree (project_id); CREATE INDEX index_project_statistics_on_repository_size_and_project_id ON project_statistics USING btree (repository_size, project_id); diff --git a/doc/administration/audit_events.md b/doc/administration/audit_events.md index fa7fa3666b3..44fa9a93420 100644 --- a/doc/administration/audit_events.md +++ b/doc/administration/audit_events.md @@ -133,12 +133,6 @@ recorded: - A user's personal access token was successfully created or revoked ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/276921) in GitLab 13.6) - A failed attempt to create or revoke a user's personal access token ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/276921) in GitLab 13.6) -It's possible to filter particular actions by choosing an audit data type from -the filter dropdown box. You can further filter by specific group, project, or user -(for authentication events). - -![audit log](img/audit_log.png) - Instance events can also be accessed via the [Instance Audit Events API](../api/audit_events.md#instance-audit-events). ### Missing events @@ -180,6 +174,19 @@ the steps bellow. Feature.enable(:repository_push_audit_event) ``` +## Search + +The search filters you can see depends on which audit level you are at. + +| Filter | Available options | +| ------ | ----------------- | +| Scope (Project level) | A specific user who performed the action. | +| Scope (Group level) | A specific user (in a group) who performed the action. | +| Scope (Instance level) | A specific group, project, or user that the action was scoped to. | +| Date range | Either via the date range buttons or pickers (maximum range of 31 days). Default is from the first day of the month to today's date. | + +![audit log](img/audit_log_v13_6.png) + ## Export to CSV **(PREMIUM ONLY)** > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/1449) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.4. @@ -193,23 +200,18 @@ This feature might not be available to you. Check the **version history** note a If available, you can enable it with a [feature flag](#enable-or-disable-audit-log-export-to-csv). Export to CSV allows customers to export the current filter view of your audit log as a -CSV file, -which stores tabular data in plain text. The data provides a comprehensive view with respect to +CSV file, which stores tabular data in plain text. The data provides a comprehensive view with respect to audit events. To export the Audit Log to CSV, navigate to **{monitor}** **Admin Area > Monitoring > Audit Log** -1. Click in the field **Search**. -1. In the dropdown menu that appears, select the event type that you want to filter by. -1. Select the preferred date range. +1. Select the available search [filters](#search). 1. Click **Export as CSV**. -![Export Audit Log](img/export_audit_log_v13_4.png) - ### Sort -Exported events are always sorted by `ID` in ascending order. +Exported events are always sorted by `created_at` in ascending order. ### Format @@ -222,8 +224,8 @@ The first row contains the headers, which are listed in the following table alon | Author ID | ID of the author | | Author Name | Full name of the author | | Entity ID | ID of the scope | -| Entity Type | Type of the entity (`Project`/`Group`/`User`) | -| Entity Path | Path of the entity | +| Entity Type | Type of the scope (`Project`/`Group`/`User`) | +| Entity Path | Path of the scope | | Target ID | ID of the target | | Target Type | Type of the target | | Target Details | Details of the target | @@ -233,7 +235,7 @@ The first row contains the headers, which are listed in the following table alon ### Limitation -The Audit Log CSV file size is limited to a maximum of `100,000` events. +The Audit Log CSV file is limited to a maximum of `100,000` events. The remaining records are truncated when this limit is reached. ### Enable or disable Audit Log Export to CSV diff --git a/doc/administration/img/audit_log.png b/doc/administration/img/audit_log.png Binary files differdeleted file mode 100644 index d4f4c2abf38..00000000000 --- a/doc/administration/img/audit_log.png +++ /dev/null diff --git a/doc/administration/img/audit_log_v13_6.png b/doc/administration/img/audit_log_v13_6.png Binary files differnew file mode 100644 index 00000000000..3268f864e81 --- /dev/null +++ b/doc/administration/img/audit_log_v13_6.png diff --git a/doc/administration/img/export_audit_log_v13_4.png b/doc/administration/img/export_audit_log_v13_4.png Binary files differdeleted file mode 100644 index e4ba330b8a9..00000000000 --- a/doc/administration/img/export_audit_log_v13_4.png +++ /dev/null diff --git a/doc/administration/sidekiq.md b/doc/administration/sidekiq.md index 2cc6acc8c87..c56b3e77dd7 100644 --- a/doc/administration/sidekiq.md +++ b/doc/administration/sidekiq.md @@ -25,8 +25,9 @@ you want using steps 1 and 2 from the GitLab downloads page. ## Optional: Enable extra Sidekiq processes sidekiq_cluster['enable'] = true - sidekiq_cluster['enable'] = true - "elastic_indexer" + sidekiq['queue_groups'] = [ + "elastic_indexer", + "*" ] ``` diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index d6c3967e4d5..63270d5cc4a 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -12781,6 +12781,11 @@ type MergeRequest implements CurrentUserTodos & Noteable { sourceBranchExists: Boolean! """ + Indicates if the source branch is protected + """ + sourceBranchProtected: Boolean! + + """ Source project of the merge request """ sourceProject: Project diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index a0f342764f3..2f4181df554 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -35043,6 +35043,24 @@ "deprecationReason": null }, { + "name": "sourceBranchProtected", + "description": "Indicates if the source branch is protected", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "sourceProject", "description": "Source project of the merge request", "args": [ diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index aa59a638c22..90c1a5a0ffd 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -1950,6 +1950,7 @@ Autogenerated return type of MarkAsSpamSnippet. | `shouldRemoveSourceBranch` | Boolean | Indicates if the source branch of the merge request will be deleted after merge | | `sourceBranch` | String! | Source branch of the merge request | | `sourceBranchExists` | Boolean! | Indicates if the source branch of the merge request exists | +| `sourceBranchProtected` | Boolean! | Indicates if the source branch is protected | | `sourceProject` | Project | Source project of the merge request | | `sourceProjectId` | Int | ID of the merge request source project | | `state` | MergeRequestState! | State of the merge request | diff --git a/doc/api/projects.md b/doc/api/projects.md index 03440f0c143..419ae5ce423 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -49,7 +49,7 @@ GET /projects | `last_activity_before` | datetime | **{dotted-circle}** No | Limit results to projects with last_activity before specified time. Format: ISO 8601 YYYY-MM-DDTHH:MM:SSZ | | `membership` | boolean | **{dotted-circle}** No | Limit by projects that the current user is a member of. | | `min_access_level` | integer | **{dotted-circle}** No | Limit by current user minimal [access level](members.md#valid-access-levels). | -| `order_by` | string | **{dotted-circle}** No | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. `repository_size`, `storage_size`, or `wiki_size` fields are only allowed for admins. Default is `created_at`. | +| `order_by` | string | **{dotted-circle}** No | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. `repository_size`, `storage_size`, `packages_size` or `wiki_size` fields are only allowed for admins. Default is `created_at`. | | `owned` | boolean | **{dotted-circle}** No | Limit by projects explicitly owned by the current user. | | `repository_checksum_failed` **(PREMIUM)** | boolean | **{dotted-circle}** No | Limit projects where the repository checksum calculation has failed ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/6137) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.2). | | `repository_storage` | string | **{dotted-circle}** No | Limit results to projects stored on `repository_storage`. _(admins only)_ | diff --git a/doc/api/runners.md b/doc/api/runners.md index 16ecdebcd4f..1b47f2ea98d 100644 --- a/doc/api/runners.md +++ b/doc/api/runners.md @@ -224,7 +224,7 @@ PUT /runners/:id | `run_untagged`| boolean | no | Flag indicating the runner can execute untagged jobs | | `locked` | boolean | no | Flag indicating the runner is locked | | `access_level` | string | no | The access_level of the runner; `not_protected` or `ref_protected` | -| `maximum_timeout` | integer | no | Maximum timeout set when this runner will handle the job | +| `maximum_timeout` | integer | no | Maximum timeout set when this runner handles the job | ```shell curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/runners/6" --form "description=test-1-20150125-test" --form "tag_list=ruby,mysql,tag1,tag2" @@ -559,7 +559,7 @@ POST /runners | `run_untagged` | boolean | no | Whether the runner should handle untagged jobs | | `tag_list` | string array | no | List of runner's tags | | `access_level` | string | no | The access_level of the runner; `not_protected` or `ref_protected` | -| `maximum_timeout` | integer | no | Maximum timeout set when this runner will handle the job | +| `maximum_timeout` | integer | no | Maximum timeout set when this runner handles the job | ```shell curl --request POST "https://gitlab.example.com/api/v4/runners" --form "token=<registration_token>" --form "description=test-1-20150125-test" --form "tag_list=ruby,mysql,tag1,tag2" diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md index c8eeb5c222a..cf558de0925 100644 --- a/doc/ci/docker/using_docker_build.md +++ b/doc/ci/docker/using_docker_build.md @@ -589,6 +589,147 @@ The configuration is picked up by the `dind` service. sub_path = "daemon.json" ``` +## Authenticating with registry in Docker-in-Docker + +When you use Docker-in-Docker, the [normal authentication +methods](using_docker_images.html#define-an-image-from-a-private-container-registry) +won't work because a fresh Docker daemon is started with the service. + +### Option 1: Run `docker login` + +In [`before_script`](../yaml/README.md#before_script) run `docker +login`: + +```yaml +image: docker:19.03.13 + +variables: + DOCKER_TLS_CERTDIR: "/certs" + +services: + - docker:19.03.13-dind + +build: + stage: build + before_script: + - echo "$DOCKER_REGISTRY_PASS" | docker login $DOCKER_REGISTRY --username $DOCKER_REGISTRY_USER --password-stdin + script: + - docker build -t my-docker-image . + - docker run my-docker-image /script/to/run/tests +``` + +To log in to Docker Hub, leave `$DOCKER_REGISTRY` +empty or remove it. + +### Option 2: Mount `~/.docker/config.json` on each job + +If you are an administrator for GitLab Runner, you can mount a file +with the authentication configuration to `~/.docker/config.json`. +Then every job that the runner picks up will be authenticated already. If you +are using the official `docker:19.03.13` image, the home directory is +under `/root`. + +If you mount the config file, any `docker` command +that modifies the `~/.docker/config.json` (for example, `docker login`) +fails, because the file is mounted as read-only. Do not change it from +read-only, because other problems will occur. + +Here is an example of `/opt/.docker/config.json` that follows the +[`DOCKER_AUTH_CONFIG`](using_docker_images.md#determining-your-docker_auth_config-data) +documentation: + +```json +{ + "auths": { + "https://index.docker.io/v1/": { + "auth": "bXlfdXNlcm5hbWU6bXlfcGFzc3dvcmQ=" + } + } +} +``` + +#### Docker + +Update the [volume +mounts](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#volumes-in-the-runnersdocker-section) +to include the file. + +```toml +[[runners]] + ... + executor = "docker" + [runners.docker] + ... + privileged = true + volumes = ["/opt/.docker/config.json:/root/.docker/config.json:ro"] +``` + +#### Kubernetes + +Create a [ConfigMap](https://kubernetes.io/docs/concepts/configuration/configmap/) with the content +of this file. You can do this with a command like: + +```shell +kubectl create configmap docker-client-config --namespace gitlab-runner --from-file /opt/.docker/config.json +``` + +Update the [volume +mounts](https://docs.gitlab.com/runner/executors/kubernetes.html#using-volumes) +to include the file. + +```toml +[[runners]] + ... + executor = "kubernetes" + [runners.kubernetes] + image = "alpine:3.12" + privileged = true + [[runners.kubernetes.volumes.config_map]] + name = "docker-client-config" + mount_path = "/root/.docker/config.json" + # If you are running GitLab Runner 13.5 + # or lower you can remove this + sub_path = "config.json" +``` + +### Option 3: Use `DOCKER_ATUH_CONFIG` + +If you already have +[`DOCKER_AUTH_CONFIG`](using_docker_images.md#determining-your-docker_auth_config-data) +defined, you can use the variable and save it in +`~/.docker/config.json`. + +There are multiple ways to define this. For example: + +- Inside + [`pre_build_script`](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runners-section) + inside of the runner config file. +- Inside [`before_script`](../yaml/README.md#before_script). +- Inside of [`script`](../yaml/README.md#script). + +Below is an example of +[`before_script`](../yaml/README.md#before_script). The same commands +apply for any solution you implement. + +```yaml +image: docker:19.03.13 + +variables: + DOCKER_TLS_CERTDIR: "/certs" + +services: + - docker:19.03.13-dind + +build: + stage: build + before_script: + - mkdir -p $HOME/.docker + - echo $DOCKER_AUTH_CONFIG > $HOME/.docker/config.json + script: + - docker build -t my-docker-image . + - docker run my-docker-image /script/to/run/tests +``` + ## Making Docker-in-Docker builds faster with Docker layer caching When using Docker-in-Docker, Docker downloads all layers of your image every diff --git a/doc/development/bulk_import.md b/doc/development/bulk_import.md new file mode 100644 index 00000000000..eb9106daa04 --- /dev/null +++ b/doc/development/bulk_import.md @@ -0,0 +1,53 @@ +--- +stage: Manage +group: Import +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/#designated-technical-writers +--- + +# GitLab Group Migration + +[Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2771) in GitLab 13.7. + +CAUTION: **Caution:** +This feature is [under construction](https://gitlab.com/groups/gitlab-org/-/epics/2771) and its API/Architecture might change in the future. + +GitLab Group Migration is the evolution of Project and Group Import functionality. The +goal is to have an easier way to the user migrate a whole Group, including +Projects, from one GitLab instance to another. + +## Design decisions + +### Overview + +The following architectural diagram illustrates how the Group Migration +works with a set of [ETL](#etl) Pipelines leveraging from the current [GitLab APIs](#api). + +![Simplified Component Overview](img/bulk_imports_overview_v13_7.png) + +### [ETL](https://www.ibm.com/cloud/learn/etl) + +<!-- Direct quote from the IBM URL link --> + +> ETL, for extract, transform and load, is a data integration process that +> combines data from multiple data sources into a single, consistent data store +> that is loaded into a data warehouse or other target system. + +Using ETL architecture makes the code more explicit and easier to follow, test and extend. The +idea is to have one ETL pipeline for each relation to be imported. + +### API + +The current [Project](../user/project/settings/import_export.md) and [Group](../user/group/settings/import_export.md) Import are file based, so they require an export +step to generate the file to be imported. + +GitLab Group migration leverages on [GitLab API](../api/README.md) to speed the migration. + +And, because we're on the road to [GraphQL](../api/README.md#road-to-graphql), +GitLab Group Migration will be contributing towards to expand the GraphQL API coverage, which benefits both GitLab +and its users. + +### Namespace + +The migration process starts with the creation of a [`BulkImport`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/models/bulk_import.rb) +record to keep track of the migration. From there all the code related to the +GitLab Group Migration can be found under the new `BulkImports` namespace in all the application layers. diff --git a/doc/development/feature_flags/process.md b/doc/development/feature_flags/process.md index b282424f59a..72dad4f9b6a 100644 --- a/doc/development/feature_flags/process.md +++ b/doc/development/feature_flags/process.md @@ -32,7 +32,8 @@ should be leveraged: requests, you can use the following workflow: 1. [Create a new feature flag](development.md#create-a-new-feature-flag) - which is **off** by default, in the first merge request. + which is **off** by default, in the first merge request which uses the flag. + Flags [should not be added separately](development.md#risk-of-a-broken-master-main-branch). 1. Submit incremental changes via one or more merge requests, ensuring that any new code added can only be reached if the feature flag is **on**. You can keep the feature flag enabled on your local GDK during development. diff --git a/doc/development/img/bulk_imports_overview_v13_7.png b/doc/development/img/bulk_imports_overview_v13_7.png Binary files differnew file mode 100644 index 00000000000..405ab7b1815 --- /dev/null +++ b/doc/development/img/bulk_imports_overview_v13_7.png diff --git a/doc/user/clusters/agent/index.md b/doc/user/clusters/agent/index.md index 74c679d9bb9..321de69fabe 100644 --- a/doc/user/clusters/agent/index.md +++ b/doc/user/clusters/agent/index.md @@ -161,23 +161,9 @@ gitops: ``` GitLab [versions 13.7 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/259669) also -supports manifest projects containing multiple directories (or subdirectories) -of YAML files. To use multiple YAML files, specify a `paths` attribute: - -```yaml -gitops: - manifest_projects: - - id: "path-to/your-manifest-project-number1" - paths: - # Read all .yaml files from team1/app1 directory. - # See https://github.com/bmatcuk/doublestar#about and - # https://pkg.go.dev/github.com/bmatcuk/doublestar/v2#Match for globbing rules. - - glob: '/team1/app1/*.yaml' - # Read all .yaml files from team2/apps and all subdirectories - - glob: '/team2/apps/**/*.yaml' - # If 'paths' is not specified or is an empty list, the configuration below is used - - glob: '/**/*.{yaml,yml,json}' -``` +supports manifest projects containing +multiple directories (or subdirectories) of YAML files. For more information see our +documentation on the [Kubernetes Agent configuration respository](repository.md). ### Create an Agent record in GitLab diff --git a/doc/user/clusters/agent/repository.md b/doc/user/clusters/agent/repository.md new file mode 100644 index 00000000000..d160e6556e3 --- /dev/null +++ b/doc/user/clusters/agent/repository.md @@ -0,0 +1,93 @@ +--- +stage: Configure +group: Configure +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/#designated-technical-writers +--- + +# Kubernetes Agent configuration repository **(PREMIUM ONLY)** + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/259669) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.7. +> - It's disabled on GitLab.com. Rolling this feature out to GitLab.com is [planned](https://gitlab.com/groups/gitlab-org/-/epics/3834). + +CAUTION: **Warning:** +This feature might not be available to you. Check the **version history** note above for details. + +The [GitLab Kubernetes Agent integration](index.md) supports hosting your configuration for +multiple GitLab Kubernetes Agents in a single repository. These agents can be running +in the same cluster or in multiple clusters, and potentially with more than one Agent per cluster. + +The Agent bootstraps with the GitLab installation URL and an authentication token, +and you provide the rest of the configuration in your repository, following +Infrastructure as Code (IaaC) best practices. + +A minimal repository layout looks like this, with `my_agent_1` as the name +of your Agent: + +```plaintext +|- .gitlab + |- agents + |- my_agent_1 + |- config.yaml +``` + +## Synchronize manifest projects + +Your `config.yaml` file contains a `gitops` section, which contains a `manifest_projects` +section. Each `id` in the `manifest_projects` section is the path to a Git repository +with Kubernetes resource definitions in YAML or JSON format. The Agent monitors +each project you declare, and when the project changes, GitLab deploys the changes +using the Agent. + +To use multiple YAML files, specify a `paths` attribute in the `gitops` section. + +By default, the Agent monitors all types of resources. You can exclude some types of resources +from monitoring. This enables you to reduce the permissions needed by the GitOps feature, +through `resource_exclusions`. + +To enable a specific named resource, first use `resource_inclusions` to enable desired resources. +The following file excerpt includes specific `api_groups` and `kinds`. The `resource_exclusions` +which follow excludes all other `api_groups` and `kinds`: + +```yaml +gitops: + # Manifest projects are watched by the agent. Whenever a project changes, + # GitLab deploys the changes using the agent. + manifest_projects: + # No authentication mechanisms are currently supported. + # The `id` is a path to a Git repository with Kubernetes resource definitions + # in YAML or JSON format. + - id: gitlab-org/cluster-integration/gitlab-agent + # Holds the only API groups and kinds of resources that gitops will monitor. + # Inclusion rules are evaluated first, then exclusion rules. + # If there is still no match, resource is monitored. + resource_inclusions: + - api_groups: + - apps + kinds: + - '*' + - api_groups: + - '' + kinds: + - 'ConfigMap' + # Holds the API groups and kinds of resources to exclude from gitops watch. + # Inclusion rules are evaluated first, then exclusion rules. + # If there is still no match, resource is monitored. + resource_exclusions: + - api_groups: + - '*' + kinds: + - '*' + # Namespace to use if not set explicitly in object manifest. + default_namespace: my-ns + # Paths inside of the repository to scan for manifest files. + # Directories with names starting with a dot are ignored. + paths: + # Read all .yaml files from team1/app1 directory. + # See https://github.com/bmatcuk/doublestar#about and + # https://pkg.go.dev/github.com/bmatcuk/doublestar/v2#Match for globbing rules. + - glob: '/team1/app1/*.yaml' + # Read all .yaml files from team2/apps and all subdirectories + - glob: '/team2/apps/**/*.yaml' + # If 'paths' is not specified or is an empty list, the configuration below is used + - glob: '/**/*.{yaml,yml,json}' +``` diff --git a/doc/user/packages/maven_repository/index.md b/doc/user/packages/maven_repository/index.md index 5d0d64b310d..fcfc8724205 100644 --- a/doc/user/packages/maven_repository/index.md +++ b/doc/user/packages/maven_repository/index.md @@ -707,23 +707,23 @@ You can create a new package each time the `master` branch is updated. 1. Make sure your `pom.xml` file includes the following. You can either let Maven use the CI environment variables, as shown in this example, - or you can hard code your project's ID. + or you can hard code your server's hostname and project's ID. ```xml <repositories> <repository> <id>gitlab-maven</id> - <url>https://gitlab.example.com/api/v4/projects/${env.CI_PROJECT_ID}/packages/maven</url> + <url>${env.CI_SERVER_URL}/api/v4/projects/${env.CI_PROJECT_ID}/packages/maven</url> </repository> </repositories> <distributionManagement> <repository> <id>gitlab-maven</id> - <url>https://gitlab.example.com/api/v4/projects/${env.CI_PROJECT_ID}/packages/maven</url> + <url>${env.CI_SERVER_URL}/api/v4/projects/${env.CI_PROJECT_ID}/packages/maven</url> </repository> <snapshotRepository> <id>gitlab-maven</id> - <url>https://gitlab.example.com/api/v4/projects/${env.CI_PROJECT_ID}/packages/maven</url> + <url>${env.CI_SERVER_URL}/api/v4/projects/${env.CI_PROJECT_ID}/packages/maven</url> </snapshotRepository> </distributionManagement> ``` diff --git a/lib/api/entities/project_statistics.rb b/lib/api/entities/project_statistics.rb index 32201e88eaf..70980e670b0 100644 --- a/lib/api/entities/project_statistics.rb +++ b/lib/api/entities/project_statistics.rb @@ -10,6 +10,7 @@ module API expose :lfs_objects_size expose :build_artifacts_size, as: :job_artifacts_size expose :snippets_size + expose :packages_size end end end diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb index 0364ba2ad9e..2fdbbce8606 100644 --- a/lib/api/helpers/projects_helpers.rb +++ b/lib/api/helpers/projects_helpers.rb @@ -6,7 +6,7 @@ module API extend ActiveSupport::Concern extend Grape::API::Helpers - STATISTICS_SORT_PARAMS = %w[storage_size repository_size wiki_size].freeze + STATISTICS_SORT_PARAMS = %w[storage_size repository_size wiki_size packages_size].freeze params :optional_project_params_ce do optional :description, type: String, desc: 'The description of the project' diff --git a/lib/gitlab/whats_new.rb b/lib/gitlab/whats_new.rb deleted file mode 100644 index 69ccb48c544..00000000000 --- a/lib/gitlab/whats_new.rb +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module WhatsNew - CACHE_DURATION = 1.hour - WHATS_NEW_FILES_PATH = Rails.root.join('data', 'whats_new', '*.yml') - - private - - def whats_new_release_items(page: 1) - Rails.cache.fetch(whats_new_items_cache_key(page), expires_in: CACHE_DURATION) do - index = page - 1 - file_path = whats_new_file_paths[index] - - next if file_path.nil? - - file = File.read(file_path) - - items = YAML.safe_load(file, permitted_classes: [Date]) - - items if items.is_a?(Array) - end - rescue => e - Gitlab::ErrorTracking.track_exception(e, page: page) - - nil - end - - def whats_new_file_paths - @whats_new_file_paths ||= Rails.cache.fetch('whats_new:file_paths', expires_in: CACHE_DURATION) do - Dir.glob(WHATS_NEW_FILES_PATH).sort.reverse - end - end - - def whats_new_items_cache_key(page) - filename = /\d*\_\d*\_\d*/.match(whats_new_file_paths&.first) - "whats_new:release_items:file-#{filename}:page-#{page}" - end - end -end diff --git a/spec/features/markdown/mermaid_spec.rb b/spec/features/markdown/mermaid_spec.rb index bdb549326fa..9875fda17a9 100644 --- a/spec/features/markdown/mermaid_spec.rb +++ b/spec/features/markdown/mermaid_spec.rb @@ -44,7 +44,7 @@ RSpec.describe 'Mermaid rendering', :js do expect(page.html.scan(expected).count).to be(4) end - it 'renders only 2 Mermaid blocks and ', :js, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/234081' } do + it 'renders only 2 Mermaid blocks and ', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/234081' do description = <<~MERMAID ```mermaid graph LR @@ -71,7 +71,7 @@ RSpec.describe 'Mermaid rendering', :js do end end - it 'correctly sizes mermaid diagram inside <details> block', :js do + it 'correctly sizes mermaid diagram inside <details> block' do description = <<~MERMAID <details> <summary>Click to show diagram</summary> @@ -102,7 +102,7 @@ RSpec.describe 'Mermaid rendering', :js do end end - it 'correctly sizes mermaid diagram block', :js do + it 'correctly sizes mermaid diagram block' do description = <<~MERMAID ```mermaid graph TD; @@ -121,7 +121,7 @@ RSpec.describe 'Mermaid rendering', :js do expect(page).to have_css('svg.mermaid[style*="max-width"][width="100%"]') end - it 'display button when diagram exceeds length', :js do + it 'display button when diagram exceeds length', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/287806' do graph_edges = "A-->B;B-->A;" * 420 description = <<~MERMAID diff --git a/spec/fixtures/whats_new/20201225_01_01.yml b/spec/fixtures/whats_new/20201225_01_01.yml index 06db95be44f..48248757b9a 100644 --- a/spec/fixtures/whats_new/20201225_01_01.yml +++ b/spec/fixtures/whats_new/20201225_01_01.yml @@ -1,2 +1,5 @@ --- - title: It's gonna be a bright + self-managed: true + gitlab-com: false + packages: ["Premium", "Ultimate"] diff --git a/spec/fixtures/whats_new/20201225_01_02.yml b/spec/fixtures/whats_new/20201225_01_02.yml index 91b0bd7036e..f0fbc036698 100644 --- a/spec/fixtures/whats_new/20201225_01_02.yml +++ b/spec/fixtures/whats_new/20201225_01_02.yml @@ -1,2 +1,5 @@ --- - title: bright + self-managed: true + gitlab-com: false + packages: ["Premium", "Ultimate"] diff --git a/spec/fixtures/whats_new/20201225_01_05.yml b/spec/fixtures/whats_new/20201225_01_05.yml index 7c95e386f00..152609296c9 100644 --- a/spec/fixtures/whats_new/20201225_01_05.yml +++ b/spec/fixtures/whats_new/20201225_01_05.yml @@ -1,3 +1,10 @@ --- - title: bright and sunshinin' day + self-managed: true + gitlab-com: false + packages: ["Premium", "Ultimate"] release: '01.05' +- title: I think I can make it now the pain is gone + self-managed: false + gitlab-com: true + packages: ["Premium", "Ultimate"] diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js index b8cd1469179..ad21e6e6f4f 100644 --- a/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js +++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js @@ -6,6 +6,7 @@ import ConflictsComponent from '~/vue_merge_request_widget/components/states/mr_ describe('MRWidgetConflicts', () => { let vm; + let mergeRequestWidgetGraphql = null; const path = '/conflicts'; function createComponent(propsData = {}) { @@ -13,7 +14,35 @@ describe('MRWidgetConflicts', () => { vm = shallowMount(localVue.extend(ConflictsComponent), { propsData, + provide: { + glFeatures: { + mergeRequestWidgetGraphql, + }, + }, + mocks: { + $apollo: { + queries: { + userPermissions: { loading: false }, + stateData: { loading: false }, + }, + }, + }, }); + + if (mergeRequestWidgetGraphql) { + vm.setData({ + userPermissions: { + canMerge: propsData.mr.canMerge, + pushToSourceBranch: propsData.mr.canPushToSourceBranch, + }, + stateData: { + shouldBeRebased: propsData.mr.shouldBeRebased, + sourceBranchProtected: propsData.mr.sourceBranchProtected, + }, + }); + } + + return vm.vm.$nextTick(); } beforeEach(() => { @@ -21,206 +50,215 @@ describe('MRWidgetConflicts', () => { }); afterEach(() => { + mergeRequestWidgetGraphql = null; vm.destroy(); }); - // There are two permissions we need to consider: - // - // 1. Is the user allowed to merge to the target branch? - // 2. Is the user allowed to push to the source branch? - // - // This yields 4 possible permutations that we need to test, and - // we test them below. A user who can push to the source - // branch should be allowed to resolve conflicts. This is - // consistent with what the backend does. - describe('when allowed to merge but not allowed to push to source branch', () => { - beforeEach(() => { - createComponent({ - mr: { - canMerge: true, - canPushToSourceBranch: false, - conflictResolutionPath: path, - conflictsDocsPath: '', - }, + [false, true].forEach(featureEnabled => { + describe(`with GraphQL feature flag ${featureEnabled ? 'enabled' : 'disabled'}`, () => { + beforeEach(() => { + mergeRequestWidgetGraphql = featureEnabled; }); - }); - - it('should tell you about conflicts without bothering other people', () => { - expect(vm.text()).toContain('There are merge conflicts'); - expect(vm.text()).not.toContain('ask someone with write access'); - }); - - it('should not allow you to resolve the conflicts', () => { - expect(vm.text()).not.toContain('Resolve conflicts'); - }); - - it('should have merge buttons', () => { - const mergeLocallyButton = vm.find('.js-merge-locally-button'); - - expect(mergeLocallyButton.text()).toContain('Merge locally'); - }); - }); - describe('when not allowed to merge but allowed to push to source branch', () => { - beforeEach(() => { - createComponent({ - mr: { - canMerge: false, - canPushToSourceBranch: true, - conflictResolutionPath: path, - conflictsDocsPath: '', - }, - }); - }); - - it('should tell you about conflicts', () => { - expect(vm.text()).toContain('There are merge conflicts'); - expect(vm.text()).toContain('ask someone with write access'); - }); - - it('should allow you to resolve the conflicts', () => { - const resolveButton = vm.find('.js-resolve-conflicts-button'); - - expect(resolveButton.text()).toContain('Resolve conflicts'); - expect(resolveButton.attributes('href')).toEqual(path); - }); - - it('should not have merge buttons', () => { - expect(vm.text()).not.toContain('Merge locally'); - }); - }); - - describe('when allowed to merge and push to source branch', () => { - beforeEach(() => { - createComponent({ - mr: { - canMerge: true, - canPushToSourceBranch: true, - conflictResolutionPath: path, - conflictsDocsPath: '', - }, + // There are two permissions we need to consider: + // + // 1. Is the user allowed to merge to the target branch? + // 2. Is the user allowed to push to the source branch? + // + // This yields 4 possible permutations that we need to test, and + // we test them below. A user who can push to the source + // branch should be allowed to resolve conflicts. This is + // consistent with what the backend does. + describe('when allowed to merge but not allowed to push to source branch', () => { + beforeEach(async () => { + await createComponent({ + mr: { + canMerge: true, + canPushToSourceBranch: false, + conflictResolutionPath: path, + conflictsDocsPath: '', + }, + }); + }); + + it('should tell you about conflicts without bothering other people', () => { + expect(vm.text()).toContain('There are merge conflicts'); + expect(vm.text()).not.toContain('ask someone with write access'); + }); + + it('should not allow you to resolve the conflicts', () => { + expect(vm.text()).not.toContain('Resolve conflicts'); + }); + + it('should have merge buttons', () => { + const mergeLocallyButton = vm.find('.js-merge-locally-button'); + + expect(mergeLocallyButton.text()).toContain('Merge locally'); + }); }); - }); - - it('should tell you about conflicts without bothering other people', () => { - expect(vm.text()).toContain('There are merge conflicts'); - expect(vm.text()).not.toContain('ask someone with write access'); - }); - it('should allow you to resolve the conflicts', () => { - const resolveButton = vm.find('.js-resolve-conflicts-button'); - - expect(resolveButton.text()).toContain('Resolve conflicts'); - expect(resolveButton.attributes('href')).toEqual(path); - }); - - it('should have merge buttons', () => { - const mergeLocallyButton = vm.find('.js-merge-locally-button'); - - expect(mergeLocallyButton.text()).toContain('Merge locally'); - }); - }); - - describe('when user does not have permission to push to source branch', () => { - it('should show proper message', () => { - createComponent({ - mr: { - canMerge: false, - canPushToSourceBranch: false, - conflictsDocsPath: '', - }, + describe('when not allowed to merge but allowed to push to source branch', () => { + beforeEach(async () => { + await createComponent({ + mr: { + canMerge: false, + canPushToSourceBranch: true, + conflictResolutionPath: path, + conflictsDocsPath: '', + }, + }); + }); + + it('should tell you about conflicts', () => { + expect(vm.text()).toContain('There are merge conflicts'); + expect(vm.text()).toContain('ask someone with write access'); + }); + + it('should allow you to resolve the conflicts', () => { + const resolveButton = vm.find('.js-resolve-conflicts-button'); + + expect(resolveButton.text()).toContain('Resolve conflicts'); + expect(resolveButton.attributes('href')).toEqual(path); + }); + + it('should not have merge buttons', () => { + expect(vm.text()).not.toContain('Merge locally'); + }); }); - expect( - vm - .text() - .trim() - .replace(/\s\s+/g, ' '), - ).toContain('ask someone with write access'); - }); - - it('should not have action buttons', () => { - createComponent({ - mr: { - canMerge: false, - canPushToSourceBranch: false, - conflictsDocsPath: '', - }, + describe('when allowed to merge and push to source branch', () => { + beforeEach(async () => { + await createComponent({ + mr: { + canMerge: true, + canPushToSourceBranch: true, + conflictResolutionPath: path, + conflictsDocsPath: '', + }, + }); + }); + + it('should tell you about conflicts without bothering other people', () => { + expect(vm.text()).toContain('There are merge conflicts'); + expect(vm.text()).not.toContain('ask someone with write access'); + }); + + it('should allow you to resolve the conflicts', () => { + const resolveButton = vm.find('.js-resolve-conflicts-button'); + + expect(resolveButton.text()).toContain('Resolve conflicts'); + expect(resolveButton.attributes('href')).toEqual(path); + }); + + it('should have merge buttons', () => { + const mergeLocallyButton = vm.find('.js-merge-locally-button'); + + expect(mergeLocallyButton.text()).toContain('Merge locally'); + }); }); - expect(vm.find('.js-resolve-conflicts-button').exists()).toBe(false); - expect(vm.find('.js-merge-locally-button').exists()).toBe(false); - }); - - it('should not have resolve button when no conflict resolution path', () => { - createComponent({ - mr: { - canMerge: true, - conflictResolutionPath: null, - conflictsDocsPath: '', - }, + describe('when user does not have permission to push to source branch', () => { + it('should show proper message', async () => { + await createComponent({ + mr: { + canMerge: false, + canPushToSourceBranch: false, + conflictsDocsPath: '', + }, + }); + + expect( + vm + .text() + .trim() + .replace(/\s\s+/g, ' '), + ).toContain('ask someone with write access'); + }); + + it('should not have action buttons', async () => { + await createComponent({ + mr: { + canMerge: false, + canPushToSourceBranch: false, + conflictsDocsPath: '', + }, + }); + + expect(vm.find('.js-resolve-conflicts-button').exists()).toBe(false); + expect(vm.find('.js-merge-locally-button').exists()).toBe(false); + }); + + it('should not have resolve button when no conflict resolution path', async () => { + await createComponent({ + mr: { + canMerge: true, + conflictResolutionPath: null, + conflictsDocsPath: '', + }, + }); + + expect(vm.find('.js-resolve-conflicts-button').exists()).toBe(false); + }); }); - expect(vm.find('.js-resolve-conflicts-button').exists()).toBe(false); - }); - }); - - describe('when fast-forward or semi-linear merge enabled', () => { - it('should tell you to rebase locally', () => { - createComponent({ - mr: { - shouldBeRebased: true, - conflictsDocsPath: '', - }, + describe('when fast-forward or semi-linear merge enabled', () => { + it('should tell you to rebase locally', async () => { + await createComponent({ + mr: { + shouldBeRebased: true, + conflictsDocsPath: '', + }, + }); + + expect(removeBreakLine(vm.text()).trim()).toContain( + 'Fast-forward merge is not possible. To merge this request, first rebase locally.', + ); + }); }); - expect(removeBreakLine(vm.text()).trim()).toContain( - 'Fast-forward merge is not possible. To merge this request, first rebase locally.', - ); - }); - }); - - describe('when source branch protected', () => { - beforeEach(() => { - createComponent({ - mr: { - canMerge: true, - canPushToSourceBranch: true, - conflictResolutionPath: TEST_HOST, - sourceBranchProtected: true, - conflictsDocsPath: '', - }, + describe('when source branch protected', () => { + beforeEach(async () => { + await createComponent({ + mr: { + canMerge: true, + canPushToSourceBranch: true, + conflictResolutionPath: TEST_HOST, + sourceBranchProtected: true, + conflictsDocsPath: '', + }, + }); + }); + + it('sets resolve button as disabled', () => { + expect(vm.find('.js-resolve-conflicts-button').attributes('disabled')).toBe('true'); + }); + + it('renders popover', () => { + expect($.fn.popover).toHaveBeenCalled(); + }); }); - }); - - it('sets resolve button as disabled', () => { - expect(vm.find('.js-resolve-conflicts-button').attributes('disabled')).toBe('true'); - }); - it('renders popover', () => { - expect($.fn.popover).toHaveBeenCalled(); - }); - }); - - describe('when source branch not protected', () => { - beforeEach(() => { - createComponent({ - mr: { - canMerge: true, - canPushToSourceBranch: true, - conflictResolutionPath: TEST_HOST, - sourceBranchProtected: false, - conflictsDocsPath: '', - }, + describe('when source branch not protected', () => { + beforeEach(async () => { + await createComponent({ + mr: { + canMerge: true, + canPushToSourceBranch: true, + conflictResolutionPath: TEST_HOST, + sourceBranchProtected: false, + conflictsDocsPath: '', + }, + }); + }); + + it('sets resolve button as disabled', () => { + expect(vm.find('.js-resolve-conflicts-button').attributes('disabled')).toBe(undefined); + }); + + it('renders popover', () => { + expect($.fn.popover).not.toHaveBeenCalled(); + }); }); }); - - it('sets resolve button as disabled', () => { - expect(vm.find('.js-resolve-conflicts-button').attributes('disabled')).toBe(undefined); - }); - - it('renders popover', () => { - expect($.fn.popover).not.toHaveBeenCalled(); - }); }); }); diff --git a/spec/graphql/types/merge_request_type_spec.rb b/spec/graphql/types/merge_request_type_spec.rb index 8800250b103..c929a93a9eb 100644 --- a/spec/graphql/types/merge_request_type_spec.rb +++ b/spec/graphql/types/merge_request_type_spec.rb @@ -27,7 +27,7 @@ RSpec.describe GitlabSchema.types['MergeRequest'] do upvotes downvotes head_pipeline pipelines task_completion_status milestone assignees participants subscribed labels discussion_locked time_estimate total_time_spent reference author merged_at commit_count current_user_todos - conflicts auto_merge_enabled approved_by + conflicts auto_merge_enabled approved_by source_branch_protected ] if Gitlab.ee? diff --git a/spec/helpers/whats_new_helper_spec.rb b/spec/helpers/whats_new_helper_spec.rb index 1c8684de75c..cdb4fc60629 100644 --- a/spec/helpers/whats_new_helper_spec.rb +++ b/spec/helpers/whats_new_helper_spec.rb @@ -3,22 +3,22 @@ require 'spec_helper' RSpec.describe WhatsNewHelper do - let(:fixture_dir_glob) { Dir.glob(File.join('spec', 'fixtures', 'whats_new', '*.yml')) } - describe '#whats_new_storage_key' do subject { helper.whats_new_storage_key } context 'when version exist' do + let(:release_item) { double(:item) } + before do - allow(Dir).to receive(:glob).with(Rails.root.join('data', 'whats_new', '*.yml')).and_return(fixture_dir_glob) + allow(ReleaseHighlight).to receive(:most_recent_version).and_return(84.0) end - it { is_expected.to eq('display-whats-new-notification-01.05') } + it { is_expected.to eq('display-whats-new-notification-84.0') } end - context 'when recent release items do NOT exist' do + context 'when most recent release highlights do NOT exist' do before do - allow(helper).to receive(:whats_new_release_items).and_return(nil) + allow(ReleaseHighlight).to receive(:most_recent_version).and_return(nil) end it { is_expected.to be_nil } @@ -30,31 +30,18 @@ RSpec.describe WhatsNewHelper do context 'when recent release items exist' do it 'returns the count from the most recent file' do - expect(Dir).to receive(:glob).with(Rails.root.join('data', 'whats_new', '*.yml')).and_return(fixture_dir_glob) + allow(ReleaseHighlight).to receive(:most_recent_item_count).and_return(1) expect(subject).to eq(1) end end context 'when recent release items do NOT exist' do - before do - allow(YAML).to receive(:safe_load).and_raise - - expect(Gitlab::ErrorTracking).to receive(:track_exception) - end + it 'returns nil' do + allow(ReleaseHighlight).to receive(:most_recent_item_count).and_return(nil) - it 'fails gracefully and logs an error' do expect(subject).to be_nil end end end - - # Testing this important private method here because the request spec required multiple confusing mocks and felt wrong and overcomplicated - describe '#whats_new_items_cache_key' do - it 'returns a key containing the most recent file name and page parameter' do - allow(Dir).to receive(:glob).with(Rails.root.join('data', 'whats_new', '*.yml')).and_return(fixture_dir_glob) - - expect(helper.send(:whats_new_items_cache_key, 2)).to eq('whats_new:release_items:file-20201225_01_05:page-2') - end - end end diff --git a/spec/models/release_highlight_spec.rb b/spec/models/release_highlight_spec.rb new file mode 100644 index 00000000000..b7817a04134 --- /dev/null +++ b/spec/models/release_highlight_spec.rb @@ -0,0 +1,135 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ReleaseHighlight do + describe '#paginated' do + let(:fixture_dir_glob) { Dir.glob(File.join('spec', 'fixtures', 'whats_new', '*.yml')) } + let(:cache_mock) { double(:cache_mock) } + let(:dot_com) { false } + + before do + allow(Gitlab).to receive(:com?).and_return(dot_com) + allow(Dir).to receive(:glob).with(Rails.root.join('data', 'whats_new', '*.yml')).and_return(fixture_dir_glob) + + expect(Rails).to receive(:cache).twice.and_return(cache_mock) + expect(cache_mock).to receive(:fetch).with('release_highlight:file_paths', expires_in: 1.hour).and_yield + end + + after do + ReleaseHighlight.instance_variable_set(:@file_paths, nil) + end + + context 'with page param' do + subject { ReleaseHighlight.paginated(page: page) } + + before do + allow(cache_mock).to receive(:fetch).and_yield + end + + context 'when there is another page of results' do + let(:page) { 2 } + + it 'responds with paginated results' do + expect(subject[:items].first['title']).to eq('bright') + expect(subject[:next_page]).to eq(3) + end + end + + context 'when there is NOT another page of results' do + let(:page) { 3 } + + it 'responds with paginated results and no next_page' do + expect(subject[:items].first['title']).to eq("It's gonna be a bright") + expect(subject[:next_page]).to eq(nil) + end + end + + context 'when that specific page does not exist' do + let(:page) { 84 } + + it 'returns nil' do + expect(subject).to be_nil + end + end + end + + context 'with no page param' do + subject { ReleaseHighlight.paginated } + + before do + expect(cache_mock).to receive(:fetch).with('release_highlight:items:file-20201225_01_05:page-1', expires_in: 1.hour).and_yield + end + + it 'returns platform specific items and uses a cache key' do + expect(subject[:items].count).to eq(1) + expect(subject[:items].first['title']).to eq("bright and sunshinin' day") + expect(subject[:next_page]).to eq(2) + end + + context 'when Gitlab.com' do + let(:dot_com) { true } + + it 'responds with a different set of data' do + expect(subject[:items].count).to eq(1) + expect(subject[:items].first['title']).to eq("I think I can make it now the pain is gone") + end + end + + context 'when recent release items do NOT exist' do + before do + allow(YAML).to receive(:safe_load).and_raise(Psych::Exception) + + expect(Gitlab::ErrorTracking).to receive(:track_exception) + end + + it 'fails gracefully and logs an error' do + expect(subject).to be_nil + end + end + end + end + + describe '.most_recent_version' do + subject { ReleaseHighlight.most_recent_version } + + context 'when version exist' do + let(:release_item) { double(:item) } + + before do + allow(ReleaseHighlight).to receive(:paginated).and_return({ items: [release_item] }) + allow(release_item).to receive(:[]).with('release').and_return(84.0) + end + + it { is_expected.to eq(84.0) } + end + + context 'when most recent release highlights do NOT exist' do + before do + allow(ReleaseHighlight).to receive(:paginated).and_return(nil) + end + + it { is_expected.to be_nil } + end + end + + describe '#most_recent_item_count' do + subject { ReleaseHighlight.most_recent_item_count } + + context 'when recent release items exist' do + it 'returns the count from the most recent file' do + allow(ReleaseHighlight).to receive(:paginated).and_return({ items: [double(:item)] }) + + expect(subject).to eq(1) + end + end + + context 'when recent release items do NOT exist' do + it 'returns nil' do + allow(ReleaseHighlight).to receive(:paginated).and_return(nil) + + expect(subject).to be_nil + end + end + end +end diff --git a/spec/presenters/gitlab/whats_new/item_presenter_spec.rb b/spec/presenters/gitlab/whats_new/item_presenter_spec.rb new file mode 100644 index 00000000000..b7b711e04c7 --- /dev/null +++ b/spec/presenters/gitlab/whats_new/item_presenter_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::WhatsNew::ItemPresenter do + let(:present) { Gitlab::WhatsNew::ItemPresenter.present(item) } + let(:item) { { "packages" => %w(Premium Ultimate) } } + let(:gitlab_com) { true } + + before do + allow(Gitlab).to receive(:com?).and_return(gitlab_com) + end + + describe '.present' do + context 'when on Gitlab.com' do + it 'transforms package names to gitlab.com friendly package names' do + expect(present).to eq({ "packages" => %w(Silver Gold) }) + end + end + + context 'when not on Gitlab.com' do + let(:gitlab_com) { false } + + it 'does not transform package names' do + expect(present).to eq({ "packages" => %w(Premium Ultimate) }) + end + end + end +end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 4a792fc218d..eb3e610934d 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -254,7 +254,7 @@ RSpec.describe API::Projects do statistics = json_response.find { |p| p['id'] == project.id }['statistics'] expect(statistics).to be_present - expect(statistics).to include('commit_count', 'storage_size', 'repository_size', 'wiki_size', 'lfs_objects_size', 'job_artifacts_size', 'snippets_size') + expect(statistics).to include('commit_count', 'storage_size', 'repository_size', 'wiki_size', 'lfs_objects_size', 'job_artifacts_size', 'snippets_size', 'packages_size') end it "does not include license by default" do @@ -619,7 +619,7 @@ RSpec.describe API::Projects do end context 'sorting by project statistics' do - %w(repository_size storage_size wiki_size).each do |order_by| + %w(repository_size storage_size wiki_size packages_size).each do |order_by| context "sorting by #{order_by}" do before do ProjectStatistics.update_all(order_by => 100) diff --git a/spec/requests/whats_new_controller_spec.rb b/spec/requests/whats_new_controller_spec.rb index c04a6b00a93..30d741ee0f0 100644 --- a/spec/requests/whats_new_controller_spec.rb +++ b/spec/requests/whats_new_controller_spec.rb @@ -5,28 +5,30 @@ require 'spec_helper' RSpec.describe WhatsNewController do describe 'whats_new_path' do context 'with whats_new_drawer feature enabled' do - let(:fixture_dir_glob) { Dir.glob(File.join('spec', 'fixtures', 'whats_new', '*.yml')) } - before do stub_feature_flags(whats_new_drawer: true) - allow(Dir).to receive(:glob).with(Rails.root.join('data', 'whats_new', '*.yml')).and_return(fixture_dir_glob) end context 'with no page param' do + let(:most_recent) { { items: [item], next_page: 2 } } + let(:item) { double(:item) } + it 'responds with paginated data and headers' do + allow(ReleaseHighlight).to receive(:paginated).with(page: 1).and_return(most_recent) + allow(Gitlab::WhatsNew::ItemPresenter).to receive(:present).with(item).and_return(item) + get whats_new_path, xhr: true - expect(response.body).to eq([{ title: "bright and sunshinin' day", release: "01.05" }].to_json) + expect(response.body).to eq(most_recent[:items].to_json) expect(response.headers['X-Next-Page']).to eq(2) end end context 'with page param' do - it 'responds with paginated data and headers' do - get whats_new_path(page: 2), xhr: true + it 'passes the page parameter' do + expect(ReleaseHighlight).to receive(:paginated).with(page: 2).and_call_original - expect(response.body).to eq([{ title: 'bright' }].to_json) - expect(response.headers['X-Next-Page']).to eq(3) + get whats_new_path(page: 2), xhr: true end it 'returns a 404 if page param is negative' do @@ -34,14 +36,6 @@ RSpec.describe WhatsNewController do expect(response).to have_gitlab_http_status(:not_found) end - - context 'when there are no more paginated results' do - it 'responds with nil X-Next-Page header' do - get whats_new_path(page: 3), xhr: true - expect(response.body).to eq([{ title: "It's gonna be a bright" }].to_json) - expect(response.headers['X-Next-Page']).to be nil - end - end end end |