diff options
39 files changed, 694 insertions, 301 deletions
diff --git a/.gitlab/issue_templates/Experiment Successful Cleanup.md b/.gitlab/issue_templates/Experiment Successful Cleanup.md index 14a29452e49..3831090aad6 100644 --- a/.gitlab/issue_templates/Experiment Successful Cleanup.md +++ b/.gitlab/issue_templates/Experiment Successful Cleanup.md @@ -10,6 +10,8 @@ The changes need to become an official part of the product. - [ ] Determine whether the feature should apply to SaaS and/or self-managed - [ ] Determine whether the feature should apply to EE - and which tiers - and/or Core - [ ] Determine if tracking should be kept as is, removed, or modified. +- [ ] Determine if any UX experiences need to be "polished" i.e. updated to further improve the end user experience. This task should be completed by the designated UX counterpart. + - [ ] (placeholder for UX polish work that needs to be completed for this cleanup issue to be considered completed) - [ ] Ensure any relevant documentation has been updated. - [ ] Determine whether there are other concerns that need to be considered before removing the feature flag. - These are typically captured in the `Experiment Successful Cleanup Concerns` section of the rollout issue. @@ -437,7 +437,7 @@ group :development, :test do end group :development, :test, :danger do - gem 'gitlab-dangerfiles', '~> 3.6.6', require: false + gem 'gitlab-dangerfiles', '~> 3.6.7', require: false end group :development, :test, :coverage do diff --git a/Gemfile.lock b/Gemfile.lock index 556647fbac4..0de6fab1d2c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1679,7 +1679,7 @@ DEPENDENCIES gettext_i18n_rails_js (~> 1.3) gitaly (~> 15.9.0.pre.rc1) gitlab-chronic (~> 0.10.5) - gitlab-dangerfiles (~> 3.6.6) + gitlab-dangerfiles (~> 3.6.7) gitlab-experiment (~> 0.7.1) gitlab-fog-azure-rm (~> 1.4.0) gitlab-labkit (~> 0.29.0) diff --git a/app/assets/javascripts/ci/runner/components/runner_bulk_delete.vue b/app/assets/javascripts/ci/runner/components/runner_bulk_delete.vue index 1ec3f8da7c3..8dde3ac4e19 100644 --- a/app/assets/javascripts/ci/runner/components/runner_bulk_delete.vue +++ b/app/assets/javascripts/ci/runner/components/runner_bulk_delete.vue @@ -162,22 +162,28 @@ export default { </script> <template> - <div v-if="checkedCount" class="gl-my-4 gl-p-4 gl-border-1 gl-border-solid gl-border-gray-100"> - <div class="gl-display-flex gl-align-items-center"> - <div> - <gl-sprintf :message="bannerMessage"> - <template #strong="{ content }"> - <strong>{{ content }}</strong> - </template> - </gl-sprintf> - </div> - <div class="gl-ml-auto"> - <gl-button variant="default" @click="onClearChecked">{{ - s__('Runners|Clear selection') - }}</gl-button> - <gl-button v-gl-modal="$options.BULK_DELETE_MODAL_ID" variant="danger">{{ - s__('Runners|Delete selected') - }}</gl-button> + <div> + <div + v-if="checkedCount" + data-testid="runner-bulk-delete-banner" + class="gl-my-4 gl-p-4 gl-border-1 gl-border-solid gl-border-gray-100" + > + <div class="gl-display-flex gl-align-items-center"> + <div> + <gl-sprintf :message="bannerMessage"> + <template #strong="{ content }"> + <strong>{{ content }}</strong> + </template> + </gl-sprintf> + </div> + <div class="gl-ml-auto"> + <gl-button variant="default" @click="onClearChecked">{{ + s__('Runners|Clear selection') + }}</gl-button> + <gl-button v-gl-modal="$options.BULK_DELETE_MODAL_ID" variant="danger">{{ + s__('Runners|Delete selected') + }}</gl-button> + </div> </div> </div> <gl-modal diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 457a519d70e..9bf382c41e7 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -4,7 +4,7 @@ import { GlBreakpointInstance as breakpointInstance } from '@gitlab/ui/dist/utils'; import $ from 'jquery'; -import { isFunction, defer, escape } from 'lodash'; +import { isFunction, defer, escape, partial, toLower } from 'lodash'; import Cookies from '~/lib/utils/cookies'; import { SCOPED_LABEL_DELIMITER } from '~/sidebar/components/labels/labels_select_widget/constants'; import { convertToCamelCase, convertToSnakeCase } from './text_utility'; @@ -552,6 +552,22 @@ export const convertObjectPropsToCamelCase = (obj = {}, options = {}) => convertObjectProps(convertToCamelCase, obj, options); /** + * This method returns a new object with lowerCase property names + * + * Reasoning for this method is to ensure consistent access for some + * sort of objects + * + * This method also supports additional params in `options` object + * + * @param {Object} obj - Object to be converted. + * @param {Object} options - Object containing additional options. + * @param {boolean} options.deep - FLag to allow deep object converting + * @param {Array[]} options.dropKeys - List of properties to discard while building new object + * @param {Array[]} options.ignoreKeyNames - List of properties to leave intact while building new object + */ +export const convertObjectPropsToLowerCase = partial(convertObjectProps, toLower); + +/** * Converts all the object keys to snake case * * This method also supports additional params in `options` object diff --git a/app/assets/javascripts/pages/projects/edit/index.js b/app/assets/javascripts/pages/projects/edit/index.js index 7d81ef30f2c..82035008459 100644 --- a/app/assets/javascripts/pages/projects/edit/index.js +++ b/app/assets/javascripts/pages/projects/edit/index.js @@ -11,6 +11,7 @@ import initSettingsPanels from '~/settings_panels'; import UserCallout from '~/user_callout'; import initTopicsTokenSelector from '~/projects/settings/topics'; import { initProjectSelects } from '~/vue_shared/components/entity_select/init_project_selects'; +import initPruneObjectsButton from '~/projects/prune_objects_button'; import initProjectPermissionsSettings from '../shared/permissions'; import initProjectLoadingSpinner from '../shared/save_project_loader'; @@ -18,6 +19,7 @@ initFilePickers(); initConfirmDanger(); initSettingsPanels(); initProjectDeleteButton(); +initPruneObjectsButton(); mountBadgeSettings(PROJECT_BADGE); new UserCallout({ className: 'js-service-desk-callout' }); // eslint-disable-line no-new diff --git a/app/assets/javascripts/projects/commit/components/branches_dropdown.vue b/app/assets/javascripts/projects/commit/components/branches_dropdown.vue index a037e721677..0ed154c47dd 100644 --- a/app/assets/javascripts/projects/commit/components/branches_dropdown.vue +++ b/app/assets/javascripts/projects/commit/components/branches_dropdown.vue @@ -1,12 +1,7 @@ <script> -import { - GlDropdown, - GlSearchBoxByType, - GlDropdownItem, - GlDropdownText, - GlLoadingIcon, -} from '@gitlab/ui'; +import { GlCollapsibleListbox } from '@gitlab/ui'; import { mapActions, mapGetters, mapState } from 'vuex'; +import { debounce } from 'lodash'; import { I18N_NO_RESULTS_MESSAGE, I18N_BRANCH_HEADER, @@ -16,11 +11,7 @@ import { export default { name: 'BranchesDropdown', components: { - GlDropdown, - GlSearchBoxByType, - GlDropdownItem, - GlDropdownText, - GlLoadingIcon, + GlCollapsibleListbox, }, props: { value: { @@ -46,19 +37,17 @@ export default { }, computed: { ...mapGetters(['joinedBranches']), - ...mapState(['isFetching', 'branch', 'branches']), - filteredResults() { - const lowerCasedSearchTerm = this.searchTerm.toLowerCase(); - return this.joinedBranches.filter((resultString) => - resultString.toLowerCase().includes(lowerCasedSearchTerm), - ); + ...mapState(['isFetching']), + listboxItems() { + return this.joinedBranches.map((value) => ({ value, text: value })); }, }, watch: { // Parent component can set the branch value (e.g. when the user selects a different project) // and we need to keep the search term in sync with the selected value value(val) { - this.searchTermChanged(val); + this.searchTerm = val; + this.fetchBranches(this.searchTerm); }, }, mounted() { @@ -67,50 +56,29 @@ export default { methods: { ...mapActions(['fetchBranches']), selectBranch(branch) { - this.$emit('selectBranch', branch); - this.searchTerm = branch; // enables isSelected to work as expected - }, - isSelected(selectedBranch) { - return selectedBranch === this.branch; + this.$emit('input', branch); }, + debouncedSearch: debounce(function debouncedSearch() { + this.fetchBranches(this.searchTerm); + }, 250), searchTermChanged(value) { - this.searchTerm = value; - this.fetchBranches(value); + this.searchTerm = value.trim(); + this.debouncedSearch(value); }, }, }; </script> <template> - <gl-dropdown :text="value" :header-text="$options.i18n.branchHeaderTitle"> - <gl-search-box-by-type - :value="searchTerm" - trim - autocomplete="off" - :debounce="250" - :placeholder="$options.i18n.branchSearchPlaceholder" - data-testid="dropdown-search-box" - @input="searchTermChanged" - /> - <gl-dropdown-item - v-for="branch in filteredResults" - v-show="!isFetching" - :key="branch" - :name="branch" - :is-checked="isSelected(branch)" - is-check-item - data-testid="dropdown-item" - @click="selectBranch(branch)" - > - {{ branch }} - </gl-dropdown-item> - <gl-dropdown-text v-show="isFetching" data-testid="dropdown-text-loading-icon"> - <gl-loading-icon size="sm" class="gl-mx-auto" /> - </gl-dropdown-text> - <gl-dropdown-text - v-if="!filteredResults.length && !isFetching" - data-testid="empty-result-message" - > - <span class="gl-text-gray-500">{{ $options.i18n.noResultsMessage }}</span> - </gl-dropdown-text> - </gl-dropdown> + <gl-collapsible-listbox + :header-text="$options.i18n.branchHeaderTitle" + :toggle-text="value" + :items="listboxItems" + searchable + :search-placeholder="$options.i18n.branchSearchPlaceholder" + :searching="isFetching" + :selected="value" + :no-results-text="$options.i18n.noResultsMessage" + @search="searchTermChanged" + @select="selectBranch" + /> </template> diff --git a/app/assets/javascripts/projects/commit/components/form_modal.vue b/app/assets/javascripts/projects/commit/components/form_modal.vue index 921c66587c9..f78afef1c17 100644 --- a/app/assets/javascripts/projects/commit/components/form_modal.vue +++ b/app/assets/javascripts/projects/commit/components/form_modal.vue @@ -151,12 +151,7 @@ export default { > <input id="start_branch" type="hidden" name="start_branch" :value="branch" /> - <branches-dropdown - class="gl-w-half" - :value="branch" - :blanked="isRevert" - @selectBranch="setBranch" - /> + <branches-dropdown :value="branch" :blanked="isRevert" @input="setBranch" /> </gl-form-group> <gl-form-checkbox diff --git a/app/assets/javascripts/projects/prune_objects_button.js b/app/assets/javascripts/projects/prune_objects_button.js new file mode 100644 index 00000000000..dba73f6a19d --- /dev/null +++ b/app/assets/javascripts/projects/prune_objects_button.js @@ -0,0 +1,23 @@ +import Vue from 'vue'; +import PruneUnreachableObjectsButton from './prune_unreachable_objects_button.vue'; + +export default (selector = '#js-project-prune-unreachable-objects-button') => { + const el = document.querySelector(selector); + + if (!el) return; + + const { pruneObjectsPath, pruneObjectsDocPath } = el.dataset; + + // eslint-disable-next-line no-new + new Vue({ + el, + render(createElement) { + return createElement(PruneUnreachableObjectsButton, { + props: { + pruneObjectsPath, + pruneObjectsDocPath, + }, + }); + }, + }); +}; diff --git a/app/assets/javascripts/projects/prune_unreachable_objects_button.vue b/app/assets/javascripts/projects/prune_unreachable_objects_button.vue new file mode 100644 index 00000000000..1387fbb78c0 --- /dev/null +++ b/app/assets/javascripts/projects/prune_unreachable_objects_button.vue @@ -0,0 +1,75 @@ +<script> +import { GlButton, GlLink, GlModal, GlModalDirective } from '@gitlab/ui'; +import csrf from '~/lib/utils/csrf'; +import { s__ } from '~/locale'; + +export default { + components: { + GlButton, + GlLink, + GlModal, + }, + PRUNE_UNREACHABLE_OBJECTS_MODAL_ID: 'prune-objects-modal', + MODAL_ACTION_PRIMARY: { + text: s__('UpdateProject|Prune'), + attributes: [{ variant: 'danger' }], + }, + MODAL_ACTION_CANCEL: { + text: s__('UpdateProject|Cancel'), + }, + directives: { + GlModal: GlModalDirective, + }, + props: { + pruneObjectsPath: { + type: String, + required: true, + }, + pruneObjectsDocPath: { + type: String, + required: true, + }, + }, + computed: { + csrfToken() { + return csrf.token; + }, + }, + methods: { + submitForm() { + this.$refs.form.submit(); + }, + }, +}; +</script> + +<template> + <form ref="form" :action="pruneObjectsPath" method="post"> + <input :value="csrfToken" type="hidden" name="authenticity_token" /> + <input value="true" type="hidden" name="prune" /> + <gl-modal + :modal-id="$options.PRUNE_UNREACHABLE_OBJECTS_MODAL_ID" + :title="s__('UpdateProject|Are you sure you want to prune unreachable objects?')" + :action-primary="$options.MODAL_ACTION_PRIMARY" + :action-cancel="$options.MODAL_ACTION_CANCEL" + size="sm" + :no-focus-on-show="true" + @ok="submitForm" + > + <p> + {{ s__('UpdateProject|Pruning unreachable objects can lead to repository corruption.') }} + <gl-link :href="pruneObjectsDocPath" target="_blank"> + {{ s__('UpdateProject|Learn more.') }} + </gl-link> + {{ s__('UpdateProject|Are you sure you want to prune?') }} + </p> + </gl-modal> + <gl-button + v-gl-modal="$options.PRUNE_UNREACHABLE_OBJECTS_MODAL_ID" + category="primary" + variant="danger" + > + {{ s__('UpdateProject|Prune unreachable objects') }} + </gl-button> + </form> +</template> diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index c5306d16bc7..0338c912b53 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -222,7 +222,13 @@ class ProjectsController < Projects::ApplicationController end def housekeeping - ::Repositories::HousekeepingService.new(@project, :eager).execute + task = if params[:prune].present? + :prune + else + :eager + end + + ::Repositories::HousekeepingService.new(@project, task).execute redirect_to( project_path(@project), diff --git a/app/services/packages/create_event_service.rb b/app/services/packages/create_event_service.rb index 8fed6e2def8..82c4292fca8 100644 --- a/app/services/packages/create_event_service.rb +++ b/app/services/packages/create_event_service.rb @@ -21,6 +21,17 @@ module Packages end end + def originator_type + case current_user + when User + :user + when DeployToken + :deploy_token + else + :guest + end + end + private def event_scope @@ -34,20 +45,5 @@ module Packages def event_name params[:event_name] end - - def originator_type - case current_user - when User - :user - when DeployToken - :deploy_token - else - :guest - end - end - - def guest? - originator_type == :guest - end end end diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index e9335762577..e87005434e4 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -74,6 +74,9 @@ = link_to _('Run housekeeping'), housekeeping_project_path(@project), method: :post, class: "btn gl-button btn-default" + .gl-display-inline-flex + #js-project-prune-unreachable-objects-button{ data: { prune_objects_path: housekeeping_project_path(@project, prune: true), prune_objects_doc_path: help_page_path('administration/housekeeping', anchor: 'prune-unreachable-objects') } } + = render 'export', project: @project = render_if_exists 'projects/settings/archive' diff --git a/app/workers/concerns/git_garbage_collect_methods.rb b/app/workers/concerns/git_garbage_collect_methods.rb index de3796f7e43..718031ec33e 100644 --- a/app/workers/concerns/git_garbage_collect_methods.rb +++ b/app/workers/concerns/git_garbage_collect_methods.rb @@ -84,13 +84,10 @@ module GitGarbageCollectMethods repository = resource.repository.raw_repository client = repository.gitaly_repository_client - case task - when :prune + if task == :prune client.prune_unreachable_objects - when :eager - client.optimize_repository(eager: true) else - client.optimize_repository + client.optimize_repository(eager: task == :eager) end rescue GRPC::NotFound => e Gitlab::GitLogger.error("#{__method__} failed:\nRepository not found") diff --git a/config/events/1675075830_API__PackagesHelpers_pull_package_by_guest.yml b/config/events/1675075830_API__PackagesHelpers_pull_package_by_guest.yml new file mode 100644 index 00000000000..b66587e845c --- /dev/null +++ b/config/events/1675075830_API__PackagesHelpers_pull_package_by_guest.yml @@ -0,0 +1,26 @@ +--- +description: "Mirrored Service Ping total metric key_path: counts.package_events_i_package_pull_package_by_guest" +category: package class +action: pull_package_by_guest +label_description: "Mirrored Service Ping total metric key_path: counts.package_events_i_package_pull_package_by_guest" +property_description: +value_description: +extra_properties: +identifiers: +- project +- user +- namespace +product_section: dev +product_stage: package +product_group: package_registry +product_category: package_registry +milestone: "15.9" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111372 +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate + diff --git a/danger/roulette/Dangerfile b/danger/roulette/Dangerfile index 6dad4a0c597..ca5a671ef29 100644 --- a/danger/roulette/Dangerfile +++ b/danger/roulette/Dangerfile @@ -104,26 +104,26 @@ categories << :product_intelligence if helper.mr_labels.include?("product intell # Skip Product intelligence reviews for growth experiment MRs categories.delete(:product_intelligence) if helper.mr_labels.include?("growth experiment") -# if changes.any? -# random_roulette_spins = roulette.spin(nil, categories, timezone_experiment: false) +if changes.any? + random_roulette_spins = roulette.spin(nil, categories, timezone_experiment: false) -# rows = random_roulette_spins.map do |spin| -# markdown_row_for_spin(spin.category, spin) -# end + rows = random_roulette_spins.map do |spin| + markdown_row_for_spin(spin.category, spin) + end -# roulette.required_approvals.each do |approval| -# rows << markdown_row_for_spin(approval.category, approval.spin) -# end + roulette.required_approvals.each do |approval| + rows << markdown_row_for_spin(approval.category, approval.spin) + end -# markdown(REVIEW_ROULETTE_SECTION) + markdown(REVIEW_ROULETTE_SECTION) -# if rows.empty? -# markdown(NO_SUGGESTIONS) -# else -# markdown(CATEGORY_TABLE + rows.join("\n")) -# markdown(POST_TABLE_MESSAGE) -# end + if rows.empty? + markdown(NO_SUGGESTIONS) + else + markdown(CATEGORY_TABLE + rows.join("\n")) + markdown(POST_TABLE_MESSAGE) + end -# unknown = changes.fetch(:unknown, []) -# markdown(UNKNOWN_FILES_MESSAGE + helper.markdown_list(unknown)) unless unknown.empty? -# end + unknown = changes.fetch(:unknown, []) + markdown(UNKNOWN_FILES_MESSAGE + helper.markdown_list(unknown)) unless unknown.empty? +end diff --git a/db/post_migrate/20230125093840_rebalance_partition_id_ci_build.rb b/db/post_migrate/20230125093840_rebalance_partition_id_ci_build.rb new file mode 100644 index 00000000000..6165c266a82 --- /dev/null +++ b/db/post_migrate/20230125093840_rebalance_partition_id_ci_build.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class RebalancePartitionIdCiBuild < Gitlab::Database::Migration[2.1] + MIGRATION = 'RebalancePartitionId' + DELAY_INTERVAL = 2.minutes.freeze + TABLE = :ci_builds + BATCH_SIZE = 5_000 + SUB_BATCH_SIZE = 500 + + restrict_gitlab_migration gitlab_schema: :gitlab_ci + + def up + return unless Gitlab.com? + + queue_batched_background_migration( + MIGRATION, + TABLE, + :id, + job_interval: DELAY_INTERVAL, + batch_size: BATCH_SIZE, + sub_batch_size: SUB_BATCH_SIZE + ) + end + + def down + return unless Gitlab.com? + + delete_batched_background_migration(MIGRATION, TABLE, :id, []) + end +end diff --git a/db/schema_migrations/20230125093840 b/db/schema_migrations/20230125093840 new file mode 100644 index 00000000000..1d2fab25619 --- /dev/null +++ b/db/schema_migrations/20230125093840 @@ -0,0 +1 @@ +c66f77a9de07e2f88b6d371b14f7f72068a5b8e25cb382cb08e578021affbeb7
\ No newline at end of file diff --git a/doc/administration/housekeeping.md b/doc/administration/housekeeping.md index 1883594e32c..d58989db70c 100644 --- a/doc/administration/housekeeping.md +++ b/doc/administration/housekeeping.md @@ -119,6 +119,29 @@ background worker asks Gitaly to perform a number of optimizations. Housekeeping also [removes unreferenced LFS files](../raketasks/cleanup.md#remove-unreferenced-lfs-files) from your project every `200` push, freeing up storage space for your project. +### Prune unreachable objects + +Unreachable objects are pruned as part of scheduled housekeeping. However, +you can trigger manual pruning as well. An example: removing commits that contain sensitive +information. Triggering housekeeping prunes unreachable objects with a grace period of +two weeks. When you manually trigger the pruning of unreachable objects, the grace period +is reduced to 30 minutes. + +WARNING: +If a concurrent process (like `git push`) has created an object but hasn't created +a reference to the object yet, your repository can become corrupted if a reference +to the object is added after the object is deleted. The grace period exists to +reduce the likelihood of such race conditions. + +To trigger a manual prune of unreachable objects: + +1. On the top bar, select **Main menu > Projects** and find your project. +1. On the left sidebar, select **Settings > General**. +1. Expand **Advanced**. +1. Select **Run housekeeping**. +1. Wait 30 minutes for the operation to complete. +1. Return to the page where you selected **Run housekeeping**, and select **Prune unreachable objects**. + ### Scheduled housekeeping While GitLab automatically performs housekeeping tasks based on the number of diff --git a/doc/user/clusters/agent/install/index.md b/doc/user/clusters/agent/install/index.md index 297210ab8ef..bb9a9c371a2 100644 --- a/doc/user/clusters/agent/install/index.md +++ b/doc/user/clusters/agent/install/index.md @@ -155,6 +155,10 @@ helm upgrade --install gitlab-agent gitlab/gitlab-agent \ ... ``` +NOTE: +DNS rebind protection is disabled when either the HTTP_PROXY or the HTTPS_PROXY environment variable is set, +and the domain DNS can't be resolved. + #### Advanced installation method GitLab also provides a [KPT package for the agent](https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/tree/master/build/deployment/gitlab-agent). This method provides greater flexibility, but is only recommended for advanced users. diff --git a/doc/user/project/import/github.md b/doc/user/project/import/github.md index 81c0a1d47f9..6234c9ef8de 100644 --- a/doc/user/project/import/github.md +++ b/doc/user/project/import/github.md @@ -7,7 +7,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Import your project from GitHub to GitLab **(FREE)** -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/381902) in GitLab 15.8, GitLab no longer automatically creates namespaces or groups that don't exist. GitLab also no longer falls back to using the user's personal namespace if the namespace or group name is taken. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/381902) in GitLab 15.8, GitLab no longer automatically creates namespaces or groups that don't exist. GitLab also no longer falls back to using the user's personal namespace if the namespace or group name is taken. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/378267) in GitLab 15.9, GitLab instances behind proxies no longer require `github.com` and `api.github.com` entries in the [allowlist for local requests](../../../security/webhooks.md#create-an-allowlist-for-local-requests). You can import your GitHub projects from either GitHub.com or GitHub Enterprise. Importing projects does not migrate or import any types of groups or organizations from GitHub to GitLab. @@ -65,9 +66,8 @@ prerequisites for those imports. If you are importing from GitHub Enterprise to a self-managed GitLab instance: - You must first enable the [GitHub integration](../../../integration/github.md). -- If GitLab is behind a HTTP/HTTPS proxy, you must populate the [allowlist for local requests](../../../security/webhooks.md#create-an-allowlist-for-local-requests) - with `github.com` and `api.github.com` to solve the hostname. For more information, read the issue - [Importing a GitHub project requires DNS resolution even when behind a proxy](https://gitlab.com/gitlab-org/gitlab/-/issues/37941). +- For GitLab 15.8 and earlier, you must add `github.com` and `api.github.com` entries in the + [allowlist for local requests](../../../security/webhooks.md#create-an-allowlist-for-local-requests). ### Importing from GitHub.com to self-managed GitLab diff --git a/lib/api/helpers/packages_helpers.rb b/lib/api/helpers/packages_helpers.rb index 46ee1167d32..0fb3a19b8fd 100644 --- a/lib/api/helpers/packages_helpers.rb +++ b/lib/api/helpers/packages_helpers.rb @@ -86,7 +86,9 @@ module API end def track_package_event(action, scope, **args) - ::Packages::CreateEventService.new(nil, current_user, event_name: action, scope: scope).execute + service = ::Packages::CreateEventService.new(nil, current_user, event_name: action, scope: scope) + service.execute + category = args.delete(:category) || self.options[:for].name event_name = "i_package_#{scope}_user" ::Gitlab::Tracking.event( @@ -100,7 +102,11 @@ module API return unless Feature.enabled?(:route_hll_to_snowplow_phase3) - track_push_package_by_deploy_token_event(action, category, args) + if action.to_s == 'push_package' && service.originator_type == :deploy_token + track_snowplow_event("push_package_by_deploy_token", category, args) + elsif action.to_s == 'pull_package' && service.originator_type == :guest + track_snowplow_event("pull_package_by_guest", category, args) + end end def present_package_file!(package_file, supports_direct_download: true) @@ -110,11 +116,9 @@ module API private - def track_push_package_by_deploy_token_event(action, category, args) - return unless action.to_s == 'push_package' && current_user.is_a?(DeployToken) - - event_name = 'i_package_push_package_by_deploy_token' - key_path = 'counts.package_events_i_package_push_package_by_deploy_token' + def track_snowplow_event(action_name, category, args) + event_name = "i_package_#{action_name}" + key_path = "counts.package_events_i_package_#{action_name}" service_ping_context = Gitlab::Tracking::ServicePingContext.new( data_source: :redis, key_path: key_path @@ -122,7 +126,7 @@ module API Gitlab::Tracking.event( category, - 'push_package_by_deploy_token', + action_name, property: event_name, label: key_path, context: [service_ping_context], diff --git a/lib/gitlab/http_connection_adapter.rb b/lib/gitlab/http_connection_adapter.rb index 3ef60be67a9..aec430f2686 100644 --- a/lib/gitlab/http_connection_adapter.rb +++ b/lib/gitlab/http_connection_adapter.rb @@ -59,8 +59,6 @@ module Gitlab end def dns_rebind_protection? - return false if Gitlab.http_proxy_env? - Gitlab::CurrentSettings.dns_rebinding_protection_enabled? end diff --git a/lib/gitlab/octokit/middleware.rb b/lib/gitlab/octokit/middleware.rb index a92860f7eb8..0e47672bb3c 100644 --- a/lib/gitlab/octokit/middleware.rb +++ b/lib/gitlab/octokit/middleware.rb @@ -11,7 +11,8 @@ module Gitlab Gitlab::UrlBlocker.validate!(env[:url], schemes: %w[http https], allow_localhost: allow_local_requests?, - allow_local_network: allow_local_requests? + allow_local_network: allow_local_requests?, + dns_rebind_protection: dns_rebind_protection? ) @app.call(env) @@ -22,6 +23,10 @@ module Gitlab def allow_local_requests? Gitlab::CurrentSettings.allow_local_requests_from_web_hooks_and_services? end + + def dns_rebind_protection? + Gitlab::CurrentSettings.dns_rebinding_protection_enabled? + end end end end diff --git a/lib/gitlab/url_blocker.rb b/lib/gitlab/url_blocker.rb index 00e609511f2..b620e9b4560 100644 --- a/lib/gitlab/url_blocker.rb +++ b/lib/gitlab/url_blocker.rb @@ -121,8 +121,8 @@ module Gitlab end rescue SocketError # If the dns rebinding protection is not enabled or the domain - # is allowed we avoid the dns rebinding checks - return if domain_allowed?(uri) || !dns_rebind_protection + # is allowed, or HTTP_PROXY is set we avoid the dns rebinding checks + return if domain_allowed?(uri) || !dns_rebind_protection || Gitlab.http_proxy_env? # In the test suite we use a lot of mocked urls that are either invalid or # don't exist. In order to avoid modifying a ton of tests and factories diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 7cbfcaea1fb..5eb4c0b0157 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -34787,6 +34787,21 @@ msgstr "" msgid "PurchaseStep|An error occurred in the purchase step. If the problem persists please contact support at https://support.gitlab.com." msgstr "" +msgid "Purchase|A full name in your profile is required to make a purchase. Check that the full name field in your %{userProfileLinkStart}user profile%{userProfileLinkEnd} has both a first and last name, then retry the purchase. If the problem persists, %{supportLinkStart}contact support%{supportLinkEnd}." +msgstr "" + +msgid "Purchase|An error occurred with your purchase because your group is currently linked to an expired subscription. %{supportLinkStart}Open a support ticket%{supportLinkEnd}, and our support team will assist with a workaround." +msgstr "" + +msgid "Purchase|An error occurred with your purchase. We detected a %{customersPortalLinkStart}Customers Portal%{customersPortalLinkEnd} account that matches your email address, but it has not been linked to your GitLab.com account. Follow the %{linkCustomersPortalHelpLinkStart}instructions to link your Customers Portal account%{linkCustomersPortalHelpLinkEnd}, and retry the purchase. If the problem persists, %{supportLinkStart}contact support%{supportLinkEnd}." +msgstr "" + +msgid "Purchase|Your card was declined due to insufficient funds. Make sure you have sufficient funds, then retry the purchase or use a different card. If the problem persists, %{supportLinkStart}contact support%{supportLinkEnd}." +msgstr "" + +msgid "Purchase|Your card was declined. Contact your card issuer for more information or %{salesLinkStart}contact our sales team%{salesLinkEnd} to pay with an alternative payment method." +msgstr "" + msgid "Push" msgstr "" @@ -45596,6 +45611,15 @@ msgstr "" msgid "Update your project name, topics, description, and avatar." msgstr "" +msgid "UpdateProject|Are you sure you want to prune unreachable objects?" +msgstr "" + +msgid "UpdateProject|Are you sure you want to prune?" +msgstr "" + +msgid "UpdateProject|Cancel" +msgstr "" + msgid "UpdateProject|Cannot rename project because it contains container registry tags!" msgstr "" @@ -45605,12 +45629,24 @@ msgstr "" msgid "UpdateProject|Could not set the default branch. Do you have a branch named 'HEAD' in your repository? (%{linkStart}How do I fix this?%{linkEnd})" msgstr "" +msgid "UpdateProject|Learn more." +msgstr "" + msgid "UpdateProject|New visibility level not allowed!" msgstr "" msgid "UpdateProject|Project could not be updated!" msgstr "" +msgid "UpdateProject|Prune" +msgstr "" + +msgid "UpdateProject|Prune unreachable objects" +msgstr "" + +msgid "UpdateProject|Pruning unreachable objects can lead to repository corruption." +msgstr "" + msgid "UpdateRepositoryStorage|Failed to verify %{type} repository checksum from %{old} to %{new}" msgstr "" diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index b711cdcf47c..c0c5dcfe21d 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -629,10 +629,21 @@ RSpec.describe ProjectsController do describe '#housekeeping' do let_it_be(:group) { create(:group) } - let_it_be(:project) { create(:project, group: group) } + let(:housekeeping_service_dbl) { instance_double(Repositories::HousekeepingService) } + let(:params) do + { + namespace_id: project.namespace.path, + id: project.path, + prune: prune + } + end + let(:prune) { nil } + let_it_be(:project) { create(:project, group: group) } let(:housekeeping) { Repositories::HousekeepingService.new(project) } + subject { post :housekeeping, params: params } + context 'when authenticated as owner' do before do group.add_owner(user) @@ -652,6 +663,18 @@ RSpec.describe ProjectsController do expect(response).to have_gitlab_http_status(:found) end + + context 'and requesting prune' do + let(:prune) { true } + + it 'enqueues pruning' do + allow(Repositories::HousekeepingService).to receive(:new).with(project, :prune).and_return(housekeeping_service_dbl) + expect(housekeeping_service_dbl).to receive(:execute) + + subject + expect(response).to have_gitlab_http_status(:found) + end + end end context 'when authenticated as developer' do diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb index 237f361bd72..a93242c0198 100644 --- a/spec/features/merge_request/user_sees_merge_widget_spec.rb +++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb @@ -89,12 +89,12 @@ RSpec.describe 'Merge request > User sees merge widget', :js, feature_category: click_button 'master' end - page.within("#{modal_selector} .dropdown-menu") do - find('[data-testid="dropdown-search-box"]').set('') + page.within("#{modal_selector} [data-testid=\"base-dropdown-menu\"]") do + fill_in 'Search branches', with: '' wait_for_requests - expect(page.all('[data-testid="dropdown-item"]').size).to be > 1 + expect(page).to have_selector('[data-testid="listbox-item-master"]', visible: true) end end end diff --git a/spec/features/projects/commit/cherry_pick_spec.rb b/spec/features/projects/commit/cherry_pick_spec.rb index dc8b84283a1..93ce851521f 100644 --- a/spec/features/projects/commit/cherry_pick_spec.rb +++ b/spec/features/projects/commit/cherry_pick_spec.rb @@ -77,10 +77,12 @@ RSpec.describe 'Cherry-pick Commits', :js, feature_category: :source_code_manage click_button 'master' end - page.within("#{modal_selector} .dropdown-menu") do - find('[data-testid="dropdown-search-box"]').set('feature') + page.within("#{modal_selector} [data-testid=\"base-dropdown-menu\"]") do + fill_in 'Search branches', with: 'feature' + wait_for_requests - click_button 'feature' + + find('[data-testid="listbox-item-feature"]').click end submit_cherry_pick diff --git a/spec/frontend/ci/runner/components/runner_bulk_delete_spec.js b/spec/frontend/ci/runner/components/runner_bulk_delete_spec.js index 64f5a0e3b57..0dc5a90fb83 100644 --- a/spec/frontend/ci/runner/components/runner_bulk_delete_spec.js +++ b/spec/frontend/ci/runner/components/runner_bulk_delete_spec.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import { makeVar } from '@apollo/client/core'; import { GlModal, GlSprintf } from '@gitlab/ui'; import VueApollo from 'vue-apollo'; import { createAlert } from '~/flash'; @@ -22,6 +23,7 @@ describe('RunnerBulkDelete', () => { let mockState; let mockCheckedRunnerIds; + const findBanner = () => wrapper.findByTestId('runner-bulk-delete-banner'); const findClearBtn = () => wrapper.findByText(s__('Runners|Clear selection')); const findDeleteBtn = () => wrapper.findByText(s__('Runners|Delete selected')); const findModal = () => wrapper.findComponent(GlModal); @@ -64,10 +66,11 @@ describe('RunnerBulkDelete', () => { beforeEach(() => { mockState = createLocalState(); + mockCheckedRunnerIds = makeVar([]); jest .spyOn(mockState.cacheConfig.typePolicies.Query.fields, 'checkedRunnerIds') - .mockImplementation(() => mockCheckedRunnerIds); + .mockImplementation(() => mockCheckedRunnerIds()); }); afterEach(() => { @@ -76,15 +79,13 @@ describe('RunnerBulkDelete', () => { describe('When no runners are checked', () => { beforeEach(async () => { - mockCheckedRunnerIds = []; - createComponent(); await waitForPromises(); }); it('shows no contents', () => { - expect(wrapper.html()).toBe(''); + expect(findBanner().exists()).toBe(false); }); }); @@ -94,7 +95,7 @@ describe('RunnerBulkDelete', () => { ${2} | ${[mockId1, mockId2]} | ${'2 runners'} `('When $count runner(s) are checked', ({ ids, text }) => { beforeEach(() => { - mockCheckedRunnerIds = ids; + mockCheckedRunnerIds(ids); createComponent(); @@ -102,7 +103,7 @@ describe('RunnerBulkDelete', () => { }); it(`shows "${text}"`, () => { - expect(wrapper.text()).toContain(text); + expect(findBanner().text()).toContain(text); }); it('clears selection', () => { @@ -133,7 +134,7 @@ describe('RunnerBulkDelete', () => { }; beforeEach(() => { - mockCheckedRunnerIds = [mockId1, mockId2]; + mockCheckedRunnerIds([mockId1, mockId2]); createComponent(); @@ -157,20 +158,23 @@ describe('RunnerBulkDelete', () => { it('mutation is called', () => { expect(bulkRunnerDeleteHandler).toHaveBeenCalledWith({ - input: { ids: mockCheckedRunnerIds }, + input: { ids: mockCheckedRunnerIds() }, }); }); }); describe('when deletion is successful', () => { + let deletedIds; + beforeEach(async () => { + deletedIds = mockCheckedRunnerIds(); bulkRunnerDeleteHandler.mockResolvedValue({ data: { - bulkRunnerDelete: { deletedIds: mockCheckedRunnerIds, errors: [] }, + bulkRunnerDelete: { deletedIds, errors: [] }, }, }); - confirmDeletion(); + mockCheckedRunnerIds([]); await waitForPromises(); }); @@ -182,12 +186,12 @@ describe('RunnerBulkDelete', () => { it('user interface is updated', () => { const { evict, gc } = apolloCache; - expect(evict).toHaveBeenCalledTimes(mockCheckedRunnerIds.length); + expect(evict).toHaveBeenCalledTimes(deletedIds.length); expect(evict).toHaveBeenCalledWith({ - id: expect.stringContaining(mockCheckedRunnerIds[0]), + id: expect.stringContaining(deletedIds[0]), }); expect(evict).toHaveBeenCalledWith({ - id: expect.stringContaining(mockCheckedRunnerIds[1]), + id: expect.stringContaining(deletedIds[1]), }); expect(gc).toHaveBeenCalledTimes(1); @@ -195,7 +199,7 @@ describe('RunnerBulkDelete', () => { it('emits deletion confirmation', () => { expect(wrapper.emitted('deleted')).toEqual([ - [{ message: expect.stringContaining(`${mockCheckedRunnerIds.length}`) }], + [{ message: expect.stringContaining(`${deletedIds.length}`) }], ]); }); diff --git a/spec/frontend/lib/utils/common_utils_spec.js b/spec/frontend/lib/utils/common_utils_spec.js index 947c38c8ae8..7b068f7d248 100644 --- a/spec/frontend/lib/utils/common_utils_spec.js +++ b/spec/frontend/lib/utils/common_utils_spec.js @@ -622,6 +622,23 @@ describe('common_utils', () => { milestones: ['12.3', '12.4'], }, }, + convertObjectPropsToLowerCase: { + obj: { + 'Project-Name': 'GitLab CE', + 'Group-Name': 'GitLab.org', + 'License-Type': 'MIT', + 'Mile-Stones': ['12.3', '12.4'], + }, + objNested: { + 'Project-Name': 'GitLab CE', + 'Group-Name': 'GitLab.org', + 'License-Type': 'MIT', + 'Tech-Stack': { + 'Frontend-Framework': 'Vue', + }, + 'Mile-Stones': ['12.3', '12.4'], + }, + }, }; describe('convertObjectProps', () => { @@ -637,6 +654,7 @@ describe('common_utils', () => { ${'convertObjectProps'} | ${mockObjects.convertObjectProps.obj} | ${mockObjects.convertObjectProps.objNested} ${'convertObjectPropsToCamelCase'} | ${mockObjects.convertObjectPropsToCamelCase.obj} | ${mockObjects.convertObjectPropsToCamelCase.objNested} ${'convertObjectPropsToSnakeCase'} | ${mockObjects.convertObjectPropsToSnakeCase.obj} | ${mockObjects.convertObjectPropsToSnakeCase.objNested} + ${'convertObjectPropsToLowerCase'} | ${mockObjects.convertObjectPropsToLowerCase.obj} | ${mockObjects.convertObjectPropsToLowerCase.objNested} `('$functionName', ({ functionName, mockObj, mockObjNested }) => { const testFunction = functionName === 'convertObjectProps' @@ -670,6 +688,12 @@ describe('common_utils', () => { absolute_web_url: 'https://gitlab.com/gitlab-org/', milestones: ['12.3', '12.4'], }, + convertObjectPropsToLowerCase: { + 'project-name': 'GitLab CE', + 'group-name': 'GitLab.org', + 'license-type': 'MIT', + 'mile-stones': ['12.3', '12.4'], + }, }; expect(testFunction(mockObj)).toEqual(expected[functionName]); @@ -710,6 +734,15 @@ describe('common_utils', () => { }, milestones: ['12.3', '12.4'], }, + convertObjectPropsToLowerCase: { + 'project-name': 'GitLab CE', + 'group-name': 'GitLab.org', + 'license-type': 'MIT', + 'tech-stack': { + 'Frontend-Framework': 'Vue', + }, + 'mile-stones': ['12.3', '12.4'], + }, }; expect(testFunction(mockObjNested)).toEqual(expected[functionName]); @@ -751,6 +784,15 @@ describe('common_utils', () => { }, milestones: ['12.3', '12.4'], }, + convertObjectPropsToLowerCase: { + 'project-name': 'GitLab CE', + 'group-name': 'GitLab.org', + 'license-type': 'MIT', + 'tech-stack': { + 'frontend-framework': 'Vue', + }, + 'mile-stones': ['12.3', '12.4'], + }, }; it('converts nested objects', () => { @@ -801,6 +843,15 @@ describe('common_utils', () => { }, milestones: ['12.3', '12.4'], }, + convertObjectPropsToLowerCase: { + 'project-name': 'GitLab CE', + 'group-name': 'GitLab.org', + 'license-type': 'MIT', + 'tech-stack': { + 'Frontend-Framework': 'Vue', + }, + 'mile-stones': ['12.3', '12.4'], + }, }; const dropKeys = { @@ -845,12 +896,20 @@ describe('common_utils', () => { }, milestones: ['12.3', '12.4'], }, + convertObjectPropsToLowerCase: { + 'project-name': 'GitLab CE', + 'tech-stack': { + 'frontend-framework': 'Vue', + }, + 'mile-stones': ['12.3', '12.4'], + }, }; const dropKeys = { convertObjectProps: ['group_name', 'database'], convertObjectPropsToCamelCase: ['group_name', 'database'], convertObjectPropsToSnakeCase: ['groupName', 'database'], + convertObjectPropsToLowerCase: ['Group-Name', 'License-Type'], }; expect( @@ -898,12 +957,22 @@ describe('common_utils', () => { }, milestones: ['12.3', '12.4'], }, + convertObjectPropsToLowerCase: { + 'project-name': 'GitLab CE', + 'Group-Name': 'GitLab.org', + 'license-type': 'MIT', + 'tech-stack': { + 'Frontend-Framework': 'Vue', + }, + 'mile-stones': ['12.3', '12.4'], + }, }; const ignoreKeyNames = { convertObjectProps: ['group_name'], convertObjectPropsToCamelCase: ['group_name'], convertObjectPropsToSnakeCase: ['groupName'], + convertObjectPropsToLowerCase: ['Group-Name'], }; expect( @@ -948,12 +1017,22 @@ describe('common_utils', () => { }, milestones: ['12.3', '12.4'], }, + convertObjectPropsToLowerCase: { + 'project-name': 'GitLab CE', + 'group-name': 'GitLab.org', + 'license-type': 'MIT', + 'tech-stack': { + 'Frontend-Framework': 'Vue', + }, + 'mile-stones': ['12.3', '12.4'], + }, }; const ignoreKeyNames = { convertObjectProps: ['group_name', 'frontend_framework'], convertObjectPropsToCamelCase: ['group_name', 'frontend_framework'], convertObjectPropsToSnakeCase: ['groupName', 'frontendFramework'], + convertObjectPropsToLowerCase: ['Frontend-Framework'], }; expect( diff --git a/spec/frontend/projects/commit/components/branches_dropdown_spec.js b/spec/frontend/projects/commit/components/branches_dropdown_spec.js index a84dd246f5d..6aa5a9a5a3a 100644 --- a/spec/frontend/projects/commit/components/branches_dropdown_spec.js +++ b/spec/frontend/projects/commit/components/branches_dropdown_spec.js @@ -1,9 +1,8 @@ -import { GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui'; +import { GlCollapsibleListbox } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; -import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import BranchesDropdown from '~/projects/commit/components/branches_dropdown.vue'; Vue.use(Vuex); @@ -35,11 +34,11 @@ describe('BranchesDropdown', () => { ); }; - const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); - const findSearchBoxByType = () => wrapper.findComponent(GlSearchBoxByType); - const findDropdownItemByIndex = (index) => wrapper.findAllComponents(GlDropdownItem).at(index); - const findNoResults = () => wrapper.findByTestId('empty-result-message'); - const findLoading = () => wrapper.findByTestId('dropdown-text-loading-icon'); + const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox); + + beforeEach(() => { + createComponent({ value: '' }); + }); afterEach(() => { wrapper.destroy(); @@ -48,138 +47,36 @@ describe('BranchesDropdown', () => { }); describe('On mount', () => { - beforeEach(() => { - createComponent({ value: '' }); - }); - it('invokes fetchBranches', () => { expect(spyFetchBranches).toHaveBeenCalled(); }); - - describe('with a value but visually blanked', () => { - beforeEach(() => { - createComponent({ value: '_main_', blanked: true }, { branch: '_main_' }); - }); - - it('renders all branches', () => { - expect(findAllDropdownItems()).toHaveLength(3); - expect(findDropdownItemByIndex(0).text()).toBe('_main_'); - expect(findDropdownItemByIndex(1).text()).toBe('_branch_1_'); - expect(findDropdownItemByIndex(2).text()).toBe('_branch_2_'); - }); - - it('selects the active branch', () => { - expect(wrapper.vm.isSelected('_main_')).toBe(true); - }); - }); }); - describe('Loading states', () => { - it('shows loading icon while fetching', () => { - createComponent({ value: '' }, { isFetching: true }); + describe('Value prop changes in parent component', () => { + it('triggers fetchBranches call', async () => { + await wrapper.setProps({ value: 'new value' }); - expect(findLoading().isVisible()).toBe(true); - }); - - it('does not show loading icon', () => { - createComponent({ value: '' }); - - expect(findLoading().isVisible()).toBe(false); - }); - }); - - describe('No branches found', () => { - beforeEach(() => { - createComponent({ value: '_non_existent_branch_' }); - }); - - it('renders empty results message', () => { - expect(findNoResults().text()).toBe('No matching results'); - }); - - it('shows GlSearchBoxByType with default attributes', () => { - expect(findSearchBoxByType().exists()).toBe(true); - expect(findSearchBoxByType().vm.$attrs).toMatchObject({ - placeholder: 'Search branches', - debounce: DEFAULT_DEBOUNCE_AND_THROTTLE_MS, - }); + expect(spyFetchBranches).toHaveBeenCalled(); }); }); - describe('Search term is empty', () => { - beforeEach(() => { - createComponent({ value: '' }); - }); + describe('Selecting Dropdown Item', () => { + it('emits event', async () => { + findDropdown().vm.$emit('select', '_anything_'); - it('renders all branches when search term is empty', () => { - expect(findAllDropdownItems()).toHaveLength(3); - expect(findDropdownItemByIndex(0).text()).toBe('_main_'); - expect(findDropdownItemByIndex(1).text()).toBe('_branch_1_'); - expect(findDropdownItemByIndex(2).text()).toBe('_branch_2_'); - }); - - it('should not be selected on the inactive branch', () => { - expect(wrapper.vm.isSelected('_main_')).toBe(false); + expect(wrapper.emitted()).toHaveProperty('input'); }); }); describe('When searching', () => { - beforeEach(() => { - createComponent({ value: '' }); - }); - it('invokes fetchBranches', async () => { const spy = jest.spyOn(wrapper.vm, 'fetchBranches'); - findSearchBoxByType().vm.$emit('input', '_anything_'); + findDropdown().vm.$emit('search', '_anything_'); await nextTick(); expect(spy).toHaveBeenCalledWith('_anything_'); - expect(wrapper.vm.searchTerm).toBe('_anything_'); - }); - }); - - describe('Branches found', () => { - beforeEach(() => { - createComponent({ value: '_branch_1_' }, { branch: '_branch_1_' }); - }); - - it('renders only the branch searched for', () => { - expect(findAllDropdownItems()).toHaveLength(1); - expect(findDropdownItemByIndex(0).text()).toBe('_branch_1_'); - }); - - it('should not display empty results message', () => { - expect(findNoResults().exists()).toBe(false); - }); - - it('should signify this branch is selected', () => { - expect(wrapper.vm.isSelected('_branch_1_')).toBe(true); - }); - - it('should signify the branch is not selected', () => { - expect(wrapper.vm.isSelected('_not_selected_branch_')).toBe(false); - }); - - describe('Custom events', () => { - it('should emit selectBranch if an branch is clicked', () => { - findDropdownItemByIndex(0).vm.$emit('click'); - - expect(wrapper.emitted('selectBranch')).toEqual([['_branch_1_']]); - expect(wrapper.vm.searchTerm).toBe('_branch_1_'); - }); - }); - }); - - describe('Case insensitive for search term', () => { - beforeEach(() => { - createComponent({ value: '_BrAnCh_1_' }); - }); - - it('renders only the branch searched for', () => { - expect(findAllDropdownItems()).toHaveLength(1); - expect(findDropdownItemByIndex(0).text()).toBe('_branch_1_'); }); }); }); diff --git a/spec/frontend/projects/commit/components/form_modal_spec.js b/spec/frontend/projects/commit/components/form_modal_spec.js index 20c312ec771..c59cf700e0d 100644 --- a/spec/frontend/projects/commit/components/form_modal_spec.js +++ b/spec/frontend/projects/commit/components/form_modal_spec.js @@ -157,7 +157,7 @@ describe('CommitFormModal', () => { }); it('Changes the start_branch input value', async () => { - findBranchesDropdown().vm.$emit('selectBranch', '_changed_branch_value_'); + findBranchesDropdown().vm.$emit('input', '_changed_branch_value_'); await nextTick(); diff --git a/spec/frontend/projects/prune_unreachable_objects_button_spec.js b/spec/frontend/projects/prune_unreachable_objects_button_spec.js new file mode 100644 index 00000000000..b345f264ca7 --- /dev/null +++ b/spec/frontend/projects/prune_unreachable_objects_button_spec.js @@ -0,0 +1,72 @@ +import { GlButton, GlModal, GlLink } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; +import { s__ } from '~/locale'; +import PruneObjectsButton from '~/projects/prune_unreachable_objects_button.vue'; + +jest.mock('~/lib/utils/csrf', () => ({ token: 'test-csrf-token' })); + +describe('Project remove modal', () => { + let wrapper; + + const findFormElement = () => wrapper.find('form'); + const findAuthenticityTokenInput = () => findFormElement().find('input[name=authenticity_token]'); + const findModal = () => wrapper.findComponent(GlModal); + const findBtn = () => wrapper.findComponent(GlButton); + const defaultProps = { + pruneObjectsPath: 'prunepath', + pruneObjectsDocPath: 'prunedocspath', + }; + + const createComponent = () => { + wrapper = shallowMountExtended(PruneObjectsButton, { + propsData: defaultProps, + directives: { + GlModal: createMockDirective(), + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('intialized', () => { + beforeEach(() => { + createComponent(); + }); + + it('sets a csrf token on the authenticity form input', () => { + expect(findAuthenticityTokenInput().element.value).toEqual('test-csrf-token'); + }); + + it('sets the form action to the provided path', () => { + expect(findFormElement().attributes('action')).toEqual(defaultProps.pruneObjectsPath); + }); + + it('sets the documentation link to the provided path', () => { + expect(findModal().findComponent(GlLink).attributes('href')).toEqual( + defaultProps.pruneObjectsDocPath, + ); + }); + + it('button opens modal', () => { + const buttonModalDirective = getBinding(findBtn().element, 'gl-modal'); + + expect(findModal().props('modalId')).toBe(buttonModalDirective.value); + expect(findModal().text()).toContain(s__('UpdateProject|Are you sure you want to prune?')); + }); + }); + + describe('when the modal is confirmed', () => { + beforeEach(() => { + createComponent(); + findModal().vm.$emit('ok'); + }); + + it('submits the form element', () => { + expect(findFormElement().element.submit).toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/lib/api/helpers/packages_helpers_spec.rb b/spec/lib/api/helpers/packages_helpers_spec.rb index bbb182071cc..2a663d5e9b2 100644 --- a/spec/lib/api/helpers/packages_helpers_spec.rb +++ b/spec/lib/api/helpers/packages_helpers_spec.rb @@ -310,5 +310,33 @@ RSpec.describe API::Helpers::PackagesHelpers, feature_category: :package_registr ) end end + + context 'when guest and action is pull package' do + let(:user) { nil } + let(:scope) { :rubygems } + let(:category) { 'API::RubygemPackages' } + let(:namespace) { project.namespace } + let(:label) { 'counts.package_events_i_package_pull_package_by_guest' } + let(:property) { 'i_package_pull_package_by_guest' } + let(:service_ping_context) do + [Gitlab::Tracking::ServicePingContext.new(data_source: :redis, key_path: 'counts.package_events_i_package_pull_package_by_guest').to_h] + end + + it 'logs a snowplow event' do + allow(helper).to receive(:current_user).and_return(nil) + args = { category: category, namespace: namespace, project: project } + helper.track_package_event('pull_package', scope, **args) + + expect_snowplow_event( + category: category, + action: 'pull_package_by_guest', + context: service_ping_context, + label: label, + namespace: namespace, + property: property, + project: project + ) + end + end end end diff --git a/spec/lib/gitlab/http_connection_adapter_spec.rb b/spec/lib/gitlab/http_connection_adapter_spec.rb index 5e2c6be8993..5137e098e2d 100644 --- a/spec/lib/gitlab/http_connection_adapter_spec.rb +++ b/spec/lib/gitlab/http_connection_adapter_spec.rb @@ -111,20 +111,6 @@ RSpec.describe Gitlab::HTTPConnectionAdapter do end end - context 'when http(s) environment variable is set' do - before do - stub_env('https_proxy' => 'https://my.proxy') - end - - it 'sets up the connection' do - expect(connection).to be_a(Gitlab::NetHttpAdapter) - expect(connection.address).to eq('example.org') - expect(connection.hostname_override).to eq(nil) - expect(connection.addr_port).to eq('example.org') - expect(connection.port).to eq(443) - end - end - context 'when URL scheme is not HTTP/HTTPS' do let(:uri) { URI('ssh://example.org') } diff --git a/spec/lib/gitlab/octokit/middleware_spec.rb b/spec/lib/gitlab/octokit/middleware_spec.rb index 7bce0788327..f7063f2c4f2 100644 --- a/spec/lib/gitlab/octokit/middleware_spec.rb +++ b/spec/lib/gitlab/octokit/middleware_spec.rb @@ -2,11 +2,11 @@ require 'spec_helper' -RSpec.describe Gitlab::Octokit::Middleware do +RSpec.describe Gitlab::Octokit::Middleware, feature_category: :importers do let(:app) { double(:app) } let(:middleware) { described_class.new(app) } - shared_examples 'Public URL' do + shared_examples 'Allowed URL' do it 'does not raise an error' do expect(app).to receive(:call).with(env) @@ -14,7 +14,7 @@ RSpec.describe Gitlab::Octokit::Middleware do end end - shared_examples 'Local URL' do + shared_examples 'Blocked URL' do it 'raises an error' do expect { middleware.call(env) }.to raise_error(Gitlab::UrlBlocker::BlockedUrlError) end @@ -24,7 +24,24 @@ RSpec.describe Gitlab::Octokit::Middleware do context 'when the URL is a public URL' do let(:env) { { url: 'https://public-url.com' } } - it_behaves_like 'Public URL' + it_behaves_like 'Allowed URL' + + context 'with failed address check' do + before do + stub_env('RSPEC_ALLOW_INVALID_URLS', 'false') + allow(Addrinfo).to receive(:getaddrinfo).and_raise(SocketError) + end + + it_behaves_like 'Blocked URL' + + context 'with disabled dns rebinding check' do + before do + stub_application_setting(dns_rebinding_protection_enabled: false) + end + + it_behaves_like 'Allowed URL' + end + end end context 'when the URL is a localhost address' do @@ -35,7 +52,7 @@ RSpec.describe Gitlab::Octokit::Middleware do stub_application_setting(allow_local_requests_from_web_hooks_and_services: false) end - it_behaves_like 'Local URL' + it_behaves_like 'Blocked URL' end context 'when localhost requests are allowed' do @@ -43,7 +60,7 @@ RSpec.describe Gitlab::Octokit::Middleware do stub_application_setting(allow_local_requests_from_web_hooks_and_services: true) end - it_behaves_like 'Public URL' + it_behaves_like 'Allowed URL' end end @@ -55,7 +72,7 @@ RSpec.describe Gitlab::Octokit::Middleware do stub_application_setting(allow_local_requests_from_web_hooks_and_services: false) end - it_behaves_like 'Local URL' + it_behaves_like 'Blocked URL' end context 'when local network requests are allowed' do @@ -63,7 +80,7 @@ RSpec.describe Gitlab::Octokit::Middleware do stub_application_setting(allow_local_requests_from_web_hooks_and_services: true) end - it_behaves_like 'Public URL' + it_behaves_like 'Allowed URL' end end diff --git a/spec/lib/gitlab/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb index 05f7af7606d..0d037984799 100644 --- a/spec/lib/gitlab/url_blocker_spec.rb +++ b/spec/lib/gitlab/url_blocker_spec.rb @@ -174,6 +174,17 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do expect { subject }.to raise_error(described_class::BlockedUrlError) end + + context 'with HTTP_PROXY' do + before do + allow(Gitlab).to receive(:http_proxy_env?).and_return(true) + end + + it_behaves_like 'validates URI and hostname' do + let(:expected_uri) { import_url } + let(:expected_hostname) { nil } + end + end end context 'when domain is too long' do diff --git a/spec/migrations/20230125093840_rebalance_partition_id_ci_build_spec.rb b/spec/migrations/20230125093840_rebalance_partition_id_ci_build_spec.rb new file mode 100644 index 00000000000..b983564a2d9 --- /dev/null +++ b/spec/migrations/20230125093840_rebalance_partition_id_ci_build_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe RebalancePartitionIdCiBuild, migration: :gitlab_ci, feature_category: :continuous_integration do + let(:migration) { described_class::MIGRATION } + + context 'when on sass' do + before do + allow(Gitlab).to receive(:com?).and_return(true) + end + + describe '#up' do + it 'schedules background jobs for each batch of ci_builds' do + migrate! + + expect(migration).to have_scheduled_batched_migration( + gitlab_schema: :gitlab_ci, + table_name: :ci_builds, + column_name: :id, + interval: described_class::DELAY_INTERVAL, + batch_size: described_class::BATCH_SIZE, + sub_batch_size: described_class::SUB_BATCH_SIZE + ) + end + end + + describe '#down' do + it 'deletes all batched migration records' do + migrate! + schema_migrate_down! + + expect(migration).not_to have_scheduled_batched_migration + end + end + end + + context 'when on self-managed instance' do + let(:migration) { described_class.new } + + describe '#up' do + it 'does not schedule background job' do + expect(migration).not_to receive(:queue_batched_background_migration) + + migration.up + end + end + + describe '#down' do + it 'does not delete background job' do + expect(migration).not_to receive(:delete_batched_background_migration) + + migration.down + end + end + end +end |