summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock4
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_table.vue7
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue10
-rw-r--r--app/assets/javascripts/repository/components/blob_button_group.vue30
-rw-r--r--app/assets/javascripts/repository/components/blob_content_viewer.vue4
-rw-r--r--app/assets/javascripts/repository/components/delete_blob_modal.vue151
-rw-r--r--app/assets/javascripts/repository/constants.js7
-rw-r--r--app/assets/javascripts/repository/queries/blob_info.query.graphql1
-rw-r--r--app/assets/javascripts/vuex_shared/bindings.js6
-rw-r--r--app/assets/stylesheets/startup/startup-dark.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_helper.scss2
-rw-r--r--app/controllers/projects/blob_controller.rb16
-rw-r--r--app/controllers/projects/tree_controller.rb4
-rw-r--r--app/helpers/clusters_helper.rb6
-rw-r--r--app/helpers/search_helper.rb15
-rw-r--r--app/models/award_emoji.rb14
-rw-r--r--app/models/issue.rb5
-rw-r--r--app/models/merge_request/cleanup_schedule.rb55
-rw-r--r--app/models/project.rb1
-rw-r--r--app/services/projects/update_service.rb2
-rw-r--r--app/views/admin/application_settings/_usage.html.haml4
-rw-r--r--app/views/admin/dev_ops_report/_callout.html.haml2
-rw-r--r--app/views/projects/_invite_members_empty_project.html.haml (renamed from app/views/projects/_invite_members.html.haml)1
-rw-r--r--app/views/projects/empty.html.haml2
-rw-r--r--app/views/search/results/_issuable.html.haml33
-rw-r--r--app/workers/merge_request_cleanup_refs_worker.rb56
-rw-r--r--app/workers/schedule_merge_request_cleanup_refs_worker.rb13
-rw-r--r--config/feature_flags/development/merge_request_discussion_cache.yml2
-rw-r--r--config/feature_flags/development/merge_request_refs_cleanup.yml2
-rw-r--r--config/feature_flags/development/search_sort_issues_by_popularity.yml8
-rw-r--r--db/migrate/20210705124128_add_project_settings_previous_default_branch.rb20
-rw-r--r--db/migrate/20210707095545_add_status_to_merge_request_cleanup_schedules.rb25
-rw-r--r--db/migrate/20210707173645_add_project_settings_previous_default_branch_text_limit.rb15
-rw-r--r--db/migrate/20210708063032_add_failed_count_to_merge_request_cleanup_schedules.rb9
-rw-r--r--db/migrate/20210713070842_update_merge_request_cleanup_schedules_scheduled_at_index.rb20
-rw-r--r--db/post_migrate/20210706115312_add_upvotes_count_index_to_issues.rb17
-rw-r--r--db/schema_migrations/202107051241281
-rw-r--r--db/schema_migrations/202107061153121
-rw-r--r--db/schema_migrations/202107070955451
-rw-r--r--db/schema_migrations/202107071736451
-rw-r--r--db/schema_migrations/202107080630321
-rw-r--r--db/schema_migrations/202107130708421
-rw-r--r--db/structure.sql12
-rw-r--r--doc/ci/pipelines/img/coverage_check_approval_rule_14_1.pngbin0 -> 116394 bytes
-rw-r--r--doc/ci/pipelines/settings.md20
-rw-r--r--doc/development/fe_guide/vuex.md55
-rw-r--r--doc/development/usage_ping/dictionary.md2
-rw-r--r--doc/user/admin_area/analytics/dev_ops_report.md28
-rw-r--r--doc/user/compliance/compliance_dashboard/index.md29
-rw-r--r--doc/user/group/import/img/bulk_imports_v13_8.pngbin22574 -> 0 bytes
-rw-r--r--doc/user/group/import/img/bulk_imports_v14_1.pngbin0 -> 24726 bytes
-rw-r--r--doc/user/group/import/img/import_panel_v13_8.pngbin39125 -> 0 bytes
-rw-r--r--doc/user/group/import/img/import_panel_v14_1.pngbin0 -> 42789 bytes
-rw-r--r--doc/user/group/import/index.md4
-rw-r--r--doc/user/group/settings/img/import_panel_v13_4.pngbin23373 -> 0 bytes
-rw-r--r--doc/user/group/settings/img/import_panel_v14_1.pngbin0 -> 42789 bytes
-rw-r--r--doc/user/group/settings/import_export.md4
-rw-r--r--doc/user/project/merge_requests/approvals/index.md1
-rw-r--r--doc/user/project/repository/branches/default.md14
-rw-r--r--lib/extracts_path.rb46
-rw-r--r--lib/gitlab/search/sort_options.rb4
-rw-r--r--lib/gitlab/search_results.rb32
-rw-r--r--locale/gitlab.pot61
-rw-r--r--spec/controllers/projects/blob_controller_spec.rb17
-rw-r--r--spec/controllers/projects/tree_controller_spec.rb19
-rw-r--r--spec/factories/merge_request_cleanup_schedules.rb15
-rw-r--r--spec/fixtures/lib/generators/gitlab/usage_metric_definition_generator/sample_metric_with_ee.yml2
-rw-r--r--spec/fixtures/lib/generators/gitlab/usage_metric_definition_generator/sample_metric_with_name_suggestions.yml2
-rw-r--r--spec/frontend/repository/components/blob_button_group_spec.js23
-rw-r--r--spec/frontend/repository/components/blob_content_viewer_spec.js37
-rw-r--r--spec/frontend/repository/components/delete_blob_modal_spec.js130
-rw-r--r--spec/frontend/vuex_shared/bindings_spec.js10
-rw-r--r--spec/helpers/clusters_helper_spec.rb7
-rw-r--r--spec/lib/extracts_path_spec.rb72
-rw-r--r--spec/lib/gitlab/search_results_spec.rb8
-rw-r--r--spec/migrations/add_upvotes_count_index_to_issues_spec.rb22
-rw-r--r--spec/models/merge_request/cleanup_schedule_spec.rb133
-rw-r--r--spec/requests/api/project_attributes.yml1
-rw-r--r--spec/services/projects/update_service_spec.rb21
-rw-r--r--spec/support/shared_examples/lib/gitlab/search_results_sorted_shared_examples.rb18
-rw-r--r--spec/views/projects/empty.html.haml_spec.rb1
-rw-r--r--spec/workers/every_sidekiq_worker_spec.rb1
-rw-r--r--spec/workers/merge_request_cleanup_refs_worker_spec.rb89
-rw-r--r--spec/workers/schedule_merge_request_cleanup_refs_worker_spec.rb25
85 files changed, 1315 insertions, 229 deletions
diff --git a/Gemfile b/Gemfile
index fc36cbc5cc7..31ff4d82df2 100644
--- a/Gemfile
+++ b/Gemfile
@@ -295,7 +295,7 @@ gem 'gon', '~> 6.4.0'
gem 'request_store', '~> 1.5'
gem 'base32', '~> 0.3.0'
-gem 'gitlab-license', '~> 1.5'
+gem 'gitlab-license', '~> 2.0'
# Protect against bruteforcing
gem 'rack-attack', '~> 6.3.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index bcca2d42006..61c01f740c8 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -490,7 +490,7 @@ GEM
opentracing (~> 0.4)
pg_query (~> 2.1)
redis (> 3.0.0, < 5.0.0)
- gitlab-license (1.5.0)
+ gitlab-license (2.0.0)
gitlab-mail_room (0.0.9)
gitlab-markup (1.7.1)
gitlab-net-dns (0.9.1)
@@ -1489,7 +1489,7 @@ DEPENDENCIES
gitlab-experiment (~> 0.6.1)
gitlab-fog-azure-rm (~> 1.1.1)
gitlab-labkit (~> 0.20.0)
- gitlab-license (~> 1.5)
+ gitlab-license (~> 2.0)
gitlab-mail_room (~> 0.0.9)
gitlab-markup (~> 1.7.1)
gitlab-net-dns (~> 0.9.1)
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
index 3daa5eebcb6..cb7e3ef9632 100644
--- a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
@@ -227,7 +227,12 @@ export default {
</template>
</gl-sprintf>
</span>
- <gl-search-box-by-click class="gl-ml-auto" @submit="filter = $event" @clear="filter = ''" />
+ <gl-search-box-by-click
+ class="gl-ml-auto"
+ :placeholder="s__('BulkImport|Filter by source group')"
+ @submit="filter = $event"
+ @clear="filter = ''"
+ />
</div>
<gl-loading-icon v-if="$apollo.loading" size="md" class="gl-mt-5" />
<template v-else>
diff --git a/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue b/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue
index 1c4413bef71..0b0560f63c1 100644
--- a/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue
+++ b/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue
@@ -225,11 +225,21 @@ export default {
{
name: 'success',
data: this.mergeLabelsAndValues(labels, success),
+ areaStyle: {
+ color: this.$options.successColor,
+ },
+ lineStyle: {
+ color: this.$options.successColor,
+ },
+ itemStyle: {
+ color: this.$options.successColor,
+ },
},
],
};
},
},
+ successColor: '#608b2f',
chartContainerHeight: CHART_CONTAINER_HEIGHT,
timesChartOptions: {
height: INNER_CHART_HEIGHT,
diff --git a/app/assets/javascripts/repository/components/blob_button_group.vue b/app/assets/javascripts/repository/components/blob_button_group.vue
index 424dc4529ff..273825b996a 100644
--- a/app/assets/javascripts/repository/components/blob_button_group.vue
+++ b/app/assets/javascripts/repository/components/blob_button_group.vue
@@ -3,6 +3,7 @@ import { GlButtonGroup, GlButton, GlModalDirective } from '@gitlab/ui';
import { uniqueId } from 'lodash';
import { sprintf, __ } from '~/locale';
import getRefMixin from '../mixins/get_ref';
+import DeleteBlobModal from './delete_blob_modal.vue';
import UploadBlobModal from './upload_blob_modal.vue';
export default {
@@ -15,6 +16,7 @@ export default {
GlButtonGroup,
GlButton,
UploadBlobModal,
+ DeleteBlobModal,
},
directives: {
GlModal: GlModalDirective,
@@ -41,10 +43,18 @@ export default {
type: String,
required: true,
},
+ deletePath: {
+ type: String,
+ required: true,
+ },
canPushCode: {
type: Boolean,
required: true,
},
+ emptyRepo: {
+ type: Boolean,
+ required: true,
+ },
},
computed: {
replaceModalId() {
@@ -53,6 +63,12 @@ export default {
replaceModalTitle() {
return sprintf(__('Replace %{name}'), { name: this.name });
},
+ deleteModalId() {
+ return uniqueId('delete-modal');
+ },
+ deleteModalTitle() {
+ return sprintf(__('Delete %{name}'), { name: this.name });
+ },
},
};
</script>
@@ -63,7 +79,9 @@ export default {
<gl-button v-gl-modal="replaceModalId">
{{ $options.i18n.replace }}
</gl-button>
- <gl-button>{{ $options.i18n.delete }}</gl-button>
+ <gl-button v-gl-modal="deleteModalId">
+ {{ $options.i18n.delete }}
+ </gl-button>
</gl-button-group>
<upload-blob-modal
:modal-id="replaceModalId"
@@ -76,5 +94,15 @@ export default {
:replace-path="replacePath"
:primary-btn-text="$options.i18n.replacePrimaryBtnText"
/>
+ <delete-blob-modal
+ :modal-id="deleteModalId"
+ :modal-title="deleteModalTitle"
+ :delete-path="deletePath"
+ :commit-message="deleteModalTitle"
+ :target-branch="targetBranch || ref"
+ :original-branch="originalBranch || ref"
+ :can-push-code="canPushCode"
+ :empty-repo="emptyRepo"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue
index c3876a77ec4..09ac60c94c7 100644
--- a/app/assets/javascripts/repository/components/blob_content_viewer.vue
+++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue
@@ -69,6 +69,7 @@ export default {
pushCode: false,
},
repository: {
+ empty: true,
blobs: {
nodes: [
{
@@ -92,6 +93,7 @@ export default {
forkPath: '',
simpleViewer: {},
richViewer: null,
+ webPath: '',
},
],
},
@@ -174,7 +176,9 @@ export default {
:path="path"
:name="blobInfo.name"
:replace-path="blobInfo.replacePath"
+ :delete-path="blobInfo.webPath"
:can-push-code="project.userPermissions.pushCode"
+ :empty-repo="project.repository.empty"
/>
</template>
</blob-header>
diff --git a/app/assets/javascripts/repository/components/delete_blob_modal.vue b/app/assets/javascripts/repository/components/delete_blob_modal.vue
new file mode 100644
index 00000000000..6599d99d7bd
--- /dev/null
+++ b/app/assets/javascripts/repository/components/delete_blob_modal.vue
@@ -0,0 +1,151 @@
+<script>
+import { GlModal, GlFormGroup, GlFormInput, GlFormTextarea, GlToggle } from '@gitlab/ui';
+import csrf from '~/lib/utils/csrf';
+import { __ } from '~/locale';
+import {
+ SECONDARY_OPTIONS_TEXT,
+ COMMIT_LABEL,
+ TARGET_BRANCH_LABEL,
+ TOGGLE_CREATE_MR_LABEL,
+} from '../constants';
+
+export default {
+ csrf,
+ components: {
+ GlModal,
+ GlFormGroup,
+ GlFormInput,
+ GlFormTextarea,
+ GlToggle,
+ },
+ i18n: {
+ PRIMARY_OPTIONS_TEXT: __('Delete file'),
+ SECONDARY_OPTIONS_TEXT,
+ COMMIT_LABEL,
+ TARGET_BRANCH_LABEL,
+ TOGGLE_CREATE_MR_LABEL,
+ },
+ props: {
+ modalId: {
+ type: String,
+ required: true,
+ },
+ modalTitle: {
+ type: String,
+ required: true,
+ },
+ deletePath: {
+ type: String,
+ required: true,
+ },
+ commitMessage: {
+ type: String,
+ required: true,
+ },
+ targetBranch: {
+ type: String,
+ required: true,
+ },
+ originalBranch: {
+ type: String,
+ required: true,
+ },
+ canPushCode: {
+ type: Boolean,
+ required: true,
+ },
+ emptyRepo: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ loading: false,
+ commit: this.commitMessage,
+ target: this.targetBranch,
+ createNewMr: true,
+ error: '',
+ };
+ },
+ computed: {
+ primaryOptions() {
+ return {
+ text: this.$options.i18n.PRIMARY_OPTIONS_TEXT,
+ attributes: [
+ {
+ variant: 'danger',
+ loading: this.loading,
+ disabled: !this.formCompleted || this.loading,
+ },
+ ],
+ };
+ },
+ cancelOptions() {
+ return {
+ text: this.$options.i18n.SECONDARY_OPTIONS_TEXT,
+ attributes: [
+ {
+ disabled: this.loading,
+ },
+ ],
+ };
+ },
+ showCreateNewMrToggle() {
+ return this.canPushCode && this.target !== this.originalBranch;
+ },
+ formCompleted() {
+ return this.commit && this.target;
+ },
+ },
+ methods: {
+ submitForm(e) {
+ e.preventDefault(); // Prevent modal from closing
+ this.loading = true;
+ this.$refs.form.submit();
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-modal
+ :modal-id="modalId"
+ :title="modalTitle"
+ :action-primary="primaryOptions"
+ :action-cancel="cancelOptions"
+ @primary="submitForm"
+ >
+ <form ref="form" :action="deletePath" method="post">
+ <input type="hidden" name="_method" value="delete" />
+ <input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
+ <template v-if="emptyRepo">
+ <!-- Once "empty_repo_upload_experiment" is made available, will need to add class 'js-branch-name'
+ Follow-up issue: https://gitlab.com/gitlab-org/gitlab/-/issues/335721 -->
+ <input type="hidden" name="branch_name" :value="originalBranch" />
+ </template>
+ <template v-else>
+ <input type="hidden" name="original_branch" :value="originalBranch" />
+ <!-- Once "push to branch" permission is made available, will need to add to conditional
+ Follow-up issue: https://gitlab.com/gitlab-org/gitlab/-/issues/335462 -->
+ <input v-if="createNewMr" type="hidden" name="create_merge_request" value="1" />
+ <gl-form-group :label="$options.i18n.COMMIT_LABEL" label-for="commit_message">
+ <gl-form-textarea v-model="commit" name="commit_message" :disabled="loading" />
+ </gl-form-group>
+ <gl-form-group
+ v-if="canPushCode"
+ :label="$options.i18n.TARGET_BRANCH_LABEL"
+ label-for="branch_name"
+ >
+ <gl-form-input v-model="target" :disabled="loading" name="branch_name" />
+ </gl-form-group>
+ <gl-toggle
+ v-if="showCreateNewMrToggle"
+ v-model="createNewMr"
+ :disabled="loading"
+ :label="$options.i18n.TOGGLE_CREATE_MR_LABEL"
+ />
+ </template>
+ </form>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/repository/constants.js b/app/assets/javascripts/repository/constants.js
index 22349261d3c..2d2faa8d9f3 100644
--- a/app/assets/javascripts/repository/constants.js
+++ b/app/assets/javascripts/repository/constants.js
@@ -1,3 +1,10 @@
+import { __ } from '~/locale';
+
export const TREE_PAGE_LIMIT = 1000; // the maximum amount of items per page
export const TREE_PAGE_SIZE = 100; // the amount of items to be fetched per (batch) request
export const TREE_INITIAL_FETCH_COUNT = TREE_PAGE_LIMIT / TREE_PAGE_SIZE; // the amount of (batch) requests to make
+
+export const SECONDARY_OPTIONS_TEXT = __('Cancel');
+export const COMMIT_LABEL = __('Commit message');
+export const TARGET_BRANCH_LABEL = __('Target branch');
+export const TOGGLE_CREATE_MR_LABEL = __('Start a new merge request with these changes');
diff --git a/app/assets/javascripts/repository/queries/blob_info.query.graphql b/app/assets/javascripts/repository/queries/blob_info.query.graphql
index 1889f2269f5..a8f263941e2 100644
--- a/app/assets/javascripts/repository/queries/blob_info.query.graphql
+++ b/app/assets/javascripts/repository/queries/blob_info.query.graphql
@@ -4,6 +4,7 @@ query getBlobInfo($projectPath: ID!, $filePath: String!) {
pushCode
}
repository {
+ empty
blobs(paths: [$filePath]) {
nodes {
webPath
diff --git a/app/assets/javascripts/vuex_shared/bindings.js b/app/assets/javascripts/vuex_shared/bindings.js
index 741690886b7..bc3741a3880 100644
--- a/app/assets/javascripts/vuex_shared/bindings.js
+++ b/app/assets/javascripts/vuex_shared/bindings.js
@@ -6,7 +6,7 @@
* @param {string} list[].getter - the name of the getter, leave it empty to not use a getter
* @param {string} list[].updateFn - the name of the action, leave it empty to use the default action
* @param {string} defaultUpdateFn - the default function to dispatch
- * @param {string} root - the key of the state where to search fo they keys described in list
+ * @param {string|function} root - the key of the state where to search for the keys described in list
* @returns {Object} a dictionary with all the computed properties generated
*/
export const mapComputed = (list, defaultUpdateFn, root) => {
@@ -21,6 +21,10 @@ export const mapComputed = (list, defaultUpdateFn, root) => {
if (getter) {
return this.$store.getters[getter];
} else if (root) {
+ if (typeof root === 'function') {
+ return root(this.$store.state)[key];
+ }
+
return this.$store.state[root][key];
}
return this.$store.state[key];
diff --git a/app/assets/stylesheets/startup/startup-dark.scss b/app/assets/stylesheets/startup/startup-dark.scss
index 79b69e11b35..a497f56f3b8 100644
--- a/app/assets/stylesheets/startup/startup-dark.scss
+++ b/app/assets/stylesheets/startup/startup-dark.scss
@@ -1673,7 +1673,7 @@ body.gl-dark .nav-sidebar .fly-out-top-item a,
body.gl-dark .nav-sidebar .fly-out-top-item.active a,
body.gl-dark .nav-sidebar .fly-out-top-item .fly-out-top-item-container {
background-color: #2f2a6b;
- color: #333;
+ color: var(--black, #333);
}
body.gl-dark .logo-text svg {
fill: var(--gl-text-color);
diff --git a/app/assets/stylesheets/themes/theme_helper.scss b/app/assets/stylesheets/themes/theme_helper.scss
index 45fbd8607fc..a94169ab494 100644
--- a/app/assets/stylesheets/themes/theme_helper.scss
+++ b/app/assets/stylesheets/themes/theme_helper.scss
@@ -185,7 +185,7 @@
&.active a,
.fly-out-top-item-container {
background-color: $purple-900;
- color: $white;
+ color: var(--black, $white);
}
}
}
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index c6c9237292d..08066acb45c 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -23,6 +23,10 @@ class Projects::BlobController < Projects::ApplicationController
# We need to assign the blob vars before `authorize_edit_tree!` so we can
# validate access to a specific ref.
before_action :assign_blob_vars
+
+ # Since BlobController doesn't use assign_ref_vars, we have to call this explicitly
+ before_action :rectify_renamed_default_branch!, only: [:show]
+
before_action :authorize_edit_tree!, only: [:new, :create, :update, :destroy]
before_action :commit, except: [:new, :create]
@@ -140,11 +144,15 @@ class Projects::BlobController < Projects::ApplicationController
end
def commit
- @commit = @repository.commit(@ref)
+ @commit ||= @repository.commit(@ref)
return render_404 unless @commit
end
+ def redirect_renamed_default_branch?
+ action_name == 'show'
+ end
+
def assign_blob_vars
@id = params[:id]
@ref, @path = extract_ref(@id)
@@ -152,6 +160,12 @@ class Projects::BlobController < Projects::ApplicationController
render_404
end
+ def rectify_renamed_default_branch!
+ @commit ||= @repository.commit(@ref)
+
+ super
+ end
+
# rubocop: disable CodeReuse/ActiveRecord
def after_edit_path
from_merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).find_by(iid: params[:from_merge_request_iid])
diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb
index b5cfc3990b2..475c9de2503 100644
--- a/app/controllers/projects/tree_controller.rb
+++ b/app/controllers/projects/tree_controller.rb
@@ -39,6 +39,10 @@ class Projects::TreeController < Projects::ApplicationController
private
+ def redirect_renamed_default_branch?
+ action_name == 'show'
+ end
+
def assign_dir_vars
@branch_name = params[:branch_name]
diff --git a/app/helpers/clusters_helper.rb b/app/helpers/clusters_helper.rb
index 14783882f5e..e9a75babb97 100644
--- a/app/helpers/clusters_helper.rb
+++ b/app/helpers/clusters_helper.rb
@@ -20,7 +20,11 @@ module ClustersHelper
{
default_branch_name: clusterable_project.default_branch,
empty_state_image: image_path('illustrations/clusters_empty.svg'),
- project_path: clusterable_project.full_path
+ project_path: clusterable_project.full_path,
+ agent_docs_url: help_page_path('user/clusters/agent/index'),
+ install_docs_url: help_page_path('administration/clusters/kas'),
+ get_started_docs_url: help_page_path('user/clusters/agent/index', anchor: 'define-a-configuration-repository'),
+ integration_docs_url: help_page_path('user/clusters/agent/index', anchor: 'get-started-with-gitops-and-the-gitlab-agent')
}
end
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index 1cbde1871d4..ec8ed3d6e7f 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -131,7 +131,7 @@ module SearchHelper
end
def search_sort_options
- [
+ options = [
{
title: _('Created date'),
sortable: true,
@@ -149,6 +149,19 @@ module SearchHelper
}
}
]
+
+ if search_service.scope == 'issues' && Feature.enabled?(:search_sort_issues_by_popularity)
+ options << {
+ title: _('Popularity'),
+ sortable: true,
+ sortParam: {
+ asc: 'popularity_asc',
+ desc: 'popularity_desc'
+ }
+ }
+ end
+
+ options
end
private
diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb
index dc37d73df85..c8f6b9aaedb 100644
--- a/app/models/award_emoji.rb
+++ b/app/models/award_emoji.rb
@@ -27,9 +27,6 @@ class AwardEmoji < ApplicationRecord
after_save :expire_cache
after_destroy :expire_cache
- after_save :update_awardable_upvotes_count
- after_destroy :update_awardable_upvotes_count
-
class << self
def votes_for_collection(ids, type)
select('name', 'awardable_id', 'COUNT(*) as count')
@@ -66,15 +63,6 @@ class AwardEmoji < ApplicationRecord
def expire_cache
awardable.try(:bump_updated_at)
awardable.try(:expire_etag_cache)
- end
-
- private
-
- def update_awardable_upvotes_count
- return unless upvote? && awardable.has_attribute?(:upvotes_count)
-
- awardable.update_column(:upvotes_count, awardable.upvotes)
+ awardable.try(:update_upvotes_count) if upvote?
end
end
-
-AwardEmoji.prepend_mod_with('AwardEmoji')
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 3b236620ed6..7926c4be489 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -520,6 +520,11 @@ class Issue < ApplicationRecord
issue_assignees.pluck(:user_id)
end
+ def update_upvotes_count
+ self.lock!
+ self.update_column(:upvotes_count, self.upvotes)
+ end
+
private
def spammable_attribute_changed?
diff --git a/app/models/merge_request/cleanup_schedule.rb b/app/models/merge_request/cleanup_schedule.rb
index 79817269be2..35194b2b318 100644
--- a/app/models/merge_request/cleanup_schedule.rb
+++ b/app/models/merge_request/cleanup_schedule.rb
@@ -1,14 +1,61 @@
# frozen_string_literal: true
class MergeRequest::CleanupSchedule < ApplicationRecord
+ STATUSES = {
+ unstarted: 0,
+ running: 1,
+ completed: 2,
+ failed: 3
+ }.freeze
+
belongs_to :merge_request, inverse_of: :cleanup_schedule
validates :scheduled_at, presence: true
- def self.scheduled_merge_request_ids(limit)
- where('completed_at IS NULL AND scheduled_at <= NOW()')
+ state_machine :status, initial: :unstarted do
+ state :unstarted, value: STATUSES[:unstarted]
+ state :running, value: STATUSES[:running]
+ state :completed, value: STATUSES[:completed]
+ state :failed, value: STATUSES[:failed]
+
+ event :run do
+ transition unstarted: :running
+ end
+
+ event :retry do
+ transition running: :unstarted
+ end
+
+ event :complete do
+ transition running: :completed
+ end
+
+ event :mark_as_failed do
+ transition running: :failed
+ end
+
+ before_transition to: [:completed] do |cleanup_schedule, _transition|
+ cleanup_schedule.completed_at = Time.current
+ end
+
+ before_transition from: :running, to: [:unstarted, :failed] do |cleanup_schedule, _transition|
+ cleanup_schedule.failed_count += 1
+ end
+ end
+
+ scope :scheduled_and_unstarted, -> {
+ where('completed_at IS NULL AND scheduled_at <= NOW() AND status = ?', STATUSES[:unstarted])
.order('scheduled_at DESC')
- .limit(limit)
- .pluck(:merge_request_id)
+ }
+
+ def self.start_next
+ MergeRequest::CleanupSchedule.transaction do
+ cleanup_schedule = scheduled_and_unstarted.lock('FOR UPDATE SKIP LOCKED').first
+
+ next if cleanup_schedule.blank?
+
+ cleanup_schedule.run!
+ cleanup_schedule
+ end
end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index e850494ab27..21d5b083476 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -416,6 +416,7 @@ class Project < ApplicationRecord
prefix: :import, to: :import_state, allow_nil: true
delegate :squash_always?, :squash_never?, :squash_enabled_by_default?, :squash_readonly?, to: :project_setting
delegate :squash_option, to: :project_setting
+ delegate :previous_default_branch, :previous_default_branch=, to: :project_setting
delegate :no_import?, to: :import_state, allow_nil: true
delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :members, to: :team, prefix: true
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index 4351a66351d..d6e7f165d72 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -66,6 +66,8 @@ module Projects
previous_default_branch = project.default_branch
if project.change_head(params[:default_branch])
+ params[:previous_default_branch] = previous_default_branch
+
after_default_branch_change(previous_default_branch)
else
raise ValidationError, s_("UpdateProject|Could not set the default branch")
diff --git a/app/views/admin/application_settings/_usage.html.haml b/app/views/admin/application_settings/_usage.html.haml
index 206d5edbf84..f45e6c5e8e9 100644
--- a/app/views/admin/application_settings/_usage.html.haml
+++ b/app/views/admin/application_settings/_usage.html.haml
@@ -31,8 +31,8 @@
.js-text.d-inline= _('Preview payload')
%pre.service-data-payload-container.js-syntax-highlight.code.highlight.mt-2.d-none{ class: payload_class, data: { endpoint: usage_data_admin_application_settings_path(format: :html) } }
- else
- = _('Service ping is disabled, and cannot be configured through this form.')
- - deactivating_service_ping_path = help_page_path('development/service_ping/index.md', anchor: 'disable-service-ping')
+ = _('Service ping is disabled in your configuration file, and cannot be enabled through this form.')
+ - deactivating_service_ping_path = help_page_path('development/service_ping/index.md', anchor: 'disable-service-ping-using-the-configuration-file')
- deactivating_service_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: deactivating_service_ping_path }
= s_('For more information, see the documentation on %{deactivating_service_ping_link_start}deactivating service ping%{deactivating_service_ping_link_end}.').html_safe % { deactivating_service_ping_link_start: deactivating_service_ping_link_start, deactivating_service_ping_link_end: '</a>'.html_safe }
.form-group
diff --git a/app/views/admin/dev_ops_report/_callout.html.haml b/app/views/admin/dev_ops_report/_callout.html.haml
index f313865478d..2b4c258a00c 100644
--- a/app/views/admin/dev_ops_report/_callout.html.haml
+++ b/app/views/admin/dev_ops_report/_callout.html.haml
@@ -8,6 +8,6 @@
%h4
= _('Introducing Your DevOps Report')
%p
- = _('Your DevOps Report gives an overview of how you are using GitLab from a feature perspective. View how you compare with other organizations, discover features you are not using, and learn best practices through blog posts and white papers.')
+ = _('Your DevOps Report gives an overview of how you are using GitLab from a feature perspective. Use it to view how you compare with other organizations.')
.svg-container.devops
= custom_icon('dev_ops_report_overview')
diff --git a/app/views/projects/_invite_members.html.haml b/app/views/projects/_invite_members_empty_project.html.haml
index fc292da6fcf..ee2215b0fbb 100644
--- a/app/views/projects/_invite_members.html.haml
+++ b/app/views/projects/_invite_members_empty_project.html.haml
@@ -6,6 +6,7 @@
.js-invite-members-trigger{ data: { variant: 'confirm',
classes: 'gl-mb-8 gl-xs-w-full',
display_text: s_('InviteMember|Invite members'),
+ trigger_source: 'project-empty-page',
event: 'click_button',
label: 'invite_members_empty_project' } }
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 027b81d6c68..0fda74a3be5 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -7,7 +7,7 @@
= render "home_panel"
= render "archived_notice", project: @project
-= render "invite_members" if can_import_members?
+= render 'invite_members_empty_project' if can_import_members?
%h4.gl-mt-0.gl-mb-3
= _('The repository for this project is empty')
diff --git a/app/views/search/results/_issuable.html.haml b/app/views/search/results/_issuable.html.haml
index da0adba88db..551f5c048bc 100644
--- a/app/views/search/results/_issuable.html.haml
+++ b/app/views/search/results/_issuable.html.haml
@@ -1,14 +1,19 @@
-%div{ class: 'search-result-row gl-pb-3! gl-mt-5 gl-mb-0!' }
- %span.gl-display-flex.gl-align-items-center
- %span.badge.badge-pill.gl-badge.sm{ class: "badge-#{issuable_state_to_badge_class(issuable)}" }= issuable_state_text(issuable)
- = sprite_icon('eye-slash', css_class: 'gl-text-gray-500 gl-ml-2') if issuable.respond_to?(:confidential?) && issuable.confidential?
- = link_to issuable_path(issuable), data: { track_event: 'click_text', track_label: "#{issuable.class.name.downcase}_title", track_property: 'search_result' }, class: 'gl-w-full' do
- %span.term.str-truncated.gl-font-weight-bold.gl-ml-2= issuable.title
- .gl-text-gray-500.gl-my-3
- = issuable_project_reference(issuable)
- &middot;
- = sprintf(s_('created %{issuable_created} by %{author}'), { issuable_created: time_ago_with_tooltip(issuable.created_at, placement: 'bottom'), author: link_to_member(@project, issuable.author, avatar: false) }).html_safe
- &middot;
- = sprintf(s_('updated %{time_ago}'), { time_ago: time_ago_with_tooltip(issuable.updated_at, placement: 'bottom') }).html_safe
- .description.term.col-sm-10.gl-px-0
- = highlight_and_truncate_issuable(issuable, @search_term, @search_highlight)
+%div{ class: 'search-result-row gl-display-flex gl-sm-flex-direction-row gl-flex-direction-column gl-align-items-center gl-pb-3! gl-mt-5 gl-mb-0!' }
+ .col-sm-9
+ %span.gl-display-flex.gl-align-items-center
+ %span.badge.badge-pill.gl-badge.sm{ class: "badge-#{issuable_state_to_badge_class(issuable)}" }= issuable_state_text(issuable)
+ = sprite_icon('eye-slash', css_class: 'gl-text-gray-500 gl-ml-2') if issuable.respond_to?(:confidential?) && issuable.confidential?
+ = link_to issuable_path(issuable), data: { track_event: 'click_text', track_label: "#{issuable.class.name.downcase}_title", track_property: 'search_result' }, class: 'gl-w-full' do
+ %span.term.str-truncated.gl-font-weight-bold.gl-ml-2= issuable.title
+ .gl-text-gray-500.gl-my-3
+ = issuable_project_reference(issuable)
+ &middot;
+ = sprintf(s_('created %{issuable_created} by %{author}'), { issuable_created: time_ago_with_tooltip(issuable.created_at, placement: 'bottom'), author: link_to_member(@project, issuable.author, avatar: false) }).html_safe
+ .description.term.gl-px-0
+ = highlight_and_truncate_issuable(issuable, @search_term, @search_highlight)
+ .col-sm-3.gl-mt-3.gl-sm-mt-0.gl-text-right
+ - if Feature.enabled?(:search_sort_issues_by_popularity) && issuable.respond_to?(:upvotes_count) && issuable.upvotes_count > 0
+ %li.issuable-upvotes.gl-list-style-none.has-tooltip{ title: _('Upvotes') }
+ = sprite_icon('thumb-up', css_class: "gl-vertical-align-middle")
+ = issuable.upvotes_count
+ %span.gl-text-gray-500= sprintf(s_('updated %{time_ago}'), { time_ago: time_ago_with_tooltip(issuable.updated_at, placement: 'bottom') }).html_safe
diff --git a/app/workers/merge_request_cleanup_refs_worker.rb b/app/workers/merge_request_cleanup_refs_worker.rb
index 162c6dc2a88..408d070d56f 100644
--- a/app/workers/merge_request_cleanup_refs_worker.rb
+++ b/app/workers/merge_request_cleanup_refs_worker.rb
@@ -2,6 +2,8 @@
class MergeRequestCleanupRefsWorker
include ApplicationWorker
+ include LimitedCapacity::Worker
+ include Gitlab::Utils::StrongMemoize
sidekiq_options retry: 3
@@ -9,20 +11,60 @@ class MergeRequestCleanupRefsWorker
tags :exclude_from_kubernetes
idempotent!
- def perform(merge_request_id)
- return unless Feature.enabled?(:merge_request_refs_cleanup, default_enabled: false)
+ # Hard-coded to 4 for now. Will be configurable later on via application settings.
+ # This means, there can only be 4 jobs running at the same time at maximum.
+ MAX_RUNNING_JOBS = 4
+ FAILURE_THRESHOLD = 3
- merge_request = MergeRequest.find_by_id(merge_request_id)
+ def perform_work
+ return unless Feature.enabled?(:merge_request_refs_cleanup, default_enabled: false)
unless merge_request
- logger.error("Failed to find merge request with ID: #{merge_request_id}")
+ logger.error('No existing merge request to be cleaned up.')
return
end
- result = ::MergeRequests::CleanupRefsService.new(merge_request).execute
+ log_extra_metadata_on_done(:merge_request_id, merge_request.id)
+
+ result = MergeRequests::CleanupRefsService.new(merge_request).execute
+
+ if result[:status] == :success
+ merge_request_cleanup_schedule.complete!
+ else
+ if merge_request_cleanup_schedule.failed_count < FAILURE_THRESHOLD
+ merge_request_cleanup_schedule.retry!
+ else
+ merge_request_cleanup_schedule.mark_as_failed!
+ end
+
+ log_extra_metadata_on_done(:message, result[:message])
+ end
+
+ log_extra_metadata_on_done(:status, merge_request_cleanup_schedule.status)
+ end
+
+ def remaining_work_count
+ MergeRequest::CleanupSchedule
+ .scheduled_and_unstarted
+ .limit(max_running_jobs)
+ .count
+ end
+
+ def max_running_jobs
+ MAX_RUNNING_JOBS
+ end
+
+ private
- return if result[:status] == :success
+ def merge_request
+ strong_memoize(:merge_request) do
+ merge_request_cleanup_schedule&.merge_request
+ end
+ end
- logger.error("Failed cleanup refs of merge request (#{merge_request_id}): #{result[:message]}")
+ def merge_request_cleanup_schedule
+ strong_memoize(:merge_request_cleanup_schedule) do
+ MergeRequest::CleanupSchedule.start_next
+ end
end
end
diff --git a/app/workers/schedule_merge_request_cleanup_refs_worker.rb b/app/workers/schedule_merge_request_cleanup_refs_worker.rb
index b5ea5298879..40a773ca58f 100644
--- a/app/workers/schedule_merge_request_cleanup_refs_worker.rb
+++ b/app/workers/schedule_merge_request_cleanup_refs_worker.rb
@@ -10,21 +10,10 @@ class ScheduleMergeRequestCleanupRefsWorker
tags :exclude_from_kubernetes
idempotent!
- # Based on existing data, MergeRequestCleanupRefsWorker can run 3 jobs per
- # second. This means that 180 jobs can be performed but since there are some
- # spikes from time time, it's better to give it some allowance.
- LIMIT = 180
- DELAY = 10.seconds
- BATCH_SIZE = 30
-
def perform
return if Gitlab::Database.read_only?
return unless Feature.enabled?(:merge_request_refs_cleanup, default_enabled: false)
- ids = MergeRequest::CleanupSchedule.scheduled_merge_request_ids(LIMIT).map { |id| [id] }
-
- MergeRequestCleanupRefsWorker.bulk_perform_in(DELAY, ids, batch_size: BATCH_SIZE) # rubocop:disable Scalability/BulkPerformWithContext
-
- log_extra_metadata_on_done(:merge_requests_count, ids.size)
+ MergeRequestCleanupRefsWorker.perform_with_capacity
end
end
diff --git a/config/feature_flags/development/merge_request_discussion_cache.yml b/config/feature_flags/development/merge_request_discussion_cache.yml
index 4dcdbebabc4..e90887fc2b3 100644
--- a/config/feature_flags/development/merge_request_discussion_cache.yml
+++ b/config/feature_flags/development/merge_request_discussion_cache.yml
@@ -1,7 +1,7 @@
---
name: merge_request_discussion_cache
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64688
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/332967
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/335799
milestone: '14.1'
type: development
group: group::code review
diff --git a/config/feature_flags/development/merge_request_refs_cleanup.yml b/config/feature_flags/development/merge_request_refs_cleanup.yml
index 79ea3c8b7a7..7df06ccc52f 100644
--- a/config/feature_flags/development/merge_request_refs_cleanup.yml
+++ b/config/feature_flags/development/merge_request_refs_cleanup.yml
@@ -1,7 +1,7 @@
---
name: merge_request_refs_cleanup
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/51558
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/296874
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/336070
milestone: '13.8'
type: development
group: group::code review
diff --git a/config/feature_flags/development/search_sort_issues_by_popularity.yml b/config/feature_flags/development/search_sort_issues_by_popularity.yml
new file mode 100644
index 00000000000..64885f00792
--- /dev/null
+++ b/config/feature_flags/development/search_sort_issues_by_popularity.yml
@@ -0,0 +1,8 @@
+---
+name: search_sort_issues_by_popularity
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65231
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/334974
+milestone: '14.1'
+type: development
+group: group::global search
+default_enabled: false
diff --git a/db/migrate/20210705124128_add_project_settings_previous_default_branch.rb b/db/migrate/20210705124128_add_project_settings_previous_default_branch.rb
new file mode 100644
index 00000000000..e54d762fa75
--- /dev/null
+++ b/db/migrate/20210705124128_add_project_settings_previous_default_branch.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class AddProjectSettingsPreviousDefaultBranch < ActiveRecord::Migration[6.1]
+ include Gitlab::Database::MigrationHelpers
+
+ # rubocop:disable Migration/AddLimitToTextColumns
+ # limit is added in 20210707173645_add_project_settings_previous_default_branch_text_limit
+ def up
+ with_lock_retries do
+ add_column :project_settings, :previous_default_branch, :text
+ end
+ end
+ # rubocop:enable Migration/AddLimitToTextColumns
+
+ def down
+ with_lock_retries do
+ remove_column :project_settings, :previous_default_branch
+ end
+ end
+end
diff --git a/db/migrate/20210707095545_add_status_to_merge_request_cleanup_schedules.rb b/db/migrate/20210707095545_add_status_to_merge_request_cleanup_schedules.rb
new file mode 100644
index 00000000000..597e274cda2
--- /dev/null
+++ b/db/migrate/20210707095545_add_status_to_merge_request_cleanup_schedules.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class AddStatusToMergeRequestCleanupSchedules < ActiveRecord::Migration[6.1]
+ include Gitlab::Database::MigrationHelpers
+
+ INDEX_NAME = 'index_merge_request_cleanup_schedules_on_status'
+
+ disable_ddl_transaction!
+
+ def up
+ unless column_exists?(:merge_request_cleanup_schedules, :status)
+ add_column(:merge_request_cleanup_schedules, :status, :integer, limit: 2, default: 0, null: false)
+ end
+
+ add_concurrent_index(:merge_request_cleanup_schedules, :status, name: INDEX_NAME)
+ end
+
+ def down
+ remove_concurrent_index_by_name(:merge_request_cleanup_schedules, INDEX_NAME)
+
+ if column_exists?(:merge_request_cleanup_schedules, :status)
+ remove_column(:merge_request_cleanup_schedules, :status)
+ end
+ end
+end
diff --git a/db/migrate/20210707173645_add_project_settings_previous_default_branch_text_limit.rb b/db/migrate/20210707173645_add_project_settings_previous_default_branch_text_limit.rb
new file mode 100644
index 00000000000..a6a83b00234
--- /dev/null
+++ b/db/migrate/20210707173645_add_project_settings_previous_default_branch_text_limit.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class AddProjectSettingsPreviousDefaultBranchTextLimit < ActiveRecord::Migration[6.1]
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ def up
+ add_text_limit :project_settings, :previous_default_branch, 4096
+ end
+
+ def down
+ remove_text_limit :project_settings, :previous_default_branch
+ end
+end
diff --git a/db/migrate/20210708063032_add_failed_count_to_merge_request_cleanup_schedules.rb b/db/migrate/20210708063032_add_failed_count_to_merge_request_cleanup_schedules.rb
new file mode 100644
index 00000000000..f613856a18c
--- /dev/null
+++ b/db/migrate/20210708063032_add_failed_count_to_merge_request_cleanup_schedules.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddFailedCountToMergeRequestCleanupSchedules < ActiveRecord::Migration[6.1]
+ include Gitlab::Database::MigrationHelpers
+
+ def change
+ add_column :merge_request_cleanup_schedules, :failed_count, :integer, default: 0, null: false
+ end
+end
diff --git a/db/migrate/20210713070842_update_merge_request_cleanup_schedules_scheduled_at_index.rb b/db/migrate/20210713070842_update_merge_request_cleanup_schedules_scheduled_at_index.rb
new file mode 100644
index 00000000000..a19d15d80a0
--- /dev/null
+++ b/db/migrate/20210713070842_update_merge_request_cleanup_schedules_scheduled_at_index.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class UpdateMergeRequestCleanupSchedulesScheduledAtIndex < ActiveRecord::Migration[6.1]
+ include Gitlab::Database::MigrationHelpers
+
+ INDEX_NAME = 'index_mr_cleanup_schedules_timestamps_status'
+ OLD_INDEX_NAME = 'index_mr_cleanup_schedules_timestamps'
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index(:merge_request_cleanup_schedules, :scheduled_at, where: 'completed_at IS NULL AND status = 0', name: INDEX_NAME)
+ remove_concurrent_index_by_name(:merge_request_cleanup_schedules, OLD_INDEX_NAME)
+ end
+
+ def down
+ remove_concurrent_index_by_name(:merge_request_cleanup_schedules, INDEX_NAME)
+ add_concurrent_index(:merge_request_cleanup_schedules, :scheduled_at, where: 'completed_at IS NULL', name: OLD_INDEX_NAME)
+ end
+end
diff --git a/db/post_migrate/20210706115312_add_upvotes_count_index_to_issues.rb b/db/post_migrate/20210706115312_add_upvotes_count_index_to_issues.rb
new file mode 100644
index 00000000000..65ec43930ea
--- /dev/null
+++ b/db/post_migrate/20210706115312_add_upvotes_count_index_to_issues.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddUpvotesCountIndexToIssues < ActiveRecord::Migration[6.1]
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ INDEX_NAME = 'index_issues_on_project_id_and_upvotes_count'
+
+ def up
+ add_concurrent_index :issues, [:project_id, :upvotes_count], name: INDEX_NAME
+ end
+
+ def down
+ remove_concurrent_index :issues, [:project_id, :upvotes_count], name: INDEX_NAME
+ end
+end
diff --git a/db/schema_migrations/20210705124128 b/db/schema_migrations/20210705124128
new file mode 100644
index 00000000000..247378331e4
--- /dev/null
+++ b/db/schema_migrations/20210705124128
@@ -0,0 +1 @@
+02aea8fe759614bc3aa751e023aa508963f8183366f6d6f518bbccc2d85ec1a1 \ No newline at end of file
diff --git a/db/schema_migrations/20210706115312 b/db/schema_migrations/20210706115312
new file mode 100644
index 00000000000..a1298418836
--- /dev/null
+++ b/db/schema_migrations/20210706115312
@@ -0,0 +1 @@
+ac150e706b115849aa3802ae7b8e07d983e89eb637c48582c64948cbc7d7163d \ No newline at end of file
diff --git a/db/schema_migrations/20210707095545 b/db/schema_migrations/20210707095545
new file mode 100644
index 00000000000..83255c22622
--- /dev/null
+++ b/db/schema_migrations/20210707095545
@@ -0,0 +1 @@
+98d4deaf0564119c1ee44d76d3a30bff1a0fceb7cab67c5dbef576faef62ddf5 \ No newline at end of file
diff --git a/db/schema_migrations/20210707173645 b/db/schema_migrations/20210707173645
new file mode 100644
index 00000000000..0cc2386b4ef
--- /dev/null
+++ b/db/schema_migrations/20210707173645
@@ -0,0 +1 @@
+e440dac0e14df7309c84e72b98ed6373c712901dc66310a474979e0fce7dc59c \ No newline at end of file
diff --git a/db/schema_migrations/20210708063032 b/db/schema_migrations/20210708063032
new file mode 100644
index 00000000000..9d3271bdd91
--- /dev/null
+++ b/db/schema_migrations/20210708063032
@@ -0,0 +1 @@
+77f6db1d2aeebdefd76c96966da6c9e4ce5da2c92a42f6ac2398b35fa21c680f \ No newline at end of file
diff --git a/db/schema_migrations/20210713070842 b/db/schema_migrations/20210713070842
new file mode 100644
index 00000000000..857dea1627e
--- /dev/null
+++ b/db/schema_migrations/20210713070842
@@ -0,0 +1 @@
+2899d954a199fa52bf6ab4beca5f22dcb9f9f0312e658f1307d1a7355394f1bb \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 3e55db5a0d4..9421cbb5473 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -14711,7 +14711,9 @@ CREATE TABLE merge_request_cleanup_schedules (
scheduled_at timestamp with time zone NOT NULL,
completed_at timestamp with time zone,
created_at timestamp with time zone NOT NULL,
- updated_at timestamp with time zone NOT NULL
+ updated_at timestamp with time zone NOT NULL,
+ status smallint DEFAULT 0 NOT NULL,
+ failed_count integer DEFAULT 0 NOT NULL
);
CREATE SEQUENCE merge_request_cleanup_schedules_merge_request_id_seq
@@ -17082,6 +17084,8 @@ CREATE TABLE project_settings (
prevent_merge_without_jira_issue boolean DEFAULT false NOT NULL,
cve_id_request_enabled boolean DEFAULT true NOT NULL,
mr_default_target_self boolean DEFAULT false NOT NULL,
+ previous_default_branch text,
+ CONSTRAINT check_3a03e7557a CHECK ((char_length(previous_default_branch) <= 4096)),
CONSTRAINT check_bde223416c CHECK ((show_default_award_emojis IS NOT NULL))
);
@@ -23866,6 +23870,8 @@ CREATE UNIQUE INDEX index_issues_on_project_id_and_external_key ON issues USING
CREATE UNIQUE INDEX index_issues_on_project_id_and_iid ON issues USING btree (project_id, iid);
+CREATE INDEX index_issues_on_project_id_and_upvotes_count ON issues USING btree (project_id, upvotes_count);
+
CREATE INDEX index_issues_on_promoted_to_epic_id ON issues USING btree (promoted_to_epic_id) WHERE (promoted_to_epic_id IS NOT NULL);
CREATE INDEX index_issues_on_sprint_id ON issues USING btree (sprint_id);
@@ -23988,6 +23994,8 @@ CREATE INDEX index_merge_request_blocks_on_blocked_merge_request_id ON merge_req
CREATE UNIQUE INDEX index_merge_request_cleanup_schedules_on_merge_request_id ON merge_request_cleanup_schedules USING btree (merge_request_id);
+CREATE INDEX index_merge_request_cleanup_schedules_on_status ON merge_request_cleanup_schedules USING btree (status);
+
CREATE UNIQUE INDEX index_merge_request_diff_commit_users_on_name_and_email ON merge_request_diff_commit_users USING btree (name, email);
CREATE INDEX index_merge_request_diff_commits_on_sha ON merge_request_diff_commits USING btree (sha);
@@ -24120,7 +24128,7 @@ CREATE INDEX index_mirror_data_non_scheduled_or_started ON project_mirror_data U
CREATE UNIQUE INDEX index_mr_blocks_on_blocking_and_blocked_mr_ids ON merge_request_blocks USING btree (blocking_merge_request_id, blocked_merge_request_id);
-CREATE INDEX index_mr_cleanup_schedules_timestamps ON merge_request_cleanup_schedules USING btree (scheduled_at) WHERE (completed_at IS NULL);
+CREATE INDEX index_mr_cleanup_schedules_timestamps_status ON merge_request_cleanup_schedules USING btree (scheduled_at) WHERE ((completed_at IS NULL) AND (status = 0));
CREATE UNIQUE INDEX index_mr_context_commits_on_merge_request_id_and_sha ON merge_request_context_commits USING btree (merge_request_id, sha);
diff --git a/doc/ci/pipelines/img/coverage_check_approval_rule_14_1.png b/doc/ci/pipelines/img/coverage_check_approval_rule_14_1.png
new file mode 100644
index 00000000000..00eb5c84ca9
--- /dev/null
+++ b/doc/ci/pipelines/img/coverage_check_approval_rule_14_1.png
Binary files differ
diff --git a/doc/ci/pipelines/settings.md b/doc/ci/pipelines/settings.md
index 236ca10190e..8aab7db04f8 100644
--- a/doc/ci/pipelines/settings.md
+++ b/doc/ci/pipelines/settings.md
@@ -241,6 +241,26 @@ you can view a graph or download a CSV file with this data. From your project:
Code coverage data is also [available at the group level](../../user/group/repositories_analytics/index.md).
+### Coverage check approval rule **(PREMIUM)**
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15765) in GitLab 14.0.
+> - [Made configurable in Project Settings](https://gitlab.com/gitlab-org/gitlab/-/issues/331001) in GitLab 14.1.
+
+You can implement merge request approvals to require approval by selected users or a group
+when merging a merge request would cause the project's test coverage to decline.
+
+Follow these steps to enable the `Coverage-Check` MR approval rule:
+
+1. Go to your project and select **Settings > General**.
+1. Expand **Merge request approvals**.
+1. Select **Enable** next to the `Coverage-Check` approval rule.
+1. Select the **Target branch**.
+1. Set the number of **Approvals required** to greater than zero.
+1. Select the users or groups to provide approval.
+1. Select **Add approval rule**.
+
+![Coverage-Check approval rule](img/coverage_check_approval_rule_14_1.png)
+
### Removing color codes
Some test coverage tools output with ANSI color codes that aren't
diff --git a/doc/development/fe_guide/vuex.md b/doc/development/fe_guide/vuex.md
index 6926a0d380d..064f01c8195 100644
--- a/doc/development/fe_guide/vuex.md
+++ b/doc/development/fe_guide/vuex.md
@@ -540,11 +540,11 @@ export default {
foo: ''
},
actions: {
- updateBar() {...}
- updateAll() {...}
+ updateBar() {...},
+ updateAll() {...},
},
getters: {
- getFoo() {...}
+ getFoo() {...},
}
}
```
@@ -559,13 +559,13 @@ export default {
* @param {string} list[].getter - the name of the getter, leave it empty to not use a getter
* @param {string} list[].updateFn - the name of the action, leave it empty to use the default action
* @param {string} defaultUpdateFn - the default function to dispatch
- * @param {string} root - optional key of the state where to search fo they keys described in list
+ * @param {string|function} root - optional key of the state where to search for they keys described in list
* @returns {Object} a dictionary with all the computed properties generated
*/
...mapComputed(
[
'baz',
- { key: 'bar', updateFn: 'updateBar' }
+ { key: 'bar', updateFn: 'updateBar' },
{ key: 'foo', getter: 'getFoo' },
],
'updateAll',
@@ -575,3 +575,48 @@ export default {
```
`mapComputed` then generates the appropriate computed properties that get the data from the store and dispatch the correct action when updated.
+
+In the event that the `root` of the key is more than one-level deep you can use a function to retrieve the relevant state object.
+
+For instance, with a store like:
+
+```javascript
+// this store is non-functional and only used to give context to the example
+export default {
+ state: {
+ foo: {
+ qux: {
+ baz: '',
+ bar: '',
+ foo: '',
+ },
+ },
+ },
+ actions: {
+ updateBar() {...},
+ updateAll() {...},
+ },
+ getters: {
+ getFoo() {...},
+ }
+}
+```
+
+The `root` could be:
+
+```javascript
+import { mapComputed } from '~/vuex_shared/bindings'
+export default {
+ computed: {
+ ...mapComputed(
+ [
+ 'baz',
+ { key: 'bar', updateFn: 'updateBar' },
+ { key: 'foo', getter: 'getFoo' },
+ ],
+ 'updateAll',
+ (state) => state.foo.qux,
+ ),
+ }
+}
+```
diff --git a/doc/development/usage_ping/dictionary.md b/doc/development/usage_ping/dictionary.md
index 246af7634d8..527da610623 100644
--- a/doc/development/usage_ping/dictionary.md
+++ b/doc/development/usage_ping/dictionary.md
@@ -4354,6 +4354,8 @@ The total count of Helm packages that have been published.
Group: `group::package`
+Data Category: `Optional`
+
Status: `implemented`
Tiers: `free`, `premium`, `ultimate`
diff --git a/doc/user/admin_area/analytics/dev_ops_report.md b/doc/user/admin_area/analytics/dev_ops_report.md
index cc96e0e788c..6158a89a13e 100644
--- a/doc/user/admin_area/analytics/dev_ops_report.md
+++ b/doc/user/admin_area/analytics/dev_ops_report.md
@@ -20,21 +20,22 @@ To see DevOps Report:
## DevOps Score
NOTE:
-Your GitLab instance's [Service Ping](../settings/usage_statistics.md#service-ping) must be activated in order to use this feature.
+To see the DevOps score, you must activate your GitLab instance's [Service Ping](../settings/usage_statistics.md#service-ping).
-The DevOps Score tab displays the usage of major GitLab features on your instance over
-the last 30 days, averaged over the number of billable users in that time period. It also
-provides a Lead score per feature, which is calculated based on GitLab analysis
-of top-performing instances based on [Service Ping data](../settings/usage_statistics.md#service-ping) that GitLab has
-collected. Your score is compared to the lead score of each feature and then expressed as a percentage at the bottom of said feature.
-Your overall **DevOps Score** is an average of your feature scores. You can use this score to compare your DevOps status to other organizations.
+You can use the DevOps score to compare your DevOps status to other organizations.
-The page also provides helpful links to articles and GitLab docs, to help you
-improve your scores.
+The DevOps Score tab displays the usage of major GitLab features on your instance over
+the last 30 days, averaged over the number of billable users in that time period.
+You can also see the Leader usage score, calculated from top-performing instances based on
+[Service Ping data](../settings/usage_statistics.md#service-ping) that GitLab has collected.
+Your score is compared to the lead score of each feature and then expressed
+as a percentage at the bottom of said feature. Your overall **DevOps Score** is an average of your
+feature scores.
Service Ping data is aggregated on GitLab servers for analysis. Your usage
-information is **not sent** to any other GitLab instances. If you have just started using GitLab, it may take a few weeks for data to be
-collected before this feature is available.
+information is **not sent** to any other GitLab instances.
+If you have just started using GitLab, it might take a few weeks for data to be collected before this
+feature is available.
## DevOps Adoption **(ULTIMATE SELF)**
@@ -46,7 +47,7 @@ collected before this feature is available.
> - The Overview tab [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/330401) in GitLab 14.1.
> - DAST and SAST metrics [added](https://gitlab.com/gitlab-org/gitlab/-/issues/328033) in GitLab 14.1.
-DevOps Adoption shows you which groups within your organization are using the most essential features of GitLab:
+DevOps Adoption shows you which groups in your organization are using the most essential features of GitLab:
- Dev
- Approvals
@@ -62,8 +63,7 @@ DevOps Adoption shows you which groups within your organization are using the mo
- Pipelines
- Runners
-When managing groups in the UI, you can add your groups with the **Add group to table**
-button, in the top right hand section the page.
+To add your groups, in the top right-hand section the page, select **Add group to table**.
DevOps Adoption allows you to:
diff --git a/doc/user/compliance/compliance_dashboard/index.md b/doc/user/compliance/compliance_dashboard/index.md
index 008c55eb347..fb6b3fe2cf6 100644
--- a/doc/user/compliance/compliance_dashboard/index.md
+++ b/doc/user/compliance/compliance_dashboard/index.md
@@ -25,10 +25,6 @@ The Compliance Dashboard shows only the latest MR on each project.
## Merge request drawer
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/299357) in GitLab 14.1.
-> - It's [deployed behind a feature flag](../../feature_flags.md), disabled by default.
-> - It's disabled on GitLab.com.
-> - It's not recommended for production use.
-> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-merge-request-drawer).
When you click on a row, a drawer is shown that provides further details about the merge
request:
@@ -104,28 +100,3 @@ the dropdown next to the **List of all merge commits** button at the top of the
NOTE:
The Chain of Custody report download is a CSV file, with a maximum size of 15 MB.
The remaining records are truncated when this limit is reached.
-
-## Enable or disable merge request drawer **(ULTIMATE SELF)**
-
-The merge request drawer is under development and not ready for production use. It is
-deployed behind a feature flag that is **disabled by default**.
-[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
-can enable it.
-
-To enable it:
-
-```ruby
-# For the instance
-Feature.enable(:compliance_dashboard_drawer)
-# For a single group
-Feature.enable(:compliance_dashboard_drawer, Group.find(<group id>))
-```
-
-To disable it:
-
-```ruby
-# For the instance
-Feature.disable(:compliance_dashboard_drawer)
-# For a single group
-Feature.disable(:compliance_dashboard_drawer, Group.find(<group id>)
-```
diff --git a/doc/user/group/import/img/bulk_imports_v13_8.png b/doc/user/group/import/img/bulk_imports_v13_8.png
deleted file mode 100644
index ae4d8567d80..00000000000
--- a/doc/user/group/import/img/bulk_imports_v13_8.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/group/import/img/bulk_imports_v14_1.png b/doc/user/group/import/img/bulk_imports_v14_1.png
new file mode 100644
index 00000000000..fb419c1df6c
--- /dev/null
+++ b/doc/user/group/import/img/bulk_imports_v14_1.png
Binary files differ
diff --git a/doc/user/group/import/img/import_panel_v13_8.png b/doc/user/group/import/img/import_panel_v13_8.png
deleted file mode 100644
index 28d61785098..00000000000
--- a/doc/user/group/import/img/import_panel_v13_8.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/group/import/img/import_panel_v14_1.png b/doc/user/group/import/img/import_panel_v14_1.png
new file mode 100644
index 00000000000..28417383b6c
--- /dev/null
+++ b/doc/user/group/import/img/import_panel_v14_1.png
Binary files differ
diff --git a/doc/user/group/import/index.md b/doc/user/group/import/index.md
index b7ba2de7bf9..d76685f992b 100644
--- a/doc/user/group/import/index.md
+++ b/doc/user/group/import/index.md
@@ -110,7 +110,7 @@ on an existing group's page.
1. On the New Group page, select **Import group**.
- ![Fill in import details](img/import_panel_v13_8.png)
+ ![Fill in import details](img/import_panel_v14_1.png)
1. Fill in source URL of your GitLab.
1. Fill in [personal access token](../../../user/profile/personal_access_tokens.md) for remote GitLab instance.
@@ -129,4 +129,4 @@ Migration importer page. Listed are the remote GitLab groups to which you have t
1. Once a group has been imported, click its GitLab path to open its GitLab URL.
-![Group Importer page](img/bulk_imports_v13_8.png)
+![Group Importer page](img/bulk_imports_v14_1.png)
diff --git a/doc/user/group/settings/img/import_panel_v13_4.png b/doc/user/group/settings/img/import_panel_v13_4.png
deleted file mode 100644
index e4e5b0e91a1..00000000000
--- a/doc/user/group/settings/img/import_panel_v13_4.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/group/settings/img/import_panel_v14_1.png b/doc/user/group/settings/img/import_panel_v14_1.png
new file mode 100644
index 00000000000..28417383b6c
--- /dev/null
+++ b/doc/user/group/settings/img/import_panel_v14_1.png
Binary files differ
diff --git a/doc/user/group/settings/import_export.md b/doc/user/group/settings/import_export.md
index 94a79ec6e74..5f732bee03f 100644
--- a/doc/user/group/settings/import_export.md
+++ b/doc/user/group/settings/import_export.md
@@ -93,9 +93,9 @@ on an existing group's page.
![Navigation paths to create a new group](img/new_group_navigation_v13_1.png)
-1. On the New Group page, select the **Import group** tab.
+1. On the New Group page, select the **Import group**.
- ![Fill in group details](img/import_panel_v13_4.png)
+ ![Fill in group details](img/import_panel_v14_1.png)
1. Enter your group name.
diff --git a/doc/user/project/merge_requests/approvals/index.md b/doc/user/project/merge_requests/approvals/index.md
index 053b5d161b3..40345f33cb2 100644
--- a/doc/user/project/merge_requests/approvals/index.md
+++ b/doc/user/project/merge_requests/approvals/index.md
@@ -104,6 +104,7 @@ Without the approvals, the work cannot merge. Required approvals enable multiple
database, for all proposed code changes.
- Use the [code owners of changed files](rules.md#code-owners-as-eligible-approvers),
to determine who should review the work.
+- Require an [approval before merging code that causes test coverage to decline](../../../../ci/pipelines/settings.md#coverage-check-approval-rule)
- [Require approval from a security team](../../../application_security/index.md#security-approvals-in-merge-requests)
before merging code that could introduce a vulnerability. **(ULTIMATE)**
diff --git a/doc/user/project/repository/branches/default.md b/doc/user/project/repository/branches/default.md
index ebc9d9aefde..0f4c831216a 100644
--- a/doc/user/project/repository/branches/default.md
+++ b/doc/user/project/repository/branches/default.md
@@ -152,6 +152,20 @@ renames a Git repository's (`example`) default branch.
1. Update references to the old branch name in related code and scripts that reside outside
your repository, such as helper utilities and integrations.
+## Default branch rename redirect
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/329100) in GitLab 14.1
+
+URLs for specific files or directories in a project embed the project's default
+branch name, and are often found in documentation or browser bookmarks. When you
+[update the default branch name in your repository](#update-the-default-branch-name-in-your-repository),
+these URLs change, and must be updated.
+
+To ease the transition period, whenever the default branch for a project is
+changed, GitLab records the name of the old default branch. If that branch is
+deleted, attempts to view a file or directory on it are redirected to the
+current default branch, instead of displaying the "not found" page.
+
## Resources
- [Discussion of default branch renaming](https://lore.kernel.org/git/pull.656.v4.git.1593009996.gitgitgadget@gmail.com/)
diff --git a/lib/extracts_path.rb b/lib/extracts_path.rb
index 4c537eeaa89..055a3a771c2 100644
--- a/lib/extracts_path.rb
+++ b/lib/extracts_path.rb
@@ -26,17 +26,17 @@ module ExtractsPath
# Automatically renders `not_found!` if a valid tree path could not be
# resolved (e.g., when a user inserts an invalid path or ref).
#
+ # Automatically redirects to the current default branch if the ref matches a
+ # previous default branch that has subsequently been deleted.
+ #
# rubocop:disable Gitlab/ModuleWithInstanceVariables
override :assign_ref_vars
def assign_ref_vars
super
- if @path.empty? && !@commit && @id.ends_with?('.atom')
- @id = @ref = extract_ref_without_atom(@id)
- @commit = @repo.commit(@ref)
+ rectify_atom!
- request.format = :atom if @commit
- end
+ rectify_renamed_default_branch! && return
raise InvalidPathError unless @commit
@@ -59,6 +59,42 @@ module ExtractsPath
private
+ # Override in controllers to determine which actions are subject to the redirect
+ def redirect_renamed_default_branch?
+ false
+ end
+
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ def rectify_atom!
+ return if @commit
+ return unless @id.ends_with?('.atom')
+ return unless @path.empty?
+
+ @id = @ref = extract_ref_without_atom(@id)
+ @commit = @repo.commit(@ref)
+
+ request.format = :atom if @commit
+ end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
+
+ # For GET/HEAD requests, if the ref doesn't exist in the repository, check
+ # whether we're trying to access a renamed default branch. If we are, we can
+ # redirect to the current default branch instead of rendering a 404.
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ def rectify_renamed_default_branch!
+ return unless redirect_renamed_default_branch?
+ return if @commit
+ return unless @id && @ref && repository_container.respond_to?(:previous_default_branch)
+ return unless repository_container.previous_default_branch == @ref
+ return unless request.get? || request.head?
+
+ flash[:notice] = _('The default branch for this project has been changed. Please update your bookmarks.')
+ redirect_to url_for(id: @id.sub(/\A#{Regexp.escape(@ref)}/, repository_container.default_branch))
+
+ true
+ end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
+
override :repository_container
def repository_container
@project
diff --git a/lib/gitlab/search/sort_options.rb b/lib/gitlab/search/sort_options.rb
index 2ab38147462..f8e5cf727ac 100644
--- a/lib/gitlab/search/sort_options.rb
+++ b/lib/gitlab/search/sort_options.rb
@@ -15,6 +15,10 @@ module Gitlab
:updated_at_asc
when %w[updated_at desc], [nil, 'updated_desc']
:updated_at_desc
+ when %w[popularity asc], [nil, 'popularity_asc']
+ :popularity_asc
+ when %w[popularity desc], [nil, 'popularity_desc']
+ :popularity_desc
else
:unknown
end
diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb
index 678c0b396ef..e6851af8264 100644
--- a/lib/gitlab/search_results.rb
+++ b/lib/gitlab/search_results.rb
@@ -7,6 +7,11 @@ module Gitlab
DEFAULT_PAGE = 1
DEFAULT_PER_PAGE = 20
+ SCOPE_ONLY_SORT = {
+ popularity_asc: %w[issues],
+ popularity_desc: %w[issues]
+ }.freeze
+
attr_reader :current_user, :query, :order_by, :sort, :filters
# Limit search results by passed projects
@@ -128,20 +133,29 @@ module Gitlab
end
# rubocop: disable CodeReuse/ActiveRecord
- def apply_sort(scope)
+ def apply_sort(results, scope: nil)
# Due to different uses of sort param we prefer order_by when
# present
- case ::Gitlab::Search::SortOptions.sort_and_direction(order_by, sort)
+ sort_by = ::Gitlab::Search::SortOptions.sort_and_direction(order_by, sort)
+
+ # Reset sort to default if the chosen one is not supported by scope
+ sort_by = nil if SCOPE_ONLY_SORT[sort_by] && !SCOPE_ONLY_SORT[sort_by].include?(scope)
+
+ case sort_by
when :created_at_asc
- scope.reorder('created_at ASC')
+ results.reorder('created_at ASC')
when :created_at_desc
- scope.reorder('created_at DESC')
+ results.reorder('created_at DESC')
when :updated_at_asc
- scope.reorder('updated_at ASC')
+ results.reorder('updated_at ASC')
when :updated_at_desc
- scope.reorder('updated_at DESC')
+ results.reorder('updated_at DESC')
+ when :popularity_asc
+ results.reorder('upvotes_count ASC')
+ when :popularity_desc
+ results.reorder('upvotes_count DESC')
else
- scope.reorder('created_at DESC')
+ results.reorder('created_at DESC')
end
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -157,7 +171,7 @@ module Gitlab
issues = issues.where(project_id: project_ids_relation) # rubocop: disable CodeReuse/ActiveRecord
end
- apply_sort(issues)
+ apply_sort(issues, scope: 'issues')
end
# rubocop: disable CodeReuse/ActiveRecord
@@ -177,7 +191,7 @@ module Gitlab
merge_requests = merge_requests.in_projects(project_ids_relation)
end
- apply_sort(merge_requests)
+ apply_sort(merge_requests, scope: 'merge_requests')
end
def default_scope
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index f4d5107de55..6ad72e44ebe 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1459,12 +1459,6 @@ msgstr ""
msgid "A member of the abuse team will review your report as soon as possible."
msgstr ""
-msgid "A merge request approval is required when a security report contains a new vulnerability of high, critical, or unknown severity."
-msgstr ""
-
-msgid "A merge request approval is required when the license compliance report contains a denied license."
-msgstr ""
-
msgid "A merge request hasn't yet been merged"
msgstr ""
@@ -5681,6 +5675,9 @@ msgstr ""
msgid "BulkImport|Existing groups"
msgstr ""
+msgid "BulkImport|Filter by source group"
+msgstr ""
+
msgid "BulkImport|From source group"
msgstr ""
@@ -19290,15 +19287,9 @@ msgstr ""
msgid "Learn more about Auto DevOps"
msgstr ""
-msgid "Learn more about License-Check"
-msgstr ""
-
msgid "Learn more about Needs relationships"
msgstr ""
-msgid "Learn more about Vulnerability-Check"
-msgstr ""
-
msgid "Learn more about Web Terminal"
msgstr ""
@@ -19494,9 +19485,6 @@ msgstr ""
msgid "License overview"
msgstr ""
-msgid "License-Check"
-msgstr ""
-
msgid "LicenseCompliance|%{docLinkStart}License Approvals%{docLinkEnd} are active"
msgstr ""
@@ -28931,18 +28919,51 @@ msgstr ""
msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
+msgid "SecurityApprovals|A merge request approval is required when a security report contains a new vulnerability of high, critical, or unknown severity."
+msgstr ""
+
+msgid "SecurityApprovals|A merge request approval is required when test coverage declines."
+msgstr ""
+
+msgid "SecurityApprovals|A merge request approval is required when the license compliance report contains a denied license."
+msgstr ""
+
msgid "SecurityApprovals|Configurable if security scanners are enabled. %{linkStart}Learn more.%{linkEnd}"
msgstr ""
+msgid "SecurityApprovals|Coverage-Check"
+msgstr ""
+
+msgid "SecurityApprovals|Learn more about Coverage-Check"
+msgstr ""
+
+msgid "SecurityApprovals|Learn more about License-Check"
+msgstr ""
+
+msgid "SecurityApprovals|Learn more about Vulnerability-Check"
+msgstr ""
+
msgid "SecurityApprovals|License Scanning must be enabled. %{linkStart}Learn more%{linkEnd}."
msgstr ""
+msgid "SecurityApprovals|License-Check"
+msgstr ""
+
msgid "SecurityApprovals|Requires approval for Denied licenses. %{linkStart}More information%{linkEnd}"
msgstr ""
+msgid "SecurityApprovals|Requires approval for decreases in test coverage. %{linkStart}More information%{linkEnd}"
+msgstr ""
+
msgid "SecurityApprovals|Requires approval for vulnerabilities of Critical, High, or Unknown severity. %{linkStart}Learn more.%{linkEnd}"
msgstr ""
+msgid "SecurityApprovals|Test coverage must be enabled. %{linkStart}Learn more%{linkEnd}."
+msgstr ""
+
+msgid "SecurityApprovals|Vulnerability-Check"
+msgstr ""
+
msgid "SecurityConfiguration|%{featureName} merge request creation mutation failed"
msgstr ""
@@ -29765,7 +29786,7 @@ msgstr ""
msgid "Service URL"
msgstr ""
-msgid "Service ping is disabled, and cannot be configured through this form."
+msgid "Service ping is disabled in your configuration file, and cannot be enabled through this form."
msgstr ""
msgid "ServiceDesk|Enable Service Desk"
@@ -32554,6 +32575,9 @@ msgstr ""
msgid "The default CI/CD configuration file and path for new projects."
msgstr ""
+msgid "The default branch for this project has been changed. Please update your bookmarks."
+msgstr ""
+
msgid "The dependency list details information about the components used within your project."
msgstr ""
@@ -36400,9 +36424,6 @@ msgstr ""
msgid "Vulnerability resolved in the default branch"
msgstr ""
-msgid "Vulnerability-Check"
-msgstr ""
-
msgid "VulnerabilityChart|%{formattedStartDate} to today"
msgstr ""
@@ -37923,7 +37944,7 @@ msgstr ""
msgid "Your CSV import for project"
msgstr ""
-msgid "Your DevOps Report gives an overview of how you are using GitLab from a feature perspective. View how you compare with other organizations, discover features you are not using, and learn best practices through blog posts and white papers."
+msgid "Your DevOps Report gives an overview of how you are using GitLab from a feature perspective. Use it to view how you compare with other organizations."
msgstr ""
msgid "Your GPG keys (%{count})"
diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb
index 9493215247a..53efcc65066 100644
--- a/spec/controllers/projects/blob_controller_spec.rb
+++ b/spec/controllers/projects/blob_controller_spec.rb
@@ -5,7 +5,8 @@ require 'spec_helper'
RSpec.describe Projects::BlobController do
include ProjectForksHelper
- let(:project) { create(:project, :public, :repository) }
+ let(:project) { create(:project, :public, :repository, previous_default_branch: previous_default_branch) }
+ let(:previous_default_branch) { nil }
describe "GET show" do
def request
@@ -42,6 +43,20 @@ RSpec.describe Projects::BlobController do
it { is_expected.to respond_with(:not_found) }
end
+ context "renamed default branch, valid file" do
+ let(:id) { 'old-default-branch/README.md' }
+ let(:previous_default_branch) { 'old-default-branch' }
+
+ it { is_expected.to redirect_to("/#{project.full_path}/-/blob/#{project.default_branch}/README.md") }
+ end
+
+ context "renamed default branch, invalid file" do
+ let(:id) { 'old-default-branch/invalid-path.rb' }
+ let(:previous_default_branch) { 'old-default-branch' }
+
+ it { is_expected.to redirect_to("/#{project.full_path}/-/blob/#{project.default_branch}/invalid-path.rb") }
+ end
+
context "binary file" do
let(:id) { 'binary-encoding/encoding/binary-1.bin' }
diff --git a/spec/controllers/projects/tree_controller_spec.rb b/spec/controllers/projects/tree_controller_spec.rb
index 8e4e275bdbe..143516e4712 100644
--- a/spec/controllers/projects/tree_controller_spec.rb
+++ b/spec/controllers/projects/tree_controller_spec.rb
@@ -3,8 +3,9 @@
require 'spec_helper'
RSpec.describe Projects::TreeController do
- let(:project) { create(:project, :repository) }
- let(:user) { create(:user) }
+ let(:project) { create(:project, :repository, previous_default_branch: previous_default_branch) }
+ let(:previous_default_branch) { nil }
+ let(:user) { create(:user) }
before do
sign_in(user)
@@ -55,6 +56,20 @@ RSpec.describe Projects::TreeController do
it { is_expected.to respond_with(:not_found) }
end
+ context "renamed default branch, valid file" do
+ let(:id) { 'old-default-branch/encoding/' }
+ let(:previous_default_branch) { 'old-default-branch' }
+
+ it { is_expected.to redirect_to("/#{project.full_path}/-/tree/#{project.default_branch}/encoding/") }
+ end
+
+ context "renamed default branch, invalid file" do
+ let(:id) { 'old-default-branch/invalid-path/' }
+ let(:previous_default_branch) { 'old-default-branch' }
+
+ it { is_expected.to redirect_to("/#{project.full_path}/-/tree/#{project.default_branch}/invalid-path/") }
+ end
+
context "valid empty branch, invalid path" do
let(:id) { 'empty-branch/invalid-path/' }
diff --git a/spec/factories/merge_request_cleanup_schedules.rb b/spec/factories/merge_request_cleanup_schedules.rb
index a89d0c88731..ecf0d5818e4 100644
--- a/spec/factories/merge_request_cleanup_schedules.rb
+++ b/spec/factories/merge_request_cleanup_schedules.rb
@@ -3,6 +3,19 @@
FactoryBot.define do
factory :merge_request_cleanup_schedule, class: 'MergeRequest::CleanupSchedule' do
merge_request
- scheduled_at { Time.current }
+ scheduled_at { 1.day.ago }
+
+ trait :running do
+ status { MergeRequest::CleanupSchedule::STATUSES[:running] }
+ end
+
+ trait :completed do
+ status { MergeRequest::CleanupSchedule::STATUSES[:completed] }
+ completed_at { Time.current }
+ end
+
+ trait :failed do
+ status { MergeRequest::CleanupSchedule::STATUSES[:failed] }
+ end
end
end
diff --git a/spec/fixtures/lib/generators/gitlab/usage_metric_definition_generator/sample_metric_with_ee.yml b/spec/fixtures/lib/generators/gitlab/usage_metric_definition_generator/sample_metric_with_ee.yml
index 5cebfbcbad9..9de4d2a5644 100644
--- a/spec/fixtures/lib/generators/gitlab/usage_metric_definition_generator/sample_metric_with_ee.yml
+++ b/spec/fixtures/lib/generators/gitlab/usage_metric_definition_generator/sample_metric_with_ee.yml
@@ -12,7 +12,7 @@ milestone: "13.9"
introduced_by_url:
time_frame: 7d
data_source:
-data_category: Operational
+data_category: Optional
distribution:
- ee
tier:
diff --git a/spec/fixtures/lib/generators/gitlab/usage_metric_definition_generator/sample_metric_with_name_suggestions.yml b/spec/fixtures/lib/generators/gitlab/usage_metric_definition_generator/sample_metric_with_name_suggestions.yml
index d448e7bf3f6..0e7de369c82 100644
--- a/spec/fixtures/lib/generators/gitlab/usage_metric_definition_generator/sample_metric_with_name_suggestions.yml
+++ b/spec/fixtures/lib/generators/gitlab/usage_metric_definition_generator/sample_metric_with_name_suggestions.yml
@@ -13,7 +13,7 @@ milestone: "13.9"
introduced_by_url:
time_frame: 7d
data_source:
-data_category: Operational
+data_category: Optional
distribution:
- ce
- ee
diff --git a/spec/frontend/repository/components/blob_button_group_spec.js b/spec/frontend/repository/components/blob_button_group_spec.js
index b9a11dd1270..a449fd6f06c 100644
--- a/spec/frontend/repository/components/blob_button_group_spec.js
+++ b/spec/frontend/repository/components/blob_button_group_spec.js
@@ -1,8 +1,8 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-
import BlobButtonGroup from '~/repository/components/blob_button_group.vue';
+import DeleteBlobModal from '~/repository/components/delete_blob_modal.vue';
import UploadBlobModal from '~/repository/components/upload_blob_modal.vue';
const DEFAULT_PROPS = {
@@ -10,6 +10,8 @@ const DEFAULT_PROPS = {
path: 'some/path',
canPushCode: true,
replacePath: 'some/replace/path',
+ deletePath: 'some/delete/path',
+ emptyRepo: false,
};
const DEFAULT_INJECT = {
@@ -39,6 +41,7 @@ describe('BlobButtonGroup component', () => {
wrapper.destroy();
});
+ const findDeleteBlobModal = () => wrapper.findComponent(DeleteBlobModal);
const findUploadBlobModal = () => wrapper.findComponent(UploadBlobModal);
const findReplaceButton = () => wrapper.findAll(GlButton).at(0);
@@ -93,4 +96,22 @@ describe('BlobButtonGroup component', () => {
primaryBtnText: 'Replace file',
});
});
+
+ it('renders DeleteBlobModel', () => {
+ createComponent();
+
+ const { targetBranch, originalBranch } = DEFAULT_INJECT;
+ const { name, canPushCode, deletePath, emptyRepo } = DEFAULT_PROPS;
+ const title = `Delete ${name}`;
+
+ expect(findDeleteBlobModal().props()).toMatchObject({
+ modalTitle: title,
+ commitMessage: title,
+ targetBranch,
+ originalBranch,
+ canPushCode,
+ deletePath,
+ emptyRepo,
+ });
+ });
});
diff --git a/spec/frontend/repository/components/blob_content_viewer_spec.js b/spec/frontend/repository/components/blob_content_viewer_spec.js
index e1ac46171bb..a83d0a607f2 100644
--- a/spec/frontend/repository/components/blob_content_viewer_spec.js
+++ b/spec/frontend/repository/components/blob_content_viewer_spec.js
@@ -58,23 +58,36 @@ const richMockData = {
renderError: null,
},
};
-const userPermissionsMockData = {
+
+const projectMockData = {
userPermissions: {
pushCode: true,
},
+ repository: {
+ empty: false,
+ },
};
const localVue = createLocalVue();
const mockAxios = new MockAdapter(axios);
-const createComponentWithApollo = (mockData, mockPermissionData = true) => {
+const createComponentWithApollo = (mockData = {}) => {
localVue.use(VueApollo);
+ const defaultPushCode = projectMockData.userPermissions.pushCode;
+ const defaultEmptyRepo = projectMockData.repository.empty;
+ const { blobs, emptyRepo = defaultEmptyRepo, canPushCode = defaultPushCode } = mockData;
+
const mockResolver = jest.fn().mockResolvedValue({
data: {
project: {
- userPermissions: { pushCode: mockPermissionData },
- repository: { blobs: { nodes: [mockData] } },
+ userPermissions: { pushCode: canPushCode },
+ repository: {
+ empty: emptyRepo,
+ blobs: {
+ nodes: [blobs],
+ },
+ },
},
},
});
@@ -209,14 +222,14 @@ describe('Blob content viewer component', () => {
describe('legacy viewers', () => {
it('does not load a legacy viewer when a rich viewer is not available', async () => {
- createComponentWithApollo(simpleMockData);
+ createComponentWithApollo({ blobs: simpleMockData });
await waitForPromises();
expect(mockAxios.history.get).toHaveLength(0);
});
it('loads a legacy viewer when a rich viewer is available', async () => {
- createComponentWithApollo(richMockData);
+ createComponentWithApollo({ blobs: richMockData });
await waitForPromises();
expect(mockAxios.history.get).toHaveLength(1);
@@ -320,16 +333,20 @@ describe('Blob content viewer component', () => {
});
describe('BlobButtonGroup', () => {
- const { name, path, replacePath } = simpleMockData;
+ const { name, path, replacePath, webPath } = simpleMockData;
const {
userPermissions: { pushCode },
- } = userPermissionsMockData;
+ repository: { empty },
+ } = projectMockData;
it('renders component', async () => {
window.gon.current_user_id = 1;
fullFactory({
- mockData: { blobInfo: simpleMockData, project: userPermissionsMockData },
+ mockData: {
+ blobInfo: simpleMockData,
+ project: { userPermissions: { pushCode }, repository: { empty } },
+ },
stubs: {
BlobContent: true,
BlobButtonGroup: true,
@@ -342,7 +359,9 @@ describe('Blob content viewer component', () => {
name,
path,
replacePath,
+ deletePath: webPath,
canPushCode: pushCode,
+ emptyRepo: empty,
});
});
diff --git a/spec/frontend/repository/components/delete_blob_modal_spec.js b/spec/frontend/repository/components/delete_blob_modal_spec.js
new file mode 100644
index 00000000000..a74e3e6d325
--- /dev/null
+++ b/spec/frontend/repository/components/delete_blob_modal_spec.js
@@ -0,0 +1,130 @@
+import { GlFormTextarea, GlModal, GlFormInput, GlToggle } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import DeleteBlobModal from '~/repository/components/delete_blob_modal.vue';
+
+jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
+
+const initialProps = {
+ modalId: 'Delete-blob',
+ modalTitle: 'Delete File',
+ deletePath: 'some/path',
+ commitMessage: 'Delete File',
+ targetBranch: 'some-target-branch',
+ originalBranch: 'main',
+ canPushCode: true,
+ emptyRepo: false,
+};
+
+describe('DeleteBlobModal', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(DeleteBlobModal, {
+ propsData: {
+ ...initialProps,
+ ...props,
+ },
+ });
+ };
+
+ const findModal = () => wrapper.findComponent(GlModal);
+ const findForm = () => wrapper.findComponent({ ref: 'form' });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders Modal component', () => {
+ createComponent();
+
+ const { modalTitle: title } = initialProps;
+
+ expect(findModal().props()).toMatchObject({
+ title,
+ size: 'md',
+ actionPrimary: {
+ text: 'Delete file',
+ },
+ actionCancel: {
+ text: 'Cancel',
+ },
+ });
+ });
+
+ describe('form', () => {
+ it('gets passed the path for action attribute', () => {
+ createComponent();
+ expect(findForm().attributes('action')).toBe(initialProps.deletePath);
+ });
+
+ it('submits the form', async () => {
+ createComponent();
+
+ const submitSpy = jest.spyOn(findForm().element, 'submit');
+ findModal().vm.$emit('primary', { preventDefault: () => {} });
+ await nextTick();
+
+ expect(submitSpy).toHaveBeenCalled();
+ submitSpy.mockRestore();
+ });
+
+ it.each`
+ component | defaultValue | canPushCode | targetBranch | originalBranch | exist
+ ${GlFormTextarea} | ${initialProps.commitMessage} | ${true} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${true}
+ ${GlFormInput} | ${initialProps.targetBranch} | ${true} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${true}
+ ${GlFormInput} | ${undefined} | ${false} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${false}
+ ${GlToggle} | ${'true'} | ${true} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${true}
+ ${GlToggle} | ${undefined} | ${true} | ${'same-branch'} | ${'same-branch'} | ${false}
+ `(
+ 'has the correct form fields ',
+ ({ component, defaultValue, canPushCode, targetBranch, originalBranch, exist }) => {
+ createComponent({
+ canPushCode,
+ targetBranch,
+ originalBranch,
+ });
+ const formField = wrapper.findComponent(component);
+
+ if (!exist) {
+ expect(formField.exists()).toBe(false);
+ return;
+ }
+
+ expect(formField.exists()).toBe(true);
+ expect(formField.attributes('value')).toBe(defaultValue);
+ },
+ );
+
+ it.each`
+ input | value | emptyRepo | canPushCode | exist
+ ${'authenticity_token'} | ${'mock-csrf-token'} | ${false} | ${true} | ${true}
+ ${'authenticity_token'} | ${'mock-csrf-token'} | ${true} | ${false} | ${true}
+ ${'_method'} | ${'delete'} | ${false} | ${true} | ${true}
+ ${'_method'} | ${'delete'} | ${true} | ${false} | ${true}
+ ${'original_branch'} | ${initialProps.originalBranch} | ${false} | ${true} | ${true}
+ ${'original_branch'} | ${undefined} | ${true} | ${true} | ${false}
+ ${'create_merge_request'} | ${'1'} | ${false} | ${false} | ${true}
+ ${'create_merge_request'} | ${'1'} | ${false} | ${true} | ${true}
+ ${'create_merge_request'} | ${undefined} | ${true} | ${false} | ${false}
+ `(
+ 'passes $input as a hidden input with the correct value',
+ ({ input, value, emptyRepo, canPushCode, exist }) => {
+ createComponent({
+ emptyRepo,
+ canPushCode,
+ });
+
+ const inputMethod = findForm().find(`input[name="${input}"]`);
+
+ if (!exist) {
+ expect(inputMethod.exists()).toBe(false);
+ return;
+ }
+
+ expect(inputMethod.attributes('type')).toBe('hidden');
+ expect(inputMethod.attributes('value')).toBe(value);
+ },
+ );
+ });
+});
diff --git a/spec/frontend/vuex_shared/bindings_spec.js b/spec/frontend/vuex_shared/bindings_spec.js
index 0f91a09018f..4e210143c8c 100644
--- a/spec/frontend/vuex_shared/bindings_spec.js
+++ b/spec/frontend/vuex_shared/bindings_spec.js
@@ -3,7 +3,7 @@ import { mapComputed } from '~/vuex_shared/bindings';
describe('Binding utils', () => {
describe('mapComputed', () => {
- const defaultArgs = [['baz'], 'bar', 'foo'];
+ const defaultArgs = [['baz'], 'bar', 'foo', 'qux'];
const createDummy = (mapComputedArgs = defaultArgs) => ({
computed: {
@@ -29,12 +29,18 @@ describe('Binding utils', () => {
},
};
- it('returns an object with keys equal to the first fn parameter ', () => {
+ it('returns an object with keys equal to the first fn parameter', () => {
const keyList = ['foo1', 'foo2'];
const result = mapComputed(keyList, 'foo', 'bar');
expect(Object.keys(result)).toEqual(keyList);
});
+ it('returns an object with keys equal to the first fn parameter when the root is a function', () => {
+ const keyList = ['foo1', 'foo2'];
+ const result = mapComputed(keyList, 'foo', (state) => state.bar);
+ expect(Object.keys(result)).toEqual(keyList);
+ });
+
it('returned object has set and get function', () => {
const result = mapComputed(['baz'], 'foo', 'bar');
expect(result.baz.set).toBeDefined();
diff --git a/spec/helpers/clusters_helper_spec.rb b/spec/helpers/clusters_helper_spec.rb
index 8c738141063..f64afa1ed71 100644
--- a/spec/helpers/clusters_helper_spec.rb
+++ b/spec/helpers/clusters_helper_spec.rb
@@ -75,6 +75,13 @@ RSpec.describe ClustersHelper do
it 'displays project path' do
expect(subject[:project_path]).to eq(project.full_path)
end
+
+ it 'generates docs urls' do
+ expect(subject[:agent_docs_url]).to eq(help_page_path('user/clusters/agent/index'))
+ expect(subject[:install_docs_url]).to eq(help_page_path('administration/clusters/kas'))
+ expect(subject[:get_started_docs_url]).to eq(help_page_path('user/clusters/agent/index', anchor: 'define-a-configuration-repository'))
+ expect(subject[:integration_docs_url]).to eq(help_page_path('user/clusters/agent/index', anchor: 'get-started-with-gitops-and-the-gitlab-agent'))
+ end
end
describe '#js_clusters_list_data' do
diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb
index 57eda63854d..05f3bb2f71a 100644
--- a/spec/lib/extracts_path_spec.rb
+++ b/spec/lib/extracts_path_spec.rb
@@ -7,10 +7,17 @@ RSpec.describe ExtractsPath do
include RepoHelpers
include Gitlab::Routing
+ # Make url_for work
+ def default_url_options
+ { controller: 'projects/blob', action: 'show', namespace_id: @project.namespace.path, project_id: @project.path }
+ end
+
let_it_be(:owner) { create(:user) }
let_it_be(:container) { create(:project, :repository, creator: owner) }
let(:request) { double('request') }
+ let(:flash) { {} }
+ let(:redirect_renamed_default_branch?) { true }
before do
@project = container
@@ -18,11 +25,14 @@ RSpec.describe ExtractsPath do
allow(container.repository).to receive(:ref_names).and_return(ref_names)
allow(request).to receive(:format=)
+ allow(request).to receive(:get?)
+ allow(request).to receive(:head?)
end
describe '#assign_ref_vars' do
let(:ref) { sample_commit[:id] }
- let(:params) { { path: sample_commit[:line_code_path], ref: ref } }
+ let(:path) { sample_commit[:line_code_path] }
+ let(:params) { { path: path, ref: ref } }
it_behaves_like 'assigns ref vars'
@@ -126,6 +136,66 @@ RSpec.describe ExtractsPath do
expect(@commit).to be_nil
end
end
+
+ context 'ref points to a previous default branch' do
+ let(:ref) { 'develop' }
+
+ before do
+ @project.update!(previous_default_branch: ref)
+
+ allow(@project).to receive(:default_branch).and_return('foo')
+ end
+
+ it 'redirects to the new default branch for a GET request' do
+ allow(request).to receive(:get?).and_return(true)
+
+ expect(self).to receive(:redirect_to).with("http://localhost/#{@project.full_path}/-/blob/foo/#{path}")
+ expect(self).not_to receive(:render_404)
+
+ assign_ref_vars
+
+ expect(@commit).to be_nil
+ expect(flash[:notice]).to match(/default branch/)
+ end
+
+ it 'redirects to the new default branch for a HEAD request' do
+ allow(request).to receive(:head?).and_return(true)
+
+ expect(self).to receive(:redirect_to).with("http://localhost/#{@project.full_path}/-/blob/foo/#{path}")
+ expect(self).not_to receive(:render_404)
+
+ assign_ref_vars
+
+ expect(@commit).to be_nil
+ expect(flash[:notice]).to match(/default branch/)
+ end
+
+ it 'returns 404 for any other request type' do
+ expect(self).not_to receive(:redirect_to)
+ expect(self).to receive(:render_404)
+
+ assign_ref_vars
+
+ expect(@commit).to be_nil
+ expect(flash).to be_empty
+ end
+
+ context 'redirect behaviour is disabled' do
+ let(:redirect_renamed_default_branch?) { false }
+
+ it 'returns 404 for a GET request' do
+ allow(request).to receive(:get?).and_return(true)
+
+ expect(self).not_to receive(:redirect_to)
+ expect(self).to receive(:render_404)
+
+ assign_ref_vars
+
+ expect(@commit).to be_nil
+ expect(flash).to be_empty
+ end
+ end
+ end
end
it_behaves_like 'extracts refs'
diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
index 06dc9b3307a..2974893ec4a 100644
--- a/spec/lib/gitlab/search_results_spec.rb
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -229,10 +229,18 @@ RSpec.describe Gitlab::SearchResults do
let!(:new_updated) { create(:issue, project: project, title: 'updated recent', updated_at: 1.day.ago) }
let!(:very_old_updated) { create(:issue, project: project, title: 'updated very old', updated_at: 1.year.ago) }
+ let!(:less_popular_result) { create(:issue, project: project, title: 'less popular', upvotes_count: 10) }
+ let!(:popular_result) { create(:issue, project: project, title: 'popular', upvotes_count: 100) }
+ let!(:non_popular_result) { create(:issue, project: project, title: 'non popular', upvotes_count: 1) }
+
include_examples 'search results sorted' do
let(:results_created) { described_class.new(user, 'sorted', Project.order(:id), sort: sort, filters: filters) }
let(:results_updated) { described_class.new(user, 'updated', Project.order(:id), sort: sort, filters: filters) }
end
+
+ include_examples 'search results sorted by popularity' do
+ let(:results_popular) { described_class.new(user, 'popular', Project.order(:id), sort: sort, filters: filters) }
+ end
end
end
diff --git a/spec/migrations/add_upvotes_count_index_to_issues_spec.rb b/spec/migrations/add_upvotes_count_index_to_issues_spec.rb
new file mode 100644
index 00000000000..c04cb98a107
--- /dev/null
+++ b/spec/migrations/add_upvotes_count_index_to_issues_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe AddUpvotesCountIndexToIssues do
+ let(:migration_instance) { described_class.new }
+
+ describe '#up' do
+ it 'adds index' do
+ expect { migrate! }.to change { migration_instance.index_exists?(:issues, [:project_id, :upvotes_count], name: described_class::INDEX_NAME) }.from(false).to(true)
+ end
+ end
+
+ describe '#down' do
+ it 'removes index' do
+ migrate!
+
+ expect { schema_migrate_down! }.to change { migration_instance.index_exists?(:issues, [:project_id, :upvotes_count], name: described_class::INDEX_NAME) }.from(true).to(false)
+ end
+ end
+end
diff --git a/spec/models/merge_request/cleanup_schedule_spec.rb b/spec/models/merge_request/cleanup_schedule_spec.rb
index 925d287088b..85208f901fd 100644
--- a/spec/models/merge_request/cleanup_schedule_spec.rb
+++ b/spec/models/merge_request/cleanup_schedule_spec.rb
@@ -11,22 +11,125 @@ RSpec.describe MergeRequest::CleanupSchedule do
it { is_expected.to validate_presence_of(:scheduled_at) }
end
- describe '.scheduled_merge_request_ids' do
- let_it_be(:mr_cleanup_schedule_1) { create(:merge_request_cleanup_schedule, scheduled_at: 2.days.ago) }
- let_it_be(:mr_cleanup_schedule_2) { create(:merge_request_cleanup_schedule, scheduled_at: 1.day.ago) }
- let_it_be(:mr_cleanup_schedule_3) { create(:merge_request_cleanup_schedule, scheduled_at: 1.day.ago, completed_at: Time.current) }
- let_it_be(:mr_cleanup_schedule_4) { create(:merge_request_cleanup_schedule, scheduled_at: 4.days.ago) }
- let_it_be(:mr_cleanup_schedule_5) { create(:merge_request_cleanup_schedule, scheduled_at: 3.days.ago) }
- let_it_be(:mr_cleanup_schedule_6) { create(:merge_request_cleanup_schedule, scheduled_at: 1.day.from_now) }
- let_it_be(:mr_cleanup_schedule_7) { create(:merge_request_cleanup_schedule, scheduled_at: 5.days.ago) }
-
- it 'only includes incomplete schedule within the specified limit' do
- expect(described_class.scheduled_merge_request_ids(4)).to eq([
- mr_cleanup_schedule_2.merge_request_id,
- mr_cleanup_schedule_1.merge_request_id,
- mr_cleanup_schedule_5.merge_request_id,
- mr_cleanup_schedule_4.merge_request_id
+ describe 'state machine transitions' do
+ let(:cleanup_schedule) { create(:merge_request_cleanup_schedule) }
+
+ it 'sets status to unstarted by default' do
+ expect(cleanup_schedule).to be_unstarted
+ end
+
+ describe '#run' do
+ it 'sets the status to running' do
+ cleanup_schedule.run
+
+ expect(cleanup_schedule.reload).to be_running
+ end
+
+ context 'when previous status is not unstarted' do
+ let(:cleanup_schedule) { create(:merge_request_cleanup_schedule, :running) }
+
+ it 'does not change status' do
+ expect { cleanup_schedule.run }.not_to change(cleanup_schedule, :status)
+ end
+ end
+ end
+
+ describe '#retry' do
+ let(:cleanup_schedule) { create(:merge_request_cleanup_schedule, :running) }
+
+ it 'sets the status to unstarted' do
+ cleanup_schedule.retry
+
+ expect(cleanup_schedule.reload).to be_unstarted
+ end
+
+ it 'increments failed_count' do
+ expect { cleanup_schedule.retry }.to change(cleanup_schedule, :failed_count).by(1)
+ end
+
+ context 'when previous status is not running' do
+ let(:cleanup_schedule) { create(:merge_request_cleanup_schedule) }
+
+ it 'does not change status' do
+ expect { cleanup_schedule.retry }.not_to change(cleanup_schedule, :status)
+ end
+ end
+ end
+
+ describe '#complete' do
+ let(:cleanup_schedule) { create(:merge_request_cleanup_schedule, :running) }
+
+ it 'sets the status to completed' do
+ cleanup_schedule.complete
+
+ expect(cleanup_schedule.reload).to be_completed
+ end
+
+ it 'sets the completed_at' do
+ expect { cleanup_schedule.complete }.to change(cleanup_schedule, :completed_at)
+ end
+
+ context 'when previous status is not running' do
+ let(:cleanup_schedule) { create(:merge_request_cleanup_schedule, :completed) }
+
+ it 'does not change status' do
+ expect { cleanup_schedule.complete }.not_to change(cleanup_schedule, :status)
+ end
+ end
+ end
+
+ describe '#mark_as_failed' do
+ let(:cleanup_schedule) { create(:merge_request_cleanup_schedule, :running) }
+
+ it 'sets the status to failed' do
+ cleanup_schedule.mark_as_failed
+
+ expect(cleanup_schedule.reload).to be_failed
+ end
+
+ it 'increments failed_count' do
+ expect { cleanup_schedule.mark_as_failed }.to change(cleanup_schedule, :failed_count).by(1)
+ end
+
+ context 'when previous status is not running' do
+ let(:cleanup_schedule) { create(:merge_request_cleanup_schedule, :failed) }
+
+ it 'does not change status' do
+ expect { cleanup_schedule.mark_as_failed }.not_to change(cleanup_schedule, :status)
+ end
+ end
+ end
+ end
+
+ describe '.scheduled_and_unstarted' do
+ let!(:cleanup_schedule_1) { create(:merge_request_cleanup_schedule, scheduled_at: 2.days.ago) }
+ let!(:cleanup_schedule_2) { create(:merge_request_cleanup_schedule, scheduled_at: 1.day.ago) }
+ let!(:cleanup_schedule_3) { create(:merge_request_cleanup_schedule, :completed, scheduled_at: 1.day.ago) }
+ let!(:cleanup_schedule_4) { create(:merge_request_cleanup_schedule, scheduled_at: 4.days.ago) }
+ let!(:cleanup_schedule_5) { create(:merge_request_cleanup_schedule, scheduled_at: 3.days.ago) }
+ let!(:cleanup_schedule_6) { create(:merge_request_cleanup_schedule, scheduled_at: 1.day.from_now) }
+ let!(:cleanup_schedule_7) { create(:merge_request_cleanup_schedule, :failed, scheduled_at: 5.days.ago) }
+
+ it 'returns records that are scheduled before or on current time and unstarted (ordered by scheduled first)' do
+ expect(described_class.scheduled_and_unstarted).to eq([
+ cleanup_schedule_2,
+ cleanup_schedule_1,
+ cleanup_schedule_5,
+ cleanup_schedule_4
])
end
end
+
+ describe '.start_next' do
+ let!(:cleanup_schedule_1) { create(:merge_request_cleanup_schedule, :completed, scheduled_at: 1.day.ago) }
+ let!(:cleanup_schedule_2) { create(:merge_request_cleanup_schedule, scheduled_at: 2.days.ago) }
+ let!(:cleanup_schedule_3) { create(:merge_request_cleanup_schedule, :running, scheduled_at: 1.day.ago) }
+ let!(:cleanup_schedule_4) { create(:merge_request_cleanup_schedule, scheduled_at: 3.days.ago) }
+ let!(:cleanup_schedule_5) { create(:merge_request_cleanup_schedule, :failed, scheduled_at: 3.days.ago) }
+
+ it 'finds the next scheduled and unstarted then marked it as running' do
+ expect(described_class.start_next).to eq(cleanup_schedule_2)
+ expect(cleanup_schedule_2.reload).to be_running
+ end
+ end
end
diff --git a/spec/requests/api/project_attributes.yml b/spec/requests/api/project_attributes.yml
index 2932447f663..8341fac3191 100644
--- a/spec/requests/api/project_attributes.yml
+++ b/spec/requests/api/project_attributes.yml
@@ -137,6 +137,7 @@ project_setting:
- has_confluence
- has_vulnerabilities
- prevent_merge_without_jira_issue
+ - previous_default_branch
- project_id
- push_rule_id
- show_default_award_emojis
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index 0cab1aa4abc..c74a8295d0a 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -200,17 +200,32 @@ RSpec.describe Projects::UpdateService do
context 'when updating a default branch' do
let(:project) { create(:project, :repository) }
- it 'changes a default branch' do
+ it 'changes default branch, tracking the previous branch' do
+ previous_default_branch = project.default_branch
+
update_project(project, admin, default_branch: 'feature')
- expect(Project.find(project.id).default_branch).to eq 'feature'
+ project.reload
+
+ expect(project.default_branch).to eq('feature')
+ expect(project.previous_default_branch).to eq(previous_default_branch)
+
+ update_project(project, admin, default_branch: previous_default_branch)
+
+ project.reload
+
+ expect(project.default_branch).to eq(previous_default_branch)
+ expect(project.previous_default_branch).to eq('feature')
end
it 'does not change a default branch' do
# The branch 'unexisted-branch' does not exist.
update_project(project, admin, default_branch: 'unexisted-branch')
- expect(Project.find(project.id).default_branch).to eq 'master'
+ project.reload
+
+ expect(project.default_branch).to eq 'master'
+ expect(project.previous_default_branch).to be_nil
end
end
diff --git a/spec/support/shared_examples/lib/gitlab/search_results_sorted_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/search_results_sorted_shared_examples.rb
index eafb49cef71..e4f09dfa0b0 100644
--- a/spec/support/shared_examples/lib/gitlab/search_results_sorted_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/search_results_sorted_shared_examples.rb
@@ -33,3 +33,21 @@ RSpec.shared_examples 'search results sorted' do
end
end
end
+
+RSpec.shared_examples 'search results sorted by popularity' do
+ context 'sort: popularity_desc' do
+ let(:sort) { 'popularity_desc' }
+
+ it 'sorts results by upvotes' do
+ expect(results_popular.objects(scope).map(&:id)).to eq([popular_result.id, less_popular_result.id, non_popular_result.id])
+ end
+ end
+
+ context 'sort: popularity_asc' do
+ let(:sort) { 'popularity_asc' }
+
+ it 'sorts results by created_at' do
+ expect(results_popular.objects(scope).map(&:id)).to eq([non_popular_result.id, less_popular_result.id, popular_result.id])
+ end
+ end
+end
diff --git a/spec/views/projects/empty.html.haml_spec.rb b/spec/views/projects/empty.html.haml_spec.rb
index 7fa95507f75..0fb0ae5ff29 100644
--- a/spec/views/projects/empty.html.haml_spec.rb
+++ b/spec/views/projects/empty.html.haml_spec.rb
@@ -64,6 +64,7 @@ RSpec.describe 'projects/empty' do
expect(rendered).to have_selector('.js-invite-members-modal')
expect(rendered).to have_selector('[data-label=invite_members_empty_project]')
expect(rendered).to have_selector('[data-event=click_button]')
+ expect(rendered).to have_selector('[data-trigger-source=project-empty-page]')
end
context 'when user does not have permissions to invite members' do
diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb
index 8c1667e5b4d..c75b9b43ef4 100644
--- a/spec/workers/every_sidekiq_worker_spec.rb
+++ b/spec/workers/every_sidekiq_worker_spec.rb
@@ -418,6 +418,7 @@ RSpec.describe 'Every Sidekiq worker' do
'ScanSecurityReportSecretsWorker' => 17,
'Security::AutoFixWorker' => 3,
'Security::StoreScansWorker' => 3,
+ 'Security::TrackSecureScansWorker' => 1,
'SelfMonitoringProjectCreateWorker' => 3,
'SelfMonitoringProjectDeleteWorker' => 3,
'ServiceDeskEmailReceiverWorker' => 3,
diff --git a/spec/workers/merge_request_cleanup_refs_worker_spec.rb b/spec/workers/merge_request_cleanup_refs_worker_spec.rb
index 7401c6dd4d7..1de927a81e4 100644
--- a/spec/workers/merge_request_cleanup_refs_worker_spec.rb
+++ b/spec/workers/merge_request_cleanup_refs_worker_spec.rb
@@ -3,18 +3,41 @@
require 'spec_helper'
RSpec.describe MergeRequestCleanupRefsWorker do
- describe '#perform' do
- context 'when merge request exists' do
- let(:merge_request) { create(:merge_request) }
- let(:job_args) { merge_request.id }
-
- include_examples 'an idempotent worker' do
- it 'calls MergeRequests::CleanupRefsService#execute' do
- expect_next_instance_of(MergeRequests::CleanupRefsService, merge_request) do |svc|
- expect(svc).to receive(:execute).and_call_original
- end.twice
-
- subject
+ let(:worker) { described_class.new }
+
+ describe '#perform_work' do
+ context 'when next cleanup schedule is found' do
+ let(:failed_count) { 0 }
+ let!(:cleanup_schedule) { create(:merge_request_cleanup_schedule, failed_count: failed_count) }
+
+ it 'marks the cleanup schedule as completed on success' do
+ stub_cleanup_service(status: :success)
+ worker.perform_work
+
+ expect(cleanup_schedule.reload).to be_completed
+ expect(cleanup_schedule.completed_at).to be_present
+ end
+
+ context 'when service fails' do
+ before do
+ stub_cleanup_service(status: :error)
+ worker.perform_work
+ end
+
+ it 'marks the cleanup schedule as unstarted and track the failure' do
+ expect(cleanup_schedule.reload).to be_unstarted
+ expect(cleanup_schedule.failed_count).to eq(1)
+ expect(cleanup_schedule.completed_at).to be_nil
+ end
+
+ context "and cleanup schedule has already failed #{described_class::FAILURE_THRESHOLD} times" do
+ let(:failed_count) { described_class::FAILURE_THRESHOLD }
+
+ it 'marks the cleanup schedule as failed and track the failure' do
+ expect(cleanup_schedule.reload).to be_failed
+ expect(cleanup_schedule.failed_count).to eq(described_class::FAILURE_THRESHOLD + 1)
+ expect(cleanup_schedule.completed_at).to be_nil
+ end
end
end
@@ -23,20 +46,52 @@ RSpec.describe MergeRequestCleanupRefsWorker do
stub_feature_flags(merge_request_refs_cleanup: false)
end
- it 'does not clean up the merge request' do
+ it 'does nothing' do
expect(MergeRequests::CleanupRefsService).not_to receive(:new)
- perform_multiple(1)
+ worker.perform_work
end
end
end
- context 'when merge request does not exist' do
- it 'does not call MergeRequests::CleanupRefsService' do
+ context 'when there is no next cleanup schedule found' do
+ it 'does nothing' do
expect(MergeRequests::CleanupRefsService).not_to receive(:new)
- perform_multiple(1)
+ worker.perform_work
+ end
+ end
+ end
+
+ describe '#remaining_work_count' do
+ let_it_be(:unstarted) { create_list(:merge_request_cleanup_schedule, 2) }
+ let_it_be(:running) { create_list(:merge_request_cleanup_schedule, 2, :running) }
+ let_it_be(:completed) { create_list(:merge_request_cleanup_schedule, 2, :completed) }
+
+ it 'returns number of scheduled and unstarted cleanup schedule records' do
+ expect(worker.remaining_work_count).to eq(unstarted.count)
+ end
+
+ context 'when count exceeds max_running_jobs' do
+ before do
+ create_list(:merge_request_cleanup_schedule, worker.max_running_jobs)
+ end
+
+ it 'gets capped at max_running_jobs' do
+ expect(worker.remaining_work_count).to eq(worker.max_running_jobs)
end
end
end
+
+ describe '#max_running_jobs' do
+ it 'returns the value of MAX_RUNNING_JOBS' do
+ expect(worker.max_running_jobs).to eq(described_class::MAX_RUNNING_JOBS)
+ end
+ end
+
+ def stub_cleanup_service(result)
+ expect_next_instance_of(MergeRequests::CleanupRefsService, cleanup_schedule.merge_request) do |svc|
+ expect(svc).to receive(:execute).and_return(result)
+ end
+ end
end
diff --git a/spec/workers/schedule_merge_request_cleanup_refs_worker_spec.rb b/spec/workers/schedule_merge_request_cleanup_refs_worker_spec.rb
index 869818b257e..ef515e43474 100644
--- a/spec/workers/schedule_merge_request_cleanup_refs_worker_spec.rb
+++ b/spec/workers/schedule_merge_request_cleanup_refs_worker_spec.rb
@@ -6,16 +6,9 @@ RSpec.describe ScheduleMergeRequestCleanupRefsWorker do
subject(:worker) { described_class.new }
describe '#perform' do
- before do
- allow(MergeRequest::CleanupSchedule)
- .to receive(:scheduled_merge_request_ids)
- .with(described_class::LIMIT)
- .and_return([1, 2, 3, 4])
- end
-
it 'does nothing if the database is read-only' do
allow(Gitlab::Database).to receive(:read_only?).and_return(true)
- expect(MergeRequestCleanupRefsWorker).not_to receive(:bulk_perform_in)
+ expect(MergeRequestCleanupRefsWorker).not_to receive(:perform_with_capacity)
worker.perform
end
@@ -26,25 +19,17 @@ RSpec.describe ScheduleMergeRequestCleanupRefsWorker do
end
it 'does not schedule any merge request clean ups' do
- expect(MergeRequestCleanupRefsWorker).not_to receive(:bulk_perform_in)
+ expect(MergeRequestCleanupRefsWorker).not_to receive(:perform_with_capacity)
worker.perform
end
end
include_examples 'an idempotent worker' do
- it 'schedules MergeRequestCleanupRefsWorker to be performed by batch' do
- expect(MergeRequestCleanupRefsWorker)
- .to receive(:bulk_perform_in)
- .with(
- described_class::DELAY,
- [[1], [2], [3], [4]],
- batch_size: described_class::BATCH_SIZE
- )
+ it 'schedules MergeRequestCleanupRefsWorker to be performed with capacity' do
+ expect(MergeRequestCleanupRefsWorker).to receive(:perform_with_capacity).twice
- expect(worker).to receive(:log_extra_metadata_on_done).with(:merge_requests_count, 4)
-
- worker.perform
+ subject
end
end
end