Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-06-29 12:08:42 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-06-29 12:08:42 +0300
commitf22f6efffe139939114f2a6a29e9864c481e3010 (patch)
treef6ef38eb2b120ec789b0678661f576c540dc2ba2
parent95085ac5cdbf6fcb12c6bdb167f663844d05147d (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/invite_members/components/group_select.vue82
-rw-r--r--app/assets/javascripts/issues/show/components/fields/description.vue37
-rw-r--r--app/controllers/explore/projects_controller.rb9
-rw-r--r--app/finders/issuable_finder.rb3
-rw-r--r--app/views/explore/projects/_project.atom.builder14
-rw-r--r--app/views/explore/projects/topic.atom.builder9
-rw-r--r--app/views/explore/projects/topic.html.haml4
-rw-r--r--config/feature_flags/development/issues_full_text_search.yml8
-rw-r--r--config/routes/explore.rb2
-rw-r--r--doc/administration/backup_restore/backup_gitlab.md5
-rw-r--r--doc/administration/geo/replication/troubleshooting.md2
-rw-r--r--doc/administration/sidekiq/sidekiq_troubleshooting.md26
-rw-r--r--doc/api/graphql/reference/index.md3
-rw-r--r--doc/ci/yaml/index.md45
-rw-r--r--doc/raketasks/backup_restore.md2
-rw-r--r--doc/user/project/working_with_projects.md4
-rw-r--r--doc/user/search/index.md1
-rw-r--r--lib/gitlab/gitaly_client/repository_service.rb6
-rw-r--r--lib/gitlab/github_import/importer/issue_importer.rb2
-rw-r--r--lib/gitlab/spamcheck/client.rb2
-rw-r--r--locale/gitlab.pot3
-rw-r--r--qa/qa/page/component/members/invite_members_modal.rb12
-rwxr-xr-xscripts/build_assets_image5
-rw-r--r--spec/controllers/dashboard_controller_spec.rb17
-rw-r--r--spec/controllers/explore/projects_controller_spec.rb53
-rw-r--r--spec/features/atom/topics_spec.rb49
-rw-r--r--spec/features/issues/filtered_search/filter_issues_spec.rb13
-rw-r--r--spec/frontend/invite_members/components/group_select_spec.js112
-rw-r--r--spec/frontend/issues/show/components/fields/description_spec.js95
-rw-r--r--spec/lib/gitlab/gitaly_client/repository_service_spec.rb17
-rw-r--r--spec/lib/gitlab/github_import/importer/issue_importer_spec.rb58
-rw-r--r--spec/lib/gitlab/spamcheck/client_spec.rb4
-rw-r--r--spec/support/helpers/features/invite_members_modal_helpers.rb2
-rw-r--r--spec/support/shared_examples/finders/issues_finder_shared_examples.rb12
-rw-r--r--spec/views/explore/projects/topic.html.haml_spec.rb24
35 files changed, 424 insertions, 318 deletions
diff --git a/app/assets/javascripts/invite_members/components/group_select.vue b/app/assets/javascripts/invite_members/components/group_select.vue
index 0e9781d77fe..a9f6ad15b23 100644
--- a/app/assets/javascripts/invite_members/components/group_select.vue
+++ b/app/assets/javascripts/invite_members/components/group_select.vue
@@ -1,11 +1,5 @@
<script>
-import {
- GlAvatarLabeled,
- GlDropdown,
- GlDropdownItem,
- GlDropdownText,
- GlSearchBoxByType,
-} from '@gitlab/ui';
+import { GlAvatarLabeled, GlCollapsibleListbox } from '@gitlab/ui';
import { debounce } from 'lodash';
import { s__ } from '~/locale';
import { getGroups, getDescendentGroups } from '~/rest_api';
@@ -15,15 +9,16 @@ export default {
name: 'GroupSelect',
components: {
GlAvatarLabeled,
- GlDropdown,
- GlDropdownItem,
- GlDropdownText,
- GlSearchBoxByType,
+ GlCollapsibleListbox,
},
model: {
prop: 'selectedGroup',
},
props: {
+ selectedGroup: {
+ type: Object,
+ required: true,
+ },
groupsFilter: {
type: String,
required: false,
@@ -43,23 +38,17 @@ export default {
return {
isFetching: false,
groups: [],
- selectedGroup: {},
searchTerm: '',
};
},
computed: {
- selectedGroupName() {
+ toggleText() {
return this.selectedGroup.name || this.$options.i18n.dropdownText;
},
isFetchResultEmpty() {
return this.groups.length === 0;
},
},
- watch: {
- searchTerm() {
- this.retrieveGroups();
- },
- },
mounted() {
this.retrieveGroups();
},
@@ -77,6 +66,8 @@ export default {
}, SEARCH_DELAY),
processGroups(response) {
const rawGroups = response.map((group) => ({
+ // `value` is needed for `GlCollapsibleListbox`
+ value: group.id,
id: group.id,
name: group.full_name,
path: group.path,
@@ -88,10 +79,12 @@ export default {
filterOutInvalidGroups(groups) {
return groups.filter((group) => this.invalidGroups.indexOf(group.id) === -1);
},
- selectGroup(group) {
- this.selectedGroup = group;
-
- this.$emit('input', this.selectedGroup);
+ onSelect(id) {
+ this.$emit('input', this.groups.find((group) => group.value === id) || {});
+ },
+ onSearch(searchTerm) {
+ this.searchTerm = searchTerm;
+ this.retrieveGroups();
},
fetchGroups() {
switch (this.groupsFilter) {
@@ -120,37 +113,30 @@ export default {
</script>
<template>
<div>
- <gl-dropdown
+ <gl-collapsible-listbox
data-testid="group-select-dropdown"
- :text="selectedGroupName"
+ :selected="selectedGroup.value"
+ :items="groups"
+ :toggle-text="toggleText"
+ searchable
+ :search-placeholder="$options.i18n.searchPlaceholder"
block
- toggle-class="gl-mb-2"
- menu-class="gl-w-full!"
+ fluid-width
+ is-check-centered
+ :searching="isFetching"
+ :no-results-text="$options.i18n.emptySearchResult"
+ @select="onSelect"
+ @search="onSearch"
>
- <gl-search-box-by-type
- v-model="searchTerm"
- :is-loading="isFetching"
- :placeholder="$options.i18n.searchPlaceholder"
- data-qa-selector="group_select_dropdown_search_field"
- />
- <gl-dropdown-item
- v-for="group in groups"
- :key="group.id"
- :name="group.name"
- data-qa-selector="group_select_dropdown_item"
- @click="selectGroup(group)"
- >
+ <template #list-item="{ item }">
<gl-avatar-labeled
- :label="group.name"
- :src="group.avatarUrl"
- :entity-id="group.id"
- :entity-name="group.name"
+ :label="item.name"
+ :src="item.avatarUrl"
+ :entity-id="item.value"
+ :entity-name="item.name"
:size="32"
/>
- </gl-dropdown-item>
- <gl-dropdown-text v-if="isFetchResultEmpty && !isFetching" data-testid="empty-result-message">
- <span class="gl-text-gray-500">{{ $options.i18n.emptySearchResult }}</span>
- </gl-dropdown-text>
- </gl-dropdown>
+ </template>
+ </gl-collapsible-listbox>
</div>
</template>
diff --git a/app/assets/javascripts/issues/show/components/fields/description.vue b/app/assets/javascripts/issues/show/components/fields/description.vue
index 3969c3b84a7..c9769dc9275 100644
--- a/app/assets/javascripts/issues/show/components/fields/description.vue
+++ b/app/assets/javascripts/issues/show/components/fields/description.vue
@@ -1,13 +1,11 @@
<script>
import { __ } from '~/locale';
-import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
import glFeaturesFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import updateMixin from '../../mixins/update';
export default {
components: {
- MarkdownField,
MarkdownEditor,
},
mixins: [updateMixin, glFeaturesFlagMixin()],
@@ -45,6 +43,11 @@ export default {
},
};
},
+ computed: {
+ autocompleteDataSources() {
+ return gl.GfmAutoComplete?.dataSources;
+ },
+ },
mounted() {
this.focus();
},
@@ -60,44 +63,20 @@ export default {
<div class="common-note-form">
<label class="sr-only" for="issue-description">{{ __('Description') }}</label>
<markdown-editor
- v-if="glFeatures.contentEditorOnIssues"
+ :enable-content-editor="Boolean(glFeatures.contentEditorOnIssues)"
class="gl-mt-3"
:value="value"
:render-markdown-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
:form-field-props="formFieldProps"
:enable-autocomplete="enableAutocomplete"
+ :autocomplete-data-sources="autocompleteDataSources"
supports-quick-actions
autofocus
+ data-qa-selector="description_field"
@input="$emit('input', $event)"
@keydown.meta.enter="updateIssuable"
@keydown.ctrl.enter="updateIssuable"
/>
- <markdown-field
- v-else
- class="gl-mt-3"
- :markdown-preview-path="markdownPreviewPath"
- :markdown-docs-path="markdownDocsPath"
- supports-quick-actions
- :can-attach-file="canAttachFile"
- :enable-autocomplete="enableAutocomplete"
- :textarea-value="value"
- >
- <template #textarea>
- <textarea
- v-bind="formFieldProps"
- ref="textarea"
- :value="value"
- class="note-textarea js-gfm-input js-autosize markdown-area"
- data-qa-selector="description_field"
- dir="auto"
- data-supports-quick-actions="true"
- @input="$emit('input', $event.target.value)"
- @keydown.meta.enter="updateIssuable"
- @keydown.ctrl.enter="updateIssuable"
- >
- </textarea>
- </template>
- </markdown-field>
</div>
</template>
diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb
index 577bd04d656..b3a1b510db9 100644
--- a/app/controllers/explore/projects_controller.rb
+++ b/app/controllers/explore/projects_controller.rb
@@ -10,6 +10,7 @@ class Explore::ProjectsController < Explore::ApplicationController
MIN_SEARCH_LENGTH = 3
PAGE_LIMIT = 50
+ RSS_ENTRIES_LIMIT = 20
before_action :set_non_archived_param
before_action :set_sorting
@@ -83,6 +84,14 @@ class Explore::ProjectsController < Explore::ApplicationController
params[:topic] = @topic.name
@projects = load_projects
+
+ respond_to do |format|
+ format.html
+ format.atom do
+ @projects = @projects.projects_order_id_desc.limit(RSS_ENTRIES_LIMIT)
+ render layout: 'xml'
+ end
+ end
end
private
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 478a2ba622c..bbbf14bb0d0 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -346,8 +346,7 @@ class IssuableFinder
def use_full_text_search?
klass.try(:pg_full_text_searchable_columns).present? &&
- params[:search] =~ FULL_TEXT_SEARCH_TERM_REGEX &&
- Feature.enabled?(:issues_full_text_search, params.project || params.group)
+ params[:search] =~ FULL_TEXT_SEARCH_TERM_REGEX
end
def filter_by_full_text_search(items)
diff --git a/app/views/explore/projects/_project.atom.builder b/app/views/explore/projects/_project.atom.builder
new file mode 100644
index 00000000000..f0500901a73
--- /dev/null
+++ b/app/views/explore/projects/_project.atom.builder
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+xml.entry do
+ xml.title project.name
+ xml.link href: project_url(project), rel: "alternate", type: "text/html"
+ xml.id project_url(project)
+ xml.updated project.created_at
+
+ if project.description.present?
+ xml.summary(type: "xhtml") do |summary|
+ summary << project.description
+ end
+ end
+end
diff --git a/app/views/explore/projects/topic.atom.builder b/app/views/explore/projects/topic.atom.builder
new file mode 100644
index 00000000000..4712d415daa
--- /dev/null
+++ b/app/views/explore/projects/topic.atom.builder
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+xml.title @topic.name
+xml.link href: topic_explore_projects_url(@topic.name, rss_url_options), rel: "self", type: "application/atom+xml"
+xml.link href: topic_explore_projects_url(@topic.name), rel: "alternate", type: "text/html"
+xml.id topic_explore_projects_url(@topic.id)
+xml.updated @projects[0].updated_at.xmlschema if @projects[0]
+
+xml << render(@projects) if @projects.any?
diff --git a/app/views/explore/projects/topic.html.haml b/app/views/explore/projects/topic.html.haml
index b26abefcb0e..bba0c1b9ca0 100644
--- a/app/views/explore/projects/topic.html.haml
+++ b/app/views/explore/projects/topic.html.haml
@@ -22,9 +22,11 @@
%div{ class: container_class }
.gl-py-5.gl-border-gray-100.gl-border-b-solid.gl-border-b-1
%h3.gl-m-0= _('Projects with this topic')
- .top-area.gl-pt-2.gl-pb-2
+ .top-area.gl-pt-2.gl-pb-2.gl-justify-content-space-between
.nav-controls
= render 'shared/projects/search_form'
= render 'filter'
+ = link_to topic_explore_projects_path(@topic.name, rss_url_options), title: s_("Topics|Subscribe to the new projects feed"), class: 'btn gl-button btn-default btn-icon d-none d-sm-inline-flex has-tooltip' do
+ = sprite_icon('rss', css_class: 'gl-icon')
= render 'projects', projects: @projects
diff --git a/config/feature_flags/development/issues_full_text_search.yml b/config/feature_flags/development/issues_full_text_search.yml
deleted file mode 100644
index 31fe543e35e..00000000000
--- a/config/feature_flags/development/issues_full_text_search.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: issues_full_text_search
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71913
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/354784
-milestone: '14.5'
-type: development
-group: group::project management
-default_enabled: true
diff --git a/config/routes/explore.rb b/config/routes/explore.rb
index 6ddf4d23138..6777571bb68 100644
--- a/config/routes/explore.rb
+++ b/config/routes/explore.rb
@@ -6,7 +6,7 @@ namespace :explore do
get :trending
get :starred
get :topics
- get 'topics/:topic_name', action: :topic, as: :topic, constraints: { topic_name: /.+/ }
+ get 'topics/:topic_name', action: :topic, as: :topic, constraints: { format: /(html|atom)/, topic_name: /.+?/ }
end
end
diff --git a/doc/administration/backup_restore/backup_gitlab.md b/doc/administration/backup_restore/backup_gitlab.md
index 5cdfbefff1a..a2d77fcf33d 100644
--- a/doc/administration/backup_restore/backup_gitlab.md
+++ b/doc/administration/backup_restore/backup_gitlab.md
@@ -1345,7 +1345,9 @@ runner authentication, which is described in more detail in the following
sections. After resetting the tokens, you should be able to visit your project
and the jobs begin running again.
-Use the information in the following sections at your own risk.
+WARNING:
+The steps in this section can potentially lead to **data loss** on the above listed items.
+Consider opening a [Support Request](https://support.gitlab.com/hc/en-us/requests/new) if you're a Premium or Ultimate customer.
#### Verify that all values can be decrypted
@@ -1620,6 +1622,7 @@ This problem stops the backup script from completing. To fix this problem, you m
WARNING:
The steps in this section can potentially lead to **data loss**. All steps must be followed strictly in the order given.
+Consider opening a [Support Request](https://support.gitlab.com/hc/en-us/requests/new) if you're a Premium or Ultimate customer.
Truncating filenames to resolve the error involves:
diff --git a/doc/administration/geo/replication/troubleshooting.md b/doc/administration/geo/replication/troubleshooting.md
index 641170bf425..aa19047a768 100644
--- a/doc/administration/geo/replication/troubleshooting.md
+++ b/doc/administration/geo/replication/troubleshooting.md
@@ -51,7 +51,7 @@ Geo::MetricsUpdateWorker.new.perform
If it raises an error, then the error is probably also preventing the jobs from completing. If it takes longer than 10 minutes, then there may be a performance issue, and the UI may always show "Unhealthy" even if the status eventually does get updated.
-If it successfully updates the status, then something may be wrong with Sidekiq. Is it running? Do the logs show errors? This job is supposed to be enqueued every minute. It takes an exclusive lease in Redis to ensure that only one of these jobs can run at a time. The primary site updates its status directly in the PostgreSQL database. Secondary sites send an HTTP Post request to the primary site with their status data.
+If it successfully updates the status, then something may be wrong with Sidekiq. Is it running? Do the logs show errors? This job is supposed to be enqueued every minute and might not run if a [job deduplication idempotency](../../sidekiq/sidekiq_troubleshooting.md#clearing-a-sidekiq-job-deduplication-idempotency-key) key was not cleared properly. It takes an exclusive lease in Redis to ensure that only one of these jobs can run at a time. The primary site updates its status directly in the PostgreSQL database. Secondary sites send an HTTP Post request to the primary site with their status data.
A site also shows as "Unhealthy" if certain health checks fail. You can reveal the failure by running the following in the [Rails console](../../operations/rails_console.md) on the affected secondary site:
diff --git a/doc/administration/sidekiq/sidekiq_troubleshooting.md b/doc/administration/sidekiq/sidekiq_troubleshooting.md
index 315714cd00b..6802bb68b31 100644
--- a/doc/administration/sidekiq/sidekiq_troubleshooting.md
+++ b/doc/administration/sidekiq/sidekiq_troubleshooting.md
@@ -529,6 +529,32 @@ The list of available jobs can be found in the [workers](https://gitlab.com/gitl
For more information about Sidekiq jobs, see the [Sidekiq-cron](https://github.com/sidekiq-cron/sidekiq-cron#work-with-job) documentation.
+## Clearing a Sidekiq job deduplication idempotency key
+
+Occasionally, jobs that are expected to run (for example, cron jobs) are observed to not run at all. When checking the logs, there might be instances where jobs are seen to not run with a `"job_status": "deduplicated"`.
+
+This can happen when a job failed and the idempotency key was not cleared properly. For example, [stopping Sidekiq kills any remaining jobs after 25 seconds](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/4918).
+
+[By default, the key expires after 6 hours](https://gitlab.com/gitlab-org/gitlab/-/blob/87c92f06eb92716a26679cd339f3787ae7edbdc3/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb#L23),
+but if you want to clear the idempotency key immediately, follow the following steps (the example provided is for `Geo::VerificationBatchWorker`):
+
+1. Find the worker class and `args` of the job in the Sidekiq logs:
+
+ ```plaintext
+ { ... "class":"Geo::VerificationBatchWorker","args":["container_repository"] ... }
+ ```
+
+1. Start a [Rails console session](../operations/rails_console.md#starting-a-rails-console-session).
+1. Run the following snippet:
+
+ ```ruby
+ worker_class = Geo::VerificationBatchWorker
+ args = ["container_repository"]
+ dj = Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob.new({ 'class' => worker_class.name, 'args' => args }, worker_class.queue)
+ dj.send(:idempotency_key)
+ dj.delete!
+ ```
+
## Omnibus GitLab 14.0 and later: remove the `sidekiq-cluster` service
Omnibus GitLab instances that were configured to run `sidekiq-cluster` prior to GitLab 14.0
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 4c892754ff1..ad111312f86 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -12635,7 +12635,8 @@ Represents a list for an issue board.
| <a id="boardlistmilestone"></a>`milestone` | [`Milestone`](#milestone) | Milestone of the list. |
| <a id="boardlistposition"></a>`position` | [`Int`](#int) | Position of list within the board. |
| <a id="boardlisttitle"></a>`title` | [`String!`](#string) | Title of the list. |
-| <a id="boardlisttotalweight"></a>`totalWeight` | [`Int`](#int) | Total weight of all issues in the list. |
+| <a id="boardlisttotalissueweight"></a>`totalIssueWeight` | [`BigInt`](#bigint) | Total weight of all issues in the list, encoded as a string. |
+| <a id="boardlisttotalweight"></a>`totalWeight` **{warning-solid}** | [`Int`](#int) | **Deprecated** in 16.2. Use `totalIssueWeight`. |
#### Fields with arguments
diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md
index b62852c506d..fece0c8fe66 100644
--- a/doc/ci/yaml/index.md
+++ b/doc/ci/yaml/index.md
@@ -2902,6 +2902,12 @@ in the project.
Use `pages` to define a [GitLab Pages](../../user/project/pages/index.md) job that
uploads static content to GitLab. The content is then published as a website.
+You must:
+
+- Define [`artifacts`](#artifacts) with a path to the content directory, which is
+ `public` by default.
+- Use [`publish`](#pagespublish) if want to use a different content directory.
+
**Keyword type**: Job name.
**Example of `pages`**:
@@ -2910,9 +2916,7 @@ uploads static content to GitLab. The content is then published as a website.
pages:
stage: deploy
script:
- - mkdir .public
- - cp -r * .public
- - mv .public public
+ - mv my-html-content public
artifacts:
paths:
- public
@@ -2921,15 +2925,38 @@ pages:
environment: production
```
-This example moves all files from the root of the project to the `public/` directory.
-The `.public` workaround is so `cp` does not also copy `public/` to itself in an infinite loop.
+This example moves all files from a `my-html-content/` directory to the `public/` directory.
+This directory is exported as an artifact and published with GitLab Pages.
-**Additional details**:
+#### `pages:publish`
-You must:
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/415821) in GitLab 16.1.
+
+Use `publish` to configure the content directory of a [`pages` job](#pages).
+
+**Keyword type**: Job keyword. You can use it only as part of a `pages` job.
+
+**Possible inputs**: A path to a directory containing the Pages content.
+
+**Example of `publish`**:
+
+```yaml
+pages:
+ stage: deploy
+ script:
+ - npx @11ty/eleventy --input=path/to/eleventy/root --output=dist
+ artifacts:
+ paths:
+ - dist
+ publish: dist
+ rules:
+ - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
+ environment: production
+```
-- Place any static content in a `public/` directory.
-- Define [`artifacts`](#artifacts) with a path to the `public/` directory.
+This example uses [Eleventy](https://www.11ty.dev) to generate a static website and
+output the generated HTML files into a the `dist/` directory. This directory is exported
+as an artifact and published with GitLab Pages.
### `parallel`
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index 8cfd8094614..97cafefb45d 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -11,4 +11,4 @@ This document was moved to [another location](../administration/backup_restore/i
<!-- This redirect file can be deleted after <2023-09-26>. -->
<!-- Redirects that point to other docs in the same project expire in three months. -->
<!-- Redirects that point to docs in a different project or site (for example, link is not relative and starts with `https:`) expire in one year. -->
-<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html --> \ No newline at end of file
+<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html -->
diff --git a/doc/user/project/working_with_projects.md b/doc/user/project/working_with_projects.md
index 5bd7c12ed31..ea937ed9244 100644
--- a/doc/user/project/working_with_projects.md
+++ b/doc/user/project/working_with_projects.md
@@ -55,6 +55,10 @@ To explore project topics:
The **Explore topics** page shows a list of topics, sorted by the number of associated projects.
+If you want to know when new projects are added to the topic, you can use
+its RSS feed for that, which is the RSS icon on the right of the filter
+bar.
+
You can assign topics to a project on the [Project Settings page](settings/index.md#assign-topics-to-a-project).
If you're an instance administrator, you can administer all project topics from the
diff --git a/doc/user/search/index.md b/doc/user/search/index.md
index 2278da9a714..9e3c2d9c0d4 100644
--- a/doc/user/search/index.md
+++ b/doc/user/search/index.md
@@ -135,6 +135,7 @@ and gives you the option to return to the search results page.
> - [Removed support for partial matches in issue searches](https://gitlab.com/gitlab-org/gitlab/-/issues/273784) in GitLab 14.9 [with a flag](../../administration/feature_flags.md) named `issues_full_text_search`. Disabled by default.
> - Feature flag [`issues_full_text_search` enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/273784) in GitLab 14.10.
> - Feature flag [`issues_full_text_search` enabled on self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/273784) in GitLab 15.2.
+> - Feature flag [`issues_full_text_search` removed](https://gitlab.com/gitlab-org/gitlab/-/issues/273784) in GitLab 16.2.
You can filter issues and merge requests by specific terms included in titles or descriptions.
diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb
index 93d58710b0c..b5b7d94b4d0 100644
--- a/lib/gitlab/gitaly_client/repository_service.rb
+++ b/lib/gitlab/gitaly_client/repository_service.rb
@@ -52,6 +52,12 @@ module Gitlab
response.size
end
+ def repository_info
+ request = Gitaly::RepositoryInfoRequest.new(repository: @gitaly_repo)
+
+ gitaly_client_call(@storage, :repository_service, :repository_info, request, timeout: GitalyClient.long_timeout)
+ end
+
def get_object_directory_size
request = Gitaly::GetObjectDirectorySizeRequest.new(repository: @gitaly_repo)
response = gitaly_client_call(@storage, :repository_service, :get_object_directory_size, request, timeout: GitalyClient.medium_timeout)
diff --git a/lib/gitlab/github_import/importer/issue_importer.rb b/lib/gitlab/github_import/importer/issue_importer.rb
index b477468d327..a537841ecf3 100644
--- a/lib/gitlab/github_import/importer/issue_importer.rb
+++ b/lib/gitlab/github_import/importer/issue_importer.rb
@@ -31,7 +31,7 @@ module Gitlab
if (issue_id = create_issue)
create_assignees(issue_id)
issuable_finder.cache_database_id(issue_id)
- update_search_data(issue_id) if Feature.enabled?(:issues_full_text_search)
+ update_search_data(issue_id)
end
end
end
diff --git a/lib/gitlab/spamcheck/client.rb b/lib/gitlab/spamcheck/client.rb
index 0afaf46fa9b..d13c3be0a09 100644
--- a/lib/gitlab/spamcheck/client.rb
+++ b/lib/gitlab/spamcheck/client.rb
@@ -58,7 +58,7 @@ module Gitlab
pb.title = spammable.spam_title || '' if pb.respond_to?(:title)
pb.description = spammable.spam_description || '' if pb.respond_to?(:description)
pb.text = spammable.spammable_text || '' if pb.respond_to?(:text)
- pb.type = spammable.spammable_entity_type if pb.respond_to?(:type)
+ pb.type = spammable.to_ability_name if pb.respond_to?(:type)
pb.created_at = convert_to_pb_timestamp(spammable.created_at) if spammable.created_at
pb.updated_at = convert_to_pb_timestamp(spammable.updated_at) if spammable.updated_at
pb.action = ACTION_MAPPING.fetch(context.fetch(:action)) if context.has_key?(:action)
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index ca754fe8e64..a7fcca6d652 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -48299,6 +48299,9 @@ msgstr ""
msgid "Topics could not be merged!"
msgstr ""
+msgid "Topics|Subscribe to the new projects feed"
+msgstr ""
+
msgid "Total"
msgstr ""
diff --git a/qa/qa/page/component/members/invite_members_modal.rb b/qa/qa/page/component/members/invite_members_modal.rb
index 0220bb1e4fc..ae51213b3e2 100644
--- a/qa/qa/page/component/members/invite_members_modal.rb
+++ b/qa/qa/page/component/members/invite_members_modal.rb
@@ -6,6 +6,7 @@ module QA
module Members
module InviteMembersModal
extend QA::Page::PageConcern
+ include QA::Page::Component::Dropdown
def self.included(base)
super
@@ -16,11 +17,6 @@ module QA
element :invite_members_modal_content
end
- base.view 'app/assets/javascripts/invite_members/components/group_select.vue' do
- element :group_select_dropdown_search_field
- element :group_select_dropdown_item
- end
-
base.view 'app/assets/javascripts/invite_members/components/members_token_select.vue' do
element :members_token_select_input
end
@@ -61,11 +57,9 @@ module QA
within_element(:invite_members_modal_content) do
click_button 'Select a group'
- Support::Waiter.wait_until { has_element?(:group_select_dropdown_item) }
-
- fill_element :group_select_dropdown_search_field, group_name
Support::WaitForRequests.wait_for_requests
- click_button group_name
+
+ search_and_select(group_name)
set_access_level(access_level)
end
diff --git a/scripts/build_assets_image b/scripts/build_assets_image
index 00f21e5dede..f43f930d64a 100755
--- a/scripts/build_assets_image
+++ b/scripts/build_assets_image
@@ -33,6 +33,11 @@ if ([ "${CI_PROJECT_NAME}" = "gitlab" ] && [ "${FOSS_ONLY}" != "1" ]) || ([ "${C
ASSETS_IMAGE_NAME="gitlab-assets-ee"
fi
+# Generate this image for https://jihulab.com/gitlab-cn/gitlab and https://gitlab.com/gitlab-jh/jh-team/gitlab
+if (["${CI_PROJECT_NAMESPACE}" = "gitlab-cn"] || ["${CI_PROJECT_NAMESPACE}" = "gitlab-jh"]); then
+ ASSETS_IMAGE_NAME="gitlab-assets-jh"
+fi
+
ASSETS_IMAGE_PATH="${CI_REGISTRY}/${CI_PROJECT_PATH}/${ASSETS_IMAGE_NAME}"
# Used in MR pipelines
diff --git a/spec/controllers/dashboard_controller_spec.rb b/spec/controllers/dashboard_controller_spec.rb
index 304e08f40bd..52f6f08e17a 100644
--- a/spec/controllers/dashboard_controller_spec.rb
+++ b/spec/controllers/dashboard_controller_spec.rb
@@ -16,22 +16,7 @@ RSpec.describe DashboardController, feature_category: :code_review_workflow do
end
describe 'GET issues' do
- context 'when issues_full_text_search is disabled' do
- before do
- stub_feature_flags(issues_full_text_search: false)
- end
-
- it_behaves_like 'issuables list meta-data', :issue, :issues
- end
-
- context 'when issues_full_text_search is enabled' do
- before do
- stub_feature_flags(issues_full_text_search: true)
- end
-
- it_behaves_like 'issuables list meta-data', :issue, :issues
- end
-
+ it_behaves_like 'issuables list meta-data', :issue, :issues
it_behaves_like 'issuables requiring filter', :issues
it 'includes tasks in issue list' do
diff --git a/spec/controllers/explore/projects_controller_spec.rb b/spec/controllers/explore/projects_controller_spec.rb
index e73e115b77d..68ae1ca218b 100644
--- a/spec/controllers/explore/projects_controller_spec.rb
+++ b/spec/controllers/explore/projects_controller_spec.rb
@@ -122,6 +122,59 @@ RSpec.describe Explore::ProjectsController, feature_category: :groups_and_projec
end
end
end
+
+ describe 'GET #topic.atom' do
+ context 'when topic does not exist' do
+ it 'renders a 404 error' do
+ get :topic, format: :atom, params: { topic_name: 'topic1' }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when topic exists' do
+ let(:topic) { create(:topic, name: 'topic1') }
+ let_it_be(:older_project) { create(:project, :public, updated_at: 1.day.ago) }
+ let_it_be(:newer_project) { create(:project, :public, updated_at: 2.days.ago) }
+
+ before do
+ create(:project_topic, project: older_project, topic: topic)
+ create(:project_topic, project: newer_project, topic: topic)
+ end
+
+ it 'renders the template' do
+ get :topic, format: :atom, params: { topic_name: 'topic1' }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template('topic', layout: :xml)
+ end
+
+ it 'sorts repos by descending creation date' do
+ get :topic, format: :atom, params: { topic_name: 'topic1' }
+
+ expect(assigns(:projects)).to match_array [newer_project, older_project]
+ end
+
+ it 'finds topic by case insensitive name' do
+ get :topic, format: :atom, params: { topic_name: 'TOPIC1' }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template('topic', layout: :xml)
+ end
+
+ describe 'when topic contains more than 20 projects' do
+ before do
+ create_list(:project, 22, :public, topics: [topic])
+ end
+
+ it 'does not assigns more than 20 projects' do
+ get :topic, format: :atom, params: { topic_name: 'topic1' }
+
+ expect(assigns(:projects).count).to be(20)
+ end
+ end
+ end
+ end
end
shared_examples "blocks high page numbers" do
diff --git a/spec/features/atom/topics_spec.rb b/spec/features/atom/topics_spec.rb
new file mode 100644
index 00000000000..078c5b55eeb
--- /dev/null
+++ b/spec/features/atom/topics_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe "Topic Feed", feature_category: :groups_and_projects do
+ let_it_be(:topic) { create(:topic, name: 'test-topic', title: 'Test topic') }
+ let_it_be(:empty_topic) { create(:topic, name: 'test-empty-topic', title: 'Test empty topic') }
+ let_it_be(:project1) { create(:project, :public, topic_list: topic.name) }
+ let_it_be(:project2) { create(:project, :public, topic_list: topic.name) }
+
+ context 'when topic does not exist' do
+ let(:path) { topic_explore_projects_path(topic_name: 'non-existing', format: 'atom') }
+
+ it 'renders 404' do
+ visit path
+
+ expect(status_code).to eq(404)
+ end
+ end
+
+ context 'when topic exists' do
+ before do
+ visit topic_explore_projects_path(topic_name: topic.name, format: 'atom')
+ end
+
+ it "renders topic atom feed" do
+ expect(body).to have_selector('feed title')
+ end
+
+ it "has project entries" do
+ expect(body).to have_content(project1.name)
+ expect(body).to have_content(project2.name)
+ end
+ end
+
+ context 'when topic is empty' do
+ before do
+ visit topic_explore_projects_path(topic_name: empty_topic.name, format: 'atom')
+ end
+
+ it "renders topic atom feed" do
+ expect(body).to have_selector('feed title')
+ end
+
+ it "has no project entry" do
+ expect(body).to have_no_selector('entry')
+ end
+ end
+end
diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb
index a65befc3115..b9562f12ef2 100644
--- a/spec/features/issues/filtered_search/filter_issues_spec.rb
+++ b/spec/features/issues/filtered_search/filter_issues_spec.rb
@@ -455,19 +455,6 @@ RSpec.describe 'Filter issues', :js, feature_category: :team_planning do
expect(page).to have_content(issue.title)
end
- it 'filters issues by searched text containing special characters' do
- stub_feature_flags(issues_full_text_search: false)
-
- issue = create(:issue, project: project, author: user, title: "issue with !@\#{$%^&*()-+")
-
- search = '!@#{$%^&*()-+'
- submit_search_term(search)
-
- expect_issues_list_count(1)
- expect_search_term(search)
- expect(page).to have_content(issue.title)
- end
-
it 'does not show any issues' do
search = 'testing'
submit_search_term(search)
diff --git a/spec/frontend/invite_members/components/group_select_spec.js b/spec/frontend/invite_members/components/group_select_spec.js
index a1ca9a69926..3186e5c6212 100644
--- a/spec/frontend/invite_members/components/group_select_spec.js
+++ b/spec/frontend/invite_members/components/group_select_spec.js
@@ -1,82 +1,68 @@
-import { GlAvatarLabeled, GlDropdown, GlSearchBoxByType } from '@gitlab/ui';
+import { nextTick } from 'vue';
+import { GlAvatarLabeled, GlCollapsibleListbox } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
-import * as groupsApi from '~/api/groups_api';
+import { getGroups } from '~/api/groups_api';
import GroupSelect from '~/invite_members/components/group_select.vue';
+jest.mock('~/api/groups_api');
+
const group1 = { id: 1, full_name: 'Group One', avatar_url: 'test' };
const group2 = { id: 2, full_name: 'Group Two', avatar_url: 'test' };
const allGroups = [group1, group2];
-const createComponent = (props = {}) => {
- return mount(GroupSelect, {
- propsData: {
- invalidGroups: [],
- ...props,
- },
- });
-};
-
describe('GroupSelect', () => {
let wrapper;
- beforeEach(() => {
- jest.spyOn(groupsApi, 'getGroups').mockResolvedValue(allGroups);
+ const createComponent = (props = {}) => {
+ wrapper = mount(GroupSelect, {
+ propsData: {
+ selectedGroup: {},
+ invalidGroups: [],
+ ...props,
+ },
+ });
+ };
- wrapper = createComponent();
+ beforeEach(() => {
+ getGroups.mockResolvedValueOnce(allGroups);
});
- const findSearchBoxByType = () => wrapper.findComponent(GlSearchBoxByType);
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findDropdownToggle = () => findDropdown().find('button[aria-haspopup="menu"]');
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findListboxToggle = () => findListbox().find('button[aria-haspopup="listbox"]');
const findAvatarByLabel = (text) =>
wrapper
.findAllComponents(GlAvatarLabeled)
.wrappers.find((dropdownItemWrapper) => dropdownItemWrapper.props('label') === text);
- it('renders GlSearchBoxByType with default attributes', () => {
- expect(findSearchBoxByType().exists()).toBe(true);
- expect(findSearchBoxByType().vm.$attrs).toMatchObject({
- placeholder: 'Search groups',
- });
- });
-
describe('when user types in the search input', () => {
- let resolveApiRequest;
-
- beforeEach(() => {
- jest.spyOn(groupsApi, 'getGroups').mockImplementation(
- () =>
- new Promise((resolve) => {
- resolveApiRequest = resolve;
- }),
- );
-
- findSearchBoxByType().vm.$emit('input', group1.name);
+ beforeEach(async () => {
+ createComponent();
+ await waitForPromises();
+ getGroups.mockClear();
+ getGroups.mockReturnValueOnce(new Promise(() => {}));
+ findListbox().vm.$emit('search', group1.name);
+ await nextTick();
});
it('calls the API', () => {
- resolveApiRequest({ data: allGroups });
-
- expect(groupsApi.getGroups).toHaveBeenCalledWith(group1.name, {
+ expect(getGroups).toHaveBeenCalledWith(group1.name, {
exclude_internal: true,
active: true,
order_by: 'similarity',
});
});
- it('displays loading icon while waiting for API call to resolve', async () => {
- expect(findSearchBoxByType().props('isLoading')).toBe(true);
-
- resolveApiRequest({ data: allGroups });
- await waitForPromises();
-
- expect(findSearchBoxByType().props('isLoading')).toBe(false);
+ it('displays loading icon while waiting for API call to resolve', () => {
+ expect(findListbox().props('searching')).toBe(true);
});
});
describe('avatar label', () => {
- it('includes the correct attributes with name and avatar_url', () => {
+ it('includes the correct attributes with name and avatar_url', async () => {
+ createComponent();
+ await waitForPromises();
+
expect(findAvatarByLabel(group1.full_name).attributes()).toMatchObject({
src: group1.avatar_url,
'entity-id': `${group1.id}`,
@@ -86,8 +72,9 @@ describe('GroupSelect', () => {
});
describe('when filtering out the group from results', () => {
- beforeEach(() => {
- wrapper = createComponent({ invalidGroups: [group1.id] });
+ beforeEach(async () => {
+ createComponent({ invalidGroups: [group1.id] });
+ await waitForPromises();
});
it('does not find an invalid group', () => {
@@ -101,16 +88,37 @@ describe('GroupSelect', () => {
});
describe('when group is selected from the dropdown', () => {
- beforeEach(() => {
- findAvatarByLabel(group1.full_name).trigger('click');
+ beforeEach(async () => {
+ createComponent({
+ selectedGroup: {
+ value: group1.id,
+ id: group1.id,
+ name: group1.full_name,
+ path: group1.path,
+ avatarUrl: group1.avatar_url,
+ },
+ });
+ await waitForPromises();
+ findListbox().vm.$emit('select', group1.id);
+ await nextTick();
});
it('emits `input` event used by `v-model`', () => {
- expect(wrapper.emitted('input')[0][0].id).toEqual(group1.id);
+ expect(wrapper.emitted('input')).toMatchObject([
+ [
+ {
+ value: group1.id,
+ id: group1.id,
+ name: group1.full_name,
+ path: group1.path,
+ avatarUrl: group1.avatar_url,
+ },
+ ],
+ ]);
});
it('sets dropdown toggle text to selected item', () => {
- expect(findDropdownToggle().text()).toBe(group1.full_name);
+ expect(findListboxToggle().text()).toBe(group1.full_name);
});
});
});
diff --git a/spec/frontend/issues/show/components/fields/description_spec.js b/spec/frontend/issues/show/components/fields/description_spec.js
index 36074e07055..aa2571ab6d7 100644
--- a/spec/frontend/issues/show/components/fields/description_spec.js
+++ b/spec/frontend/issues/show/components/fields/description_spec.js
@@ -7,11 +7,9 @@ import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue
describe('Description field component', () => {
let wrapper;
- const findTextarea = () => wrapper.findComponent({ ref: 'textarea' });
const findMarkdownEditor = () => wrapper.findComponent(MarkdownEditor);
-
- const mountComponent = ({ description = 'test', contentEditorOnIssues = false } = {}) =>
- shallowMount(DescriptionField, {
+ const mountComponent = ({ description = 'test', contentEditorOnIssues = false } = {}) => {
+ wrapper = shallowMount(DescriptionField, {
attachTo: document.body,
propsData: {
markdownPreviewPath: '/',
@@ -27,89 +25,58 @@ describe('Description field component', () => {
MarkdownField,
},
});
+ };
beforeEach(() => {
jest.spyOn(eventHub, '$emit');
- });
-
- it('renders markdown field with description', () => {
- wrapper = mountComponent();
- expect(findTextarea().element.value).toBe('test');
+ mountComponent({ contentEditorOnIssues: true });
});
- it('renders markdown field with a markdown description', () => {
- const markdown = '**test**';
+ it('passes feature flag to the MarkdownEditorComponent', () => {
+ expect(findMarkdownEditor().props('enableContentEditor')).toBe(true);
- wrapper = mountComponent({ description: markdown });
+ mountComponent({ contentEditorOnIssues: false });
- expect(findTextarea().element.value).toBe(markdown);
+ expect(findMarkdownEditor().props('enableContentEditor')).toBe(false);
});
- it('focuses field when mounted', () => {
- wrapper = mountComponent();
-
- expect(document.activeElement).toBe(findTextarea().element);
+ it('uses the MarkdownEditor component to edit markdown', () => {
+ expect(findMarkdownEditor().props()).toMatchObject({
+ value: 'test',
+ renderMarkdownPath: '/',
+ autofocus: true,
+ supportsQuickActions: true,
+ markdownDocsPath: '/',
+ enableAutocomplete: true,
+ });
});
it('triggers update with meta+enter', () => {
- wrapper = mountComponent();
-
- findTextarea().trigger('keydown.enter', { metaKey: true });
+ findMarkdownEditor().vm.$emit('keydown', {
+ type: 'keydown',
+ keyCode: 13,
+ metaKey: true,
+ });
expect(eventHub.$emit).toHaveBeenCalledWith('update.issuable');
});
it('triggers update with ctrl+enter', () => {
- wrapper = mountComponent();
-
- findTextarea().trigger('keydown.enter', { ctrlKey: true });
+ findMarkdownEditor().vm.$emit('keydown', {
+ type: 'keydown',
+ keyCode: 13,
+ ctrlKey: true,
+ });
expect(eventHub.$emit).toHaveBeenCalledWith('update.issuable');
});
- describe('when contentEditorOnIssues feature flag is on', () => {
- beforeEach(() => {
- wrapper = mountComponent({ contentEditorOnIssues: true });
- });
+ it('emits input event when MarkdownEditor emits input event', () => {
+ const markdown = 'markdown';
- it('uses the MarkdownEditor component to edit markdown', () => {
- expect(findMarkdownEditor().props()).toMatchObject({
- value: 'test',
- renderMarkdownPath: '/',
- autofocus: true,
- supportsQuickActions: true,
- markdownDocsPath: '/',
- enableAutocomplete: true,
- });
- });
-
- it('triggers update with meta+enter', () => {
- findMarkdownEditor().vm.$emit('keydown', {
- type: 'keydown',
- keyCode: 13,
- metaKey: true,
- });
-
- expect(eventHub.$emit).toHaveBeenCalledWith('update.issuable');
- });
-
- it('triggers update with ctrl+enter', () => {
- findMarkdownEditor().vm.$emit('keydown', {
- type: 'keydown',
- keyCode: 13,
- ctrlKey: true,
- });
+ findMarkdownEditor().vm.$emit('input', markdown);
- expect(eventHub.$emit).toHaveBeenCalledWith('update.issuable');
- });
-
- it('emits input event when MarkdownEditor emits input event', () => {
- const markdown = 'markdown';
-
- findMarkdownEditor().vm.$emit('input', markdown);
-
- expect(wrapper.emitted('input')).toEqual([[markdown]]);
- });
+ expect(wrapper.emitted('input')).toEqual([[markdown]]);
});
});
diff --git a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
index f457ba06074..08457e20ec3 100644
--- a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GitalyClient::RepositoryService do
+RSpec.describe Gitlab::GitalyClient::RepositoryService, feature_category: :gitaly do
using RSpec::Parameterized::TableSyntax
let_it_be(:project) { create(:project, :repository) }
@@ -79,6 +79,21 @@ RSpec.describe Gitlab::GitalyClient::RepositoryService do
end
end
+ describe '#repository_info' do
+ it 'sends a repository_info message' do
+ expect_any_instance_of(Gitaly::RepositoryService::Stub)
+ .to receive(:repository_info)
+ .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+ .and_call_original
+
+ response = client.repository_info
+
+ expect(response.size).to be_an(Integer)
+ expect(response.references).to be_a(Gitaly::RepositoryInfoResponse::ReferencesInfo)
+ expect(response.objects).to be_a(Gitaly::RepositoryInfoResponse::ObjectsInfo)
+ end
+ end
+
describe '#get_object_directory_size' do
it 'sends a get_object_directory_size message' do
expect_any_instance_of(Gitaly::RepositoryService::Stub)
diff --git a/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb
index 1692aac49f2..bf2ffda3bf1 100644
--- a/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb
@@ -58,54 +58,24 @@ RSpec.describe Gitlab::GithubImport::Importer::IssueImporter, :clean_gitlab_redi
describe '#execute' do
let(:importer) { described_class.new(issue, project, client) }
- context 'when :issues_full_test_search is disabled' do
- before do
- stub_feature_flags(issues_full_text_search: false)
- end
-
- it 'creates the issue and assignees but does not update search data' do
- expect(importer)
- .to receive(:create_issue)
- .and_return(10)
-
- expect(importer)
- .to receive(:create_assignees)
- .with(10)
+ it 'creates the issue and assignees and updates_search_data' do
+ expect(importer)
+ .to receive(:create_issue)
+ .and_return(10)
- expect(importer.issuable_finder)
- .to receive(:cache_database_id)
- .with(10)
+ expect(importer)
+ .to receive(:create_assignees)
+ .with(10)
- expect(importer).not_to receive(:update_search_data)
+ expect(importer.issuable_finder)
+ .to receive(:cache_database_id)
+ .with(10)
- importer.execute
- end
- end
-
- context 'when :issues_full_text_search feature is enabled' do
- before do
- stub_feature_flags(issues_full_text_search: true)
- end
+ expect(importer)
+ .to receive(:update_search_data)
+ .with(10)
- it 'creates the issue and assignees and updates_search_data' do
- expect(importer)
- .to receive(:create_issue)
- .and_return(10)
-
- expect(importer)
- .to receive(:create_assignees)
- .with(10)
-
- expect(importer.issuable_finder)
- .to receive(:cache_database_id)
- .with(10)
-
- expect(importer)
- .to receive(:update_search_data)
- .with(10)
-
- importer.execute
- end
+ importer.execute
end
end
diff --git a/spec/lib/gitlab/spamcheck/client_spec.rb b/spec/lib/gitlab/spamcheck/client_spec.rb
index 080c2803ddd..354f25c0b01 100644
--- a/spec/lib/gitlab/spamcheck/client_spec.rb
+++ b/spec/lib/gitlab/spamcheck/client_spec.rb
@@ -107,7 +107,7 @@ RSpec.describe Gitlab::Spamcheck::Client, feature_category: :instance_resiliency
before do
allow(generic_spammable).to receive_messages(
- spammable_entity_type: 'generic',
+ to_ability_name: 'generic_spammable',
spammable_text: 'generic spam',
created_at: generic_created_at,
updated_at: generic_updated_at,
@@ -152,7 +152,7 @@ RSpec.describe Gitlab::Spamcheck::Client, feature_category: :instance_resiliency
generic_pb, _ = described_class.new.send(:build_protobuf, spammable: generic_spammable, user: user, context: cxt, extra_features: {})
expect(generic_pb.text).to eq 'generic spam'
- expect(generic_pb.type).to eq 'generic'
+ expect(generic_pb.type).to eq 'generic_spammable'
expect(generic_pb.created_at).to eq timestamp_to_protobuf_timestamp(generic_created_at)
expect(generic_pb.updated_at).to eq timestamp_to_protobuf_timestamp(generic_updated_at)
expect(generic_pb.action).to be ::Spamcheck::Action.lookup(::Spamcheck::Action::CREATE)
diff --git a/spec/support/helpers/features/invite_members_modal_helpers.rb b/spec/support/helpers/features/invite_members_modal_helpers.rb
index 75573616686..deb75cffe0d 100644
--- a/spec/support/helpers/features/invite_members_modal_helpers.rb
+++ b/spec/support/helpers/features/invite_members_modal_helpers.rb
@@ -52,7 +52,7 @@ module Features
click_on 'Select a group'
wait_for_requests
- click_button name
+ find('[role="option"]', text: name).click
choose_options(role, expires_at)
submit_invites
diff --git a/spec/support/shared_examples/finders/issues_finder_shared_examples.rb b/spec/support/shared_examples/finders/issues_finder_shared_examples.rb
index 67fed00b5ca..30041456d00 100644
--- a/spec/support/shared_examples/finders/issues_finder_shared_examples.rb
+++ b/spec/support/shared_examples/finders/issues_finder_shared_examples.rb
@@ -663,18 +663,6 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context
expect(items).to contain_exactly(japanese)
end
end
-
- context 'when full-text search is disabled' do
- let(:search_term) { 'ometh' }
-
- before do
- stub_feature_flags(issues_full_text_search: false)
- end
-
- it 'allows partial word matches' do
- expect(items).to contain_exactly(english)
- end
- end
end
context 'filtering by item term in title' do
diff --git a/spec/views/explore/projects/topic.html.haml_spec.rb b/spec/views/explore/projects/topic.html.haml_spec.rb
new file mode 100644
index 00000000000..1d2085b3be6
--- /dev/null
+++ b/spec/views/explore/projects/topic.html.haml_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'explore/projects/topic.html.haml', feature_category: :groups_and_projects do
+ let(:topic) { build_stubbed(:topic, name: 'test-topic', title: 'Test topic') }
+ let(:project) { build_stubbed(:project, :public, topic_list: topic.name) }
+
+ before do
+ assign(:topic, topic)
+ assign(:projects, [project])
+
+ controller.params[:controller] = 'explore/projects'
+ controller.params[:action] = 'topic'
+
+ allow(view).to receive(:current_user).and_return(nil)
+
+ render
+ end
+
+ it 'renders atom feed button with matching path' do
+ expect(rendered).to have_link(href: topic_explore_projects_path(topic.name, format: 'atom'))
+ end
+end