summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-02-10 21:08:12 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2023-02-10 21:08:12 +0000
commitddfa6a1f19f1c6847d30314858f1d0ad21de13f9 (patch)
tree715fd181e594d7c6339d90eb8daaa45c962f2565
parentf27a1b0faf16a83ba9c3f71f660262e368f4509a (diff)
downloadgitlab-ce-ddfa6a1f19f1c6847d30314858f1d0ad21de13f9.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/issue_templates/Experiment Successful Cleanup.md2
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock2
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_bulk_delete.vue38
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js18
-rw-r--r--app/assets/javascripts/pages/projects/edit/index.js2
-rw-r--r--app/assets/javascripts/projects/commit/components/branches_dropdown.vue84
-rw-r--r--app/assets/javascripts/projects/commit/components/form_modal.vue7
-rw-r--r--app/assets/javascripts/projects/prune_objects_button.js23
-rw-r--r--app/assets/javascripts/projects/prune_unreachable_objects_button.vue75
-rw-r--r--app/controllers/projects_controller.rb8
-rw-r--r--app/services/packages/create_event_service.rb26
-rw-r--r--app/views/projects/edit.html.haml3
-rw-r--r--app/workers/concerns/git_garbage_collect_methods.rb7
-rw-r--r--config/events/1675075830_API__PackagesHelpers_pull_package_by_guest.yml26
-rw-r--r--danger/roulette/Dangerfile36
-rw-r--r--db/post_migrate/20230125093840_rebalance_partition_id_ci_build.rb30
-rw-r--r--db/schema_migrations/202301250938401
-rw-r--r--doc/administration/housekeeping.md23
-rw-r--r--doc/user/clusters/agent/install/index.md4
-rw-r--r--doc/user/project/import/github.md8
-rw-r--r--lib/api/helpers/packages_helpers.rb20
-rw-r--r--lib/gitlab/http_connection_adapter.rb2
-rw-r--r--lib/gitlab/octokit/middleware.rb7
-rw-r--r--lib/gitlab/url_blocker.rb4
-rw-r--r--locale/gitlab.pot36
-rw-r--r--spec/controllers/projects_controller_spec.rb25
-rw-r--r--spec/features/merge_request/user_sees_merge_widget_spec.rb6
-rw-r--r--spec/features/projects/commit/cherry_pick_spec.rb8
-rw-r--r--spec/frontend/ci/runner/components/runner_bulk_delete_spec.js32
-rw-r--r--spec/frontend/lib/utils/common_utils_spec.js79
-rw-r--r--spec/frontend/projects/commit/components/branches_dropdown_spec.js133
-rw-r--r--spec/frontend/projects/commit/components/form_modal_spec.js2
-rw-r--r--spec/frontend/projects/prune_unreachable_objects_button_spec.js72
-rw-r--r--spec/lib/api/helpers/packages_helpers_spec.rb28
-rw-r--r--spec/lib/gitlab/http_connection_adapter_spec.rb14
-rw-r--r--spec/lib/gitlab/octokit/middleware_spec.rb33
-rw-r--r--spec/lib/gitlab/url_blocker_spec.rb11
-rw-r--r--spec/migrations/20230125093840_rebalance_partition_id_ci_build_spec.rb58
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.
diff --git a/Gemfile b/Gemfile
index 72569a35dc0..73940aaa7aa 100644
--- a/Gemfile
+++ b/Gemfile
@@ -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