+
+
+
+ {{ __('Create project label') }}
+
+
+ {{ __('Manage project labels') }}
+
+
+
+
+
+ {{ __('Create label') }}
+
+
+
+
diff --git a/app/assets/javascripts/work_items/components/work_item_links/index.js b/app/assets/javascripts/work_items/components/work_item_links/index.js
index 616dce4d2c0..05d8969a332 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/index.js
+++ b/app/assets/javascripts/work_items/components/work_item_links/index.js
@@ -15,13 +15,17 @@ export default function initWorkItemLinks() {
const {
fullPath,
+ isGroup,
+ registerPath,
+ signInPath,
+ wiCanAdminLabel,
+ wiGroupPath,
wiHasIssueWeightsFeature,
wiHasIterationsFeature,
wiHasIssuableHealthStatusFeature,
- registerPath,
- signInPath,
+ wiIssuesListPath,
+ wiLabelsManagePath,
wiReportAbusePath,
- isGroup,
} = workItemLinksRoot.dataset;
return new Vue({
@@ -30,13 +34,18 @@ export default function initWorkItemLinks() {
apolloProvider,
provide: {
fullPath,
+ isGroup: parseBoolean(isGroup),
+ registerPath,
+ signInPath,
+ // for work item modal
+ canAdminLabel: wiCanAdminLabel,
+ groupPath: wiGroupPath,
hasIssueWeightsFeature: wiHasIssueWeightsFeature,
hasIterationsFeature: wiHasIterationsFeature,
hasIssuableHealthStatusFeature: wiHasIssuableHealthStatusFeature,
- registerPath,
- signInPath,
+ issuesListPath: wiIssuesListPath,
+ labelsManagePath: wiLabelsManagePath,
reportAbusePath: wiReportAbusePath,
- isGroup: parseBoolean(isGroup),
},
render: (createElement) =>
createElement(WorkItemLinks, {
diff --git a/app/assets/javascripts/work_items/index.js b/app/assets/javascripts/work_items/index.js
index d6b4c9ab0c1..b652fd4d8da 100644
--- a/app/assets/javascripts/work_items/index.js
+++ b/app/assets/javascripts/work_items/index.js
@@ -24,11 +24,13 @@ export const initWorkItemsRoot = ({ workItemType, workspaceType } = {}) => {
addShortcutsExtension(ShortcutsWorkItems);
const {
+ canAdminLabel,
fullPath,
groupPath,
hasIssueWeightsFeature,
iid,
issuesListPath,
+ labelsManagePath,
registerPath,
signInPath,
hasIterationsFeature,
@@ -47,11 +49,13 @@ export const initWorkItemsRoot = ({ workItemType, workspaceType } = {}) => {
router: createRouter({ fullPath, workItemType, workspaceType, defaultBranch }),
apolloProvider,
provide: {
+ canAdminLabel,
fullPath,
isGroup,
hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
hasOkrsFeature: parseBoolean(hasOkrsFeature),
issuesListPath,
+ labelsManagePath,
registerPath,
signInPath,
hasIterationsFeature: parseBoolean(hasIterationsFeature),
diff --git a/app/helpers/work_items_helper.rb b/app/helpers/work_items_helper.rb
index 9e6cbebffd2..890cf25ccf6 100644
--- a/app/helpers/work_items_helper.rb
+++ b/app/helpers/work_items_helper.rb
@@ -5,10 +5,13 @@ module WorkItemsHelper
group = resource_parent.is_a?(Group) ? resource_parent : resource_parent.group
{
+ can_admin_label: can?(current_user, :admin_label, resource_parent).to_s,
full_path: resource_parent.full_path,
group_path: group&.full_path,
issues_list_path:
resource_parent.is_a?(Group) ? issues_group_path(resource_parent) : project_issues_path(resource_parent),
+ labels_manage_path:
+ resource_parent.is_a?(Group) ? group_labels_path(resource_parent) : project_labels_path(resource_parent),
register_path: new_user_registration_path(redirect_to_referer: 'yes'),
sign_in_path: new_session_path(:user, redirect_to_referer: 'yes'),
new_comment_template_paths: new_comment_template_paths(group).to_json,
diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml
index 9f42897d1da..6f79e883833 100644
--- a/app/views/admin/groups/index.html.haml
+++ b/app/views/admin/groups/index.html.haml
@@ -1,18 +1,25 @@
- page_title _("Groups")
- add_page_specific_style 'page_bundles/search'
-.top-area
- .gl-mt-3.gl-mb-3
- = form_tag admin_groups_path, method: :get, class: 'js-search-form' do |f|
- = hidden_field_tag :sort, @sort
- .search-holder
- .search-field-holder
- = search_field_tag :name, params[:name].presence, class: "form-control search-text-input js-search-input", autofocus: true, spellcheck: false, placeholder: 'Search by name', data: { testid: 'group-search-field' }
- = sprite_icon('search', css_class: 'search-icon')
- = render "shared/groups/dropdown", options_hash: admin_groups_sort_options_hash
- = render Pajamas::ButtonComponent.new(variant: :confirm, href: new_admin_group_path) do
- = _('New group')
-%ul.content-list
- = render @groups
+= render ::Layouts::PageHeadingComponent.new(_('Groups')) do |c|
+ - c.with_actions do
+ = link_button_to new_admin_group_path, variant: :confirm do
+ = _('New group')
+
+.md:gl-flex.gl-min-w-0.gl-grow.row-content-block
+ = form_tag admin_groups_path, method: :get, class: 'js-search-form gl-w-full' do |f|
+ = hidden_field_tag :sort, @sort
+ .search-holder
+ .search-field-holder
+ = search_field_tag :name, params[:name].presence, class: "form-control search-text-input js-search-input", spellcheck: false, placeholder: 'Search by name', data: { testid: 'group-search-field' }
+ = sprite_icon('search', css_class: 'search-icon')
+ = render "shared/groups/dropdown", options_hash: admin_groups_sort_options_hash
+
+- if @groups.any?
+ %ul.content-list
+ = render @groups
+- else
+ = render Pajamas::EmptyStateComponent.new(svg_path: 'illustrations/empty-state/empty-groups-md.svg',
+ title: _('No groups found'))
= paginate @groups, theme: "gitlab"
diff --git a/app/views/projects/issues/_related_issues.html.haml b/app/views/projects/issues/_related_issues.html.haml
index 73a88e63a4e..f4a53496d95 100644
--- a/app/views/projects/issues/_related_issues.html.haml
+++ b/app/views/projects/issues/_related_issues.html.haml
@@ -4,7 +4,8 @@
full_path: @project.full_path,
has_issue_weights_feature: @project.licensed_feature_available?(:issue_weights).to_s,
help_path: help_page_path('user/project/issues/related_issues'),
+ is_group: 'false',
issuable_type: @issue.issue_type,
show_categorized_issues: @project.licensed_feature_available?(:blocked_issues).to_s,
has_iterations_feature: @project.licensed_feature_available?(:iterations).to_s,
- report_abuse_path: add_category_abuse_reports_path } }
+ wi: work_items_show_data(@project) } }
diff --git a/config/feature_flags/gitlab_com_derisk/ci_expand_nested_resource_group_variables.yml b/config/feature_flags/gitlab_com_derisk/ci_expand_nested_resource_group_variables.yml
deleted file mode 100644
index 749d42fe485..00000000000
--- a/config/feature_flags/gitlab_com_derisk/ci_expand_nested_resource_group_variables.yml
+++ /dev/null
@@ -1,9 +0,0 @@
----
-name: ci_expand_nested_resource_group_variables
-feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/361438
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/155594
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/466168
-milestone: '17.1'
-group: group::pipeline security
-type: gitlab_com_derisk
-default_enabled: false
diff --git a/doc/administration/geo/secondary_proxy/index.md b/doc/administration/geo/secondary_proxy/index.md
index fdc83632457..7b4565ef2ee 100644
--- a/doc/administration/geo/secondary_proxy/index.md
+++ b/doc/administration/geo/secondary_proxy/index.md
@@ -104,8 +104,8 @@ Considering that web traffic is proxied to the primary, the behavior of the seco
site is inaccessible:
- UI and API traffic return the same errors as the primary (or fail if the primary is not accessible at all), since they are proxied.
-- For repositories that already exist on the specific secondary site being accessed, Git read operations still work as expected,
- including authentication through HTTP(s) or SSH.
+- For repositories that are fully up-to-date on the specific secondary site being accessed, Git read operations still work as expected,
+ including authentication through HTTP(s) or SSH. However, Git reads performed by GitLab Runners will fail.
- Git operations for repositories that are not replicated to the secondary site return the same errors
as the primary site, since they are proxied.
- All Git write operations return the same errors as the primary site, since they are proxied.
diff --git a/doc/security/hardening_configuration_recommendations.md b/doc/security/hardening_configuration_recommendations.md
index c4eae062ef7..10110fec53a 100644
--- a/doc/security/hardening_configuration_recommendations.md
+++ b/doc/security/hardening_configuration_recommendations.md
@@ -51,7 +51,7 @@ the security of NGINX itself:
nginx['ssl_session_timeout'] = "5m"
# Should prevent logjam attack etc
- nginx['ssl_dhparam'] = "/etc/gitlab/ssl/dhparams.pem" # changed from nil
+ nginx['ssl_dhparam'] = "/etc/gitlab/ssl/dhparam.pem" # changed from nil
# Turn off session ticket reuse
nginx['ssl_session_tickets'] = "off"
diff --git a/doc/topics/git/clone.md b/doc/topics/git/clone.md
index ddf25db8723..fae8e0af2b5 100644
--- a/doc/topics/git/clone.md
+++ b/doc/topics/git/clone.md
@@ -88,3 +88,199 @@ For example:
```shell
git clone https://
:@gitlab.example.com/tanuki/awesome_project.git
```
+
+## Reduce clone size
+
+As Git repositories grow in size, they can become cumbersome to work with
+because of:
+
+- The large amount of history that must be downloaded.
+- The large amount of disk space they require.
+
+[Partial clone](https://git-scm.com/docs/partial-clone)
+is a performance optimization that allows Git to function without having a
+complete copy of the repository. The goal of this work is to allow Git better
+handle extremely large repositories.
+
+Git 2.22.0 or later is required.
+
+### Filter by file size
+
+Storing large binary files in Git is usually discouraged, because every large
+file added is downloaded by everyone who clones or fetches changes
+thereafter. These downloads are slow and problematic, especially when working from a slow
+or unreliable internet connection.
+
+Using partial clone with a file size filter solves this problem, by excluding
+troublesome large files from clones and fetches. When Git encounters a missing
+file, it's downloaded on demand.
+
+When cloning a repository, use the `--filter=blob:limit=` argument. For example,
+to clone the repository excluding files larger than 1 megabyte:
+
+```shell
+git clone --filter=blob:limit=1m git@gitlab.com:gitlab-com/www-gitlab-com.git
+```
+
+This would produce the following output:
+
+```shell
+Cloning into 'www-gitlab-com'...
+remote: Enumerating objects: 832467, done.
+remote: Counting objects: 100% (832467/832467), done.
+remote: Compressing objects: 100% (207226/207226), done.
+remote: Total 832467 (delta 585563), reused 826624 (delta 580099), pack-reused 0
+Receiving objects: 100% (832467/832467), 2.34 GiB | 5.05 MiB/s, done.
+Resolving deltas: 100% (585563/585563), done.
+remote: Enumerating objects: 146, done.
+remote: Counting objects: 100% (146/146), done.
+remote: Compressing objects: 100% (138/138), done.
+remote: Total 146 (delta 8), reused 144 (delta 8), pack-reused 0
+Receiving objects: 100% (146/146), 471.45 MiB | 4.60 MiB/s, done.
+Resolving deltas: 100% (8/8), done.
+Updating files: 100% (13008/13008), done.
+Filtering content: 100% (3/3), 131.24 MiB | 4.65 MiB/s, done.
+```
+
+The output is longer because Git:
+
+1. Clones the repository excluding files larger than 1 megabyte.
+1. Downloads any missing large files needed to check out the default branch.
+
+When changing branches, Git may download more missing files.
+
+### Filter by object type
+
+For repositories with millions of files and a long history, you can exclude all files and use
+[`git sparse-checkout`](https://git-scm.com/docs/git-sparse-checkout) to reduce the size of
+your working copy.
+
+```shell
+# Clone the repo excluding all files
+$ git clone --filter=blob:none --sparse git@gitlab.com:gitlab-com/www-gitlab-com.git
+Cloning into 'www-gitlab-com'...
+remote: Enumerating objects: 678296, done.
+remote: Counting objects: 100% (678296/678296), done.
+remote: Compressing objects: 100% (165915/165915), done.
+remote: Total 678296 (delta 472342), reused 673292 (delta 467476), pack-reused 0
+Receiving objects: 100% (678296/678296), 81.06 MiB | 5.74 MiB/s, done.
+Resolving deltas: 100% (472342/472342), done.
+remote: Enumerating objects: 28, done.
+remote: Counting objects: 100% (28/28), done.
+remote: Compressing objects: 100% (25/25), done.
+remote: Total 28 (delta 0), reused 12 (delta 0), pack-reused 0
+Receiving objects: 100% (28/28), 140.29 KiB | 341.00 KiB/s, done.
+Updating files: 100% (28/28), done.
+
+$ cd www-gitlab-com
+
+$ git sparse-checkout set data --cone
+remote: Enumerating objects: 301, done.
+remote: Counting objects: 100% (301/301), done.
+remote: Compressing objects: 100% (292/292), done.
+remote: Total 301 (delta 16), reused 102 (delta 9), pack-reused 0
+Receiving objects: 100% (301/301), 1.15 MiB | 608.00 KiB/s, done.
+Resolving deltas: 100% (16/16), done.
+Updating files: 100% (302/302), done.
+```
+
+For more details, see the Git documentation for
+[`sparse-checkout`](https://git-scm.com/docs/git-sparse-checkout).
+
+### Filter by file path
+
+Deeper integration between partial clone and sparse checkout is possible through the
+`--filter=sparse:oid=` filter spec. This mode of filtering uses a format similar to a
+`.gitignore` file to specify which files to include when cloning and fetching.
+
+WARNING:
+Partial clone using `sparse` filters is still experimental. It might be slow and significantly increase
+[Gitaly](../../administration/gitaly/index.md) resource utilization when cloning and fetching.
+[Filter all blobs and use sparse-checkout](#filter-by-object-type) instead, because
+[`git-sparse-checkout`](https://git-scm.com/docs/git-sparse-checkout) simplifies
+this type of partial clone use and overcomes its limitations.
+
+For more details, see the Git documentation for
+[`rev-list-options`](https://git-scm.com/docs/git-rev-list#Documentation/git-rev-list.txt---filterltfilter-specgt).
+
+1. Create a filter spec. For example, consider a monolithic repository with many applications,
+ each in a different subdirectory in the root. Create a file `shiny-app/.filterspec`:
+
+ ```plaintext
+ # Only the paths listed in the file will be downloaded when performing a
+ # partial clone using `--filter=sparse:oid=shiny-app/.gitfilterspec`
+
+ # Explicitly include filterspec needed to configure sparse checkout with
+ # git config --local core.sparsecheckout true
+ # git show master:snazzy-app/.gitfilterspec >> .git/info/sparse-checkout
+ shiny-app/.gitfilterspec
+
+ # Shiny App
+ shiny-app/
+
+ # Dependencies
+ shimmery-app/
+ shared-component-a/
+ shared-component-b/
+ ```
+
+1. Clone and filter by path. Support for `--filter=sparse:oid` using the
+ clone command is not fully integrated with sparse checkout.
+
+ ```shell
+ # Clone the filtered set of objects using the filterspec stored on the
+ # server. WARNING: this step may be very slow!
+ git clone --sparse --filter=sparse:oid=master:shiny-app/.gitfilterspec
+
+ # Optional: observe there are missing objects that we have not fetched
+ git rev-list --all --quiet --objects --missing=print | wc -l
+ ```
+
+ WARNING:
+ Git integrations with `bash`, Zsh, etc and editors that automatically
+ show Git status information often run `git fetch` which fetches the
+ entire repository. Disabling or reconfiguring these integrations might be required.
+
+### Remove partial clone filtering
+
+Git repositories with partial clone filtering can have the filtering removed. To
+remove filtering:
+
+1. Fetch everything that has been excluded by the filters, to make sure that the
+ repository is complete. If `git sparse-checkout` was used, use
+ `git sparse-checkout disable` to disable it. See the
+ [`disable` documentation](https://git-scm.com/docs/git-sparse-checkout#Documentation/git-sparse-checkout.txt-emdisableem)
+ for more information.
+
+ Then do a regular `fetch` to ensure that the repository is complete. To check if
+ there are missing objects to fetch, and then fetch them, especially when not using
+ `git sparse-checkout`, the following commands can be used:
+
+ ```shell
+ # Show missing objects
+ git rev-list --objects --all --missing=print | grep -e '^\?'
+
+ # Show missing objects without a '?' character before them (needs GNU grep)
+ git rev-list --objects --all --missing=print | grep -oP '^\?\K\w+'
+
+ # Fetch missing objects
+ git fetch origin $(git rev-list --objects --all --missing=print | grep -oP '^\?\K\w+')
+
+ # Show number of missing objects
+ git rev-list --objects --all --missing=print | grep -e '^\?' | wc -l
+ ```
+
+1. Repack everything. This can be done using `git repack -a -d`, for example. This
+ should leave only three files in `.git/objects/pack/`:
+ - A `pack-.pack` file.
+ - Its corresponding `pack-.idx` file.
+ - A `pack-.promisor` file.
+
+1. Delete the `.promisor` file. The above step should have left only one
+ `pack-.promisor` file, which should be empty and should be deleted.
+
+1. Remove partial clone configuration. The partial clone-related configuration
+ variables should be removed from Git configuration files. Usually only the following
+ configuration must be removed:
+ - `remote.origin.promisor`.
+ - `remote.origin.partialclonefilter`.
diff --git a/doc/topics/git/partial_clone.md b/doc/topics/git/partial_clone.md
index 3e59031d4f6..cf03fe26d07 100644
--- a/doc/topics/git/partial_clone.md
+++ b/doc/topics/git/partial_clone.md
@@ -1,201 +1,11 @@
---
-stage: Create
-group: Source Code
-info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments"
+redirect_to: 'clone.md'
+remove_date: '2024-09-25'
---
-# Use partial clones to reduce clone size
+This document was moved to [another location](clone.md).
-As Git repositories grow in size, they can become cumbersome to work with
-because of:
-
-- The large amount of history that must be downloaded.
-- The large amount of disk space they require.
-
-[Partial clone](https://git-scm.com/docs/partial-clone)
-is a performance optimization that allows Git to function without having a
-complete copy of the repository. The goal of this work is to allow Git better
-handle extremely large repositories.
-
-Git 2.22.0 or later is required.
-
-## Filter by file size
-
-Storing large binary files in Git is usually discouraged, because every large
-file added is downloaded by everyone who clones or fetches changes
-thereafter. These downloads are slow and problematic, especially when working from a slow
-or unreliable internet connection.
-
-Using partial clone with a file size filter solves this problem, by excluding
-troublesome large files from clones and fetches. When Git encounters a missing
-file, it's downloaded on demand.
-
-When cloning a repository, use the `--filter=blob:limit=` argument. For example,
-to clone the repository excluding files larger than 1 megabyte:
-
-```shell
-git clone --filter=blob:limit=1m git@gitlab.com:gitlab-com/www-gitlab-com.git
-```
-
-This would produce the following output:
-
-```shell
-Cloning into 'www-gitlab-com'...
-remote: Enumerating objects: 832467, done.
-remote: Counting objects: 100% (832467/832467), done.
-remote: Compressing objects: 100% (207226/207226), done.
-remote: Total 832467 (delta 585563), reused 826624 (delta 580099), pack-reused 0
-Receiving objects: 100% (832467/832467), 2.34 GiB | 5.05 MiB/s, done.
-Resolving deltas: 100% (585563/585563), done.
-remote: Enumerating objects: 146, done.
-remote: Counting objects: 100% (146/146), done.
-remote: Compressing objects: 100% (138/138), done.
-remote: Total 146 (delta 8), reused 144 (delta 8), pack-reused 0
-Receiving objects: 100% (146/146), 471.45 MiB | 4.60 MiB/s, done.
-Resolving deltas: 100% (8/8), done.
-Updating files: 100% (13008/13008), done.
-Filtering content: 100% (3/3), 131.24 MiB | 4.65 MiB/s, done.
-```
-
-The output is longer because Git:
-
-1. Clones the repository excluding files larger than 1 megabyte.
-1. Downloads any missing large files needed to check out the default branch.
-
-When changing branches, Git may download more missing files.
-
-## Filter by object type
-
-For repositories with millions of files and a long history, you can exclude all files and use
-[`git sparse-checkout`](https://git-scm.com/docs/git-sparse-checkout) to reduce the size of
-your working copy.
-
-```shell
-# Clone the repo excluding all files
-$ git clone --filter=blob:none --sparse git@gitlab.com:gitlab-com/www-gitlab-com.git
-Cloning into 'www-gitlab-com'...
-remote: Enumerating objects: 678296, done.
-remote: Counting objects: 100% (678296/678296), done.
-remote: Compressing objects: 100% (165915/165915), done.
-remote: Total 678296 (delta 472342), reused 673292 (delta 467476), pack-reused 0
-Receiving objects: 100% (678296/678296), 81.06 MiB | 5.74 MiB/s, done.
-Resolving deltas: 100% (472342/472342), done.
-remote: Enumerating objects: 28, done.
-remote: Counting objects: 100% (28/28), done.
-remote: Compressing objects: 100% (25/25), done.
-remote: Total 28 (delta 0), reused 12 (delta 0), pack-reused 0
-Receiving objects: 100% (28/28), 140.29 KiB | 341.00 KiB/s, done.
-Updating files: 100% (28/28), done.
-
-$ cd www-gitlab-com
-
-$ git sparse-checkout set data --cone
-remote: Enumerating objects: 301, done.
-remote: Counting objects: 100% (301/301), done.
-remote: Compressing objects: 100% (292/292), done.
-remote: Total 301 (delta 16), reused 102 (delta 9), pack-reused 0
-Receiving objects: 100% (301/301), 1.15 MiB | 608.00 KiB/s, done.
-Resolving deltas: 100% (16/16), done.
-Updating files: 100% (302/302), done.
-```
-
-For more details, see the Git documentation for
-[`sparse-checkout`](https://git-scm.com/docs/git-sparse-checkout).
-
-## Filter by file path
-
-Deeper integration between partial clone and sparse checkout is possible through the
-`--filter=sparse:oid=` filter spec. This mode of filtering uses a format similar to a
-`.gitignore` file to specify which files to include when cloning and fetching.
-
-WARNING:
-Partial clone using `sparse` filters is still experimental. It might be slow and significantly increase
-[Gitaly](../../administration/gitaly/index.md) resource utilization when cloning and fetching.
-[Filter all blobs and use sparse-checkout](#filter-by-object-type) instead, because
-[`git-sparse-checkout`](https://git-scm.com/docs/git-sparse-checkout) simplifies
-this type of partial clone use and overcomes its limitations.
-
-For more details, see the Git documentation for
-[`rev-list-options`](https://git-scm.com/docs/git-rev-list#Documentation/git-rev-list.txt---filterltfilter-specgt).
-
-1. Create a filter spec. For example, consider a monolithic repository with many applications,
- each in a different subdirectory in the root. Create a file `shiny-app/.filterspec`:
-
- ```plaintext
- # Only the paths listed in the file will be downloaded when performing a
- # partial clone using `--filter=sparse:oid=shiny-app/.gitfilterspec`
-
- # Explicitly include filterspec needed to configure sparse checkout with
- # git config --local core.sparsecheckout true
- # git show master:snazzy-app/.gitfilterspec >> .git/info/sparse-checkout
- shiny-app/.gitfilterspec
-
- # Shiny App
- shiny-app/
-
- # Dependencies
- shimmery-app/
- shared-component-a/
- shared-component-b/
- ```
-
-1. Clone and filter by path. Support for `--filter=sparse:oid` using the
- clone command is not fully integrated with sparse checkout.
-
- ```shell
- # Clone the filtered set of objects using the filterspec stored on the
- # server. WARNING: this step may be very slow!
- git clone --sparse --filter=sparse:oid=master:shiny-app/.gitfilterspec
-
- # Optional: observe there are missing objects that we have not fetched
- git rev-list --all --quiet --objects --missing=print | wc -l
- ```
-
- WARNING:
- Git integrations with `bash`, Zsh, etc and editors that automatically
- show Git status information often run `git fetch` which fetches the
- entire repository. Disabling or reconfiguring these integrations might be required.
-
-## Remove partial clone filtering
-
-Git repositories with partial clone filtering can have the filtering removed. To
-remove filtering:
-
-1. Fetch everything that has been excluded by the filters, to make sure that the
- repository is complete. If `git sparse-checkout` was used, use
- `git sparse-checkout disable` to disable it. See the
- [`disable` documentation](https://git-scm.com/docs/git-sparse-checkout#Documentation/git-sparse-checkout.txt-emdisableem)
- for more information.
-
- Then do a regular `fetch` to ensure that the repository is complete. To check if
- there are missing objects to fetch, and then fetch them, especially when not using
- `git sparse-checkout`, the following commands can be used:
-
- ```shell
- # Show missing objects
- git rev-list --objects --all --missing=print | grep -e '^\?'
-
- # Show missing objects without a '?' character before them (needs GNU grep)
- git rev-list --objects --all --missing=print | grep -oP '^\?\K\w+'
-
- # Fetch missing objects
- git fetch origin $(git rev-list --objects --all --missing=print | grep -oP '^\?\K\w+')
-
- # Show number of missing objects
- git rev-list --objects --all --missing=print | grep -e '^\?' | wc -l
- ```
-
-1. Repack everything. This can be done using `git repack -a -d`, for example. This
- should leave only three files in `.git/objects/pack/`:
- - A `pack-.pack` file.
- - Its corresponding `pack-.idx` file.
- - A `pack-.promisor` file.
-
-1. Delete the `.promisor` file. The above step should have left only one
- `pack-.promisor` file, which should be empty and should be deleted.
-
-1. Remove partial clone configuration. The partial clone-related configuration
- variables should be removed from Git configuration files. Usually only the following
- configuration must be removed:
- - `remote.origin.promisor`.
- - `remote.origin.partialclonefilter`.
+
+
+
+
diff --git a/lib/gitlab/ci/pipeline/seed/processable/resource_group.rb b/lib/gitlab/ci/pipeline/seed/processable/resource_group.rb
index 2821c83db9e..0b9d0416e9a 100644
--- a/lib/gitlab/ci/pipeline/seed/processable/resource_group.rb
+++ b/lib/gitlab/ci/pipeline/seed/processable/resource_group.rb
@@ -28,13 +28,7 @@ module Gitlab
def expanded_resource_group_key
strong_memoize(:expanded_resource_group_key) do
- variable_set = if Feature.enabled?(:ci_expand_nested_resource_group_variables, processable.project)
- variables.sort_and_expand_all
- else
- variables
- end
-
- ExpandVariables.expand(resource_group_key, -> { variable_set })
+ ExpandVariables.expand(resource_group_key, -> { variables.sort_and_expand_all })
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 5438500b217..3fcc724499f 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -20563,6 +20563,9 @@ msgstr ""
msgid "Environments|Search by environment name"
msgstr ""
+msgid "Environments|See all environments."
+msgstr ""
+
msgid "Environments|Select Flux resource"
msgstr ""
@@ -20584,6 +20587,9 @@ msgstr ""
msgid "Environments|Show all"
msgstr ""
+msgid "Environments|Showing %{listedEnvironmentsCount} of %{totalEnvironmentsCount} environments in this folder."
+msgstr ""
+
msgid "Environments|Stop"
msgstr ""
@@ -30441,6 +30447,9 @@ msgid_plural "Labels added: %{labels}"
msgstr[0] ""
msgstr[1] ""
+msgid "Label name"
+msgstr ""
+
msgid "Label priority"
msgstr ""
@@ -33464,9 +33473,6 @@ msgid_plural "MlModelRegistry|%d versions"
msgstr[0] ""
msgstr[1] ""
-msgid "MlModelRegistry|A semver version like 1.0.0"
-msgstr ""
-
msgid "MlModelRegistry|Add a model"
msgstr ""
@@ -33557,7 +33563,10 @@ msgstr ""
msgid "MlModelRegistry|Enter a model description"
msgstr ""
-msgid "MlModelRegistry|Enter a semver version."
+msgid "MlModelRegistry|Enter a semantic version."
+msgstr ""
+
+msgid "MlModelRegistry|Enter a semantic version. Latest version is %{latestVersion}"
msgstr ""
msgid "MlModelRegistry|Enter some description"
@@ -33581,6 +33590,9 @@ msgstr ""
msgid "MlModelRegistry|File \"%{name}\" is %{size}. It is larger than max allowed size of %{maxAllowedFileSize}"
msgstr ""
+msgid "MlModelRegistry|For example 1.0.0"
+msgstr ""
+
msgid "MlModelRegistry|For example 1.0.0. Must be a semantic version."
msgstr ""
@@ -33599,9 +33611,6 @@ msgstr ""
msgid "MlModelRegistry|Latest version"
msgstr ""
-msgid "MlModelRegistry|Latest version is %{latestVersion}"
-msgstr ""
-
msgid "MlModelRegistry|Leave empty to skip version creation."
msgstr ""
@@ -33707,6 +33716,12 @@ msgstr ""
msgid "MlModelRegistry|Version description"
msgstr ""
+msgid "MlModelRegistry|Version is not a valid semantic version."
+msgstr ""
+
+msgid "MlModelRegistry|Version is valid semantic version."
+msgstr ""
+
msgid "MlModelRegistry|Versions"
msgstr ""
@@ -33961,9 +33976,6 @@ msgstr ""
msgid "Name must start with a letter, digit, emoji, or underscore."
msgstr ""
-msgid "Name new label"
-msgstr ""
-
msgid "Name the deployment (must be unique)"
msgstr ""
@@ -34790,6 +34802,9 @@ msgstr ""
msgid "No group provided"
msgstr ""
+msgid "No groups found"
+msgstr ""
+
msgid "No issues found"
msgstr ""
diff --git a/qa/qa/specs/features/browser_ui/2_plan/project_wiki/project_based_page_deletion_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/project_wiki/project_based_page_deletion_spec.rb
index 1fd7c548b21..88dfe14537f 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/project_wiki/project_based_page_deletion_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/project_wiki/project_based_page_deletion_spec.rb
@@ -12,7 +12,7 @@ module QA
Flow::Login.sign_in
end
- it 'can delete a page', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347815' do
+ it 'can delete a page', :blocking, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347815' do
initial_wiki.visit!
Page::Project::Wiki::Show.perform(&:click_edit)
diff --git a/spec/features/boards/sidebar_labels_spec.rb b/spec/features/boards/sidebar_labels_spec.rb
index 45b0163dfb6..79dbb999295 100644
--- a/spec/features/boards/sidebar_labels_spec.rb
+++ b/spec/features/boards/sidebar_labels_spec.rb
@@ -212,7 +212,7 @@ RSpec.describe 'Project issue boards sidebar labels', :js, feature_category: :po
wait_for_requests
click_on 'Create project label'
- fill_in 'Name new label', with: 'test label'
+ fill_in 'Label name', with: 'test label'
first('.suggested-colors a').click
click_button 'Create'
wait_for_requests
diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb
index 7fd405685a4..335ebb5c249 100644
--- a/spec/features/issues/form_spec.rb
+++ b/spec/features/issues/form_spec.rb
@@ -575,7 +575,7 @@ RSpec.describe 'New/edit issue', :js, feature_category: :team_planning do
within_testid 'sidebar-labels' do
click_button _('Create project label')
- fill_in _('Name new label'), with: 'test label'
+ fill_in _('Label name'), with: 'test label'
first('.suggest-colors-dropdown a').click
click_button 'Create'
end
diff --git a/spec/frontend/environments/environment_folder_spec.js b/spec/frontend/environments/environment_folder_spec.js
index ca697300df6..61185a03c03 100644
--- a/spec/frontend/environments/environment_folder_spec.js
+++ b/spec/frontend/environments/environment_folder_spec.js
@@ -5,7 +5,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { stubTransition } from 'helpers/stub_transition';
-import { __, s__ } from '~/locale';
+import { __, s__, sprintf } from '~/locale';
import EnvironmentsFolder from '~/environments/components/environment_folder.vue';
import EnvironmentItem from '~/environments/components/new_environment_item.vue';
import { resolvedEnvironmentsApp, resolvedFolder } from './graphql/mock_data';
@@ -18,7 +18,17 @@ describe('~/environments/components/environments_folder.vue', () => {
let intervalMock;
let nestedEnvironment;
- const findLink = () => wrapper.findByRole('link', { name: s__('Environments|Show all') });
+ const findLink = () => wrapper.findByText(s__('Environments|See all environments.'));
+ const findMessage = () =>
+ wrapper.findByText(
+ sprintf(
+ s__(
+ 'Environments|Showing %{listedEnvironmentsCount} of %{totalEnvironmentsCount} environments in this folder.',
+ ),
+ { listedEnvironmentsCount: 2, totalEnvironmentsCount: 4 },
+ ),
+ );
+ const findFolderMessageElement = () => wrapper.findByTestId('environment-folder-message-element');
const createApolloProvider = () => {
const mockResolvers = { Query: { folder: environmentFolderMock, interval: intervalMock } };
@@ -77,13 +87,10 @@ describe('~/environments/components/environments_folder.vue', () => {
});
it('is collapsed by default', () => {
- const link = findLink();
-
expect(collapse.props('visible')).toBe(false);
const iconNames = icons.wrappers.map((i) => i.props('name')).slice(0, 2);
expect(iconNames).toEqual(['chevron-lg-right', 'folder-o']);
expect(folderName.classes('gl-font-bold')).toBe(false);
- expect(link.exists()).toBe(false);
});
it('opens on click and starts polling', async () => {
@@ -93,14 +100,11 @@ describe('~/environments/components/environments_folder.vue', () => {
jest.advanceTimersByTime(2000);
await waitForPromises();
- const link = findLink();
-
expect(button.attributes('aria-label')).toBe(__('Collapse'));
expect(collapse.props('visible')).toBe(true);
const iconNames = icons.wrappers.map((i) => i.props('name')).slice(0, 2);
expect(iconNames).toEqual(['chevron-lg-down', 'folder-open']);
expect(folderName.classes('gl-font-bold')).toBe(true);
- expect(link.attributes('href')).toBe(nestedEnvironment.latest.folderPath);
expect(environmentFolderMock).toHaveBeenCalledTimes(2);
});
@@ -129,6 +133,36 @@ describe('~/environments/components/environments_folder.vue', () => {
expect(environmentFolderMock).toHaveBeenCalledTimes(2);
});
+
+ describe('when there are more environments to show inside the folder', () => {
+ it('displays the message with the correct text and a link to navigate to all environments', async () => {
+ environmentFolderMock.mockReturnValue({ ...resolvedFolder, activeCount: 4 });
+ wrapper = createWrapper({ scope: 'active', nestedEnvironment }, createApolloProvider());
+
+ await nextTick();
+ await waitForPromises();
+
+ const link = findLink();
+ const message = findMessage();
+ const element = findFolderMessageElement();
+
+ expect(element.exists()).toBe(true);
+ expect(link.attributes('href')).toBe(nestedEnvironment.latest.folderPath);
+ expect(message.exists()).toBe(true);
+ });
+ });
+
+ describe('when all available environments inside the folder are shown', () => {
+ it("doesn't display the message and a link", () => {
+ const link = findLink();
+ const message = findMessage();
+ const element = findFolderMessageElement();
+
+ expect(element.exists()).toBe(false);
+ expect(link.exists()).toBe(false);
+ expect(message.exists()).toBe(false);
+ });
+ });
});
});
diff --git a/spec/frontend/ml/model_registry/components/model_version_create_spec.js b/spec/frontend/ml/model_registry/components/model_version_create_spec.js
index fb8200fbafe..b4be74e8d32 100644
--- a/spec/frontend/ml/model_registry/components/model_version_create_spec.js
+++ b/spec/frontend/ml/model_registry/components/model_version_create_spec.js
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { GlAlert, GlModal } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@@ -98,10 +98,14 @@ describe('ModelVersionCreate', () => {
expect(findVersionInput().exists()).toBe(true);
});
- it('renders the version input label', () => {
+ it('renders the version input label for initial state', () => {
expect(wrapper.findByTestId('versionDescriptionId').attributes().description).toBe(
- 'Enter a semver version.',
+ 'Enter a semantic version.',
);
+ expect(wrapper.findByTestId('versionDescriptionId').attributes('invalid-feedback')).toBe(
+ '',
+ );
+ expect(wrapper.findByTestId('versionDescriptionId').attributes('valid-feedback')).toBe('');
});
it('renders the description input', () => {
@@ -124,9 +128,9 @@ describe('ModelVersionCreate', () => {
});
});
- it('renders the create button in the modal', () => {
+ it('disables the create button in the modal when semver is incorrect', () => {
expect(findGlModal().props('actionPrimary')).toEqual({
- attributes: { variant: 'confirm' },
+ attributes: { variant: 'confirm', disabled: true },
text: 'Create & import',
});
});
@@ -147,6 +151,47 @@ describe('ModelVersionCreate', () => {
});
});
+ describe('It reacts to semantic version input', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+ it('renders the version input label for initial state', () => {
+ expect(wrapper.findByTestId('versionDescriptionId').attributes('invalid-feedback')).toBe('');
+ expect(findGlModal().props('actionPrimary')).toEqual({
+ attributes: { variant: 'confirm', disabled: true },
+ text: 'Create & import',
+ });
+ });
+ it.each(['1.0', '1', 'abc', '1.abc', '1.0.0.0'])(
+ 'renders the version input label for invalid state',
+ async (version) => {
+ findVersionInput().vm.$emit('input', version);
+ await nextTick();
+ expect(wrapper.findByTestId('versionDescriptionId').attributes('invalid-feedback')).toBe(
+ 'Version is not a valid semantic version.',
+ );
+ expect(findGlModal().props('actionPrimary')).toEqual({
+ attributes: { variant: 'confirm', disabled: true },
+ text: 'Create & import',
+ });
+ },
+ );
+ it.each(['1.0.0', '0.0.0-b', '24.99.99-b99'])(
+ 'renders the version input label for valid state',
+ async (version) => {
+ findVersionInput().vm.$emit('input', version);
+ await nextTick();
+ expect(wrapper.findByTestId('versionDescriptionId').attributes('valid-feedback')).toBe(
+ 'Version is valid semantic version.',
+ );
+ expect(findGlModal().props('actionPrimary')).toEqual({
+ attributes: { variant: 'confirm', disabled: false },
+ text: 'Create & import',
+ });
+ },
+ );
+ });
+
describe('Latest version available', () => {
beforeEach(() => {
createWrapper(undefined, { latestVersion: '1.2.3' });
@@ -154,7 +199,7 @@ describe('ModelVersionCreate', () => {
it('renders the version input label', () => {
expect(wrapper.findByTestId('versionDescriptionId').attributes().description).toBe(
- 'Latest version is 1.2.3',
+ 'Enter a semantic version. Latest version is 1.2.3',
);
});
});
diff --git a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_create_view_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_create_view_spec.js
index bcef99afc46..3fb9c2b5306 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_create_view_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_create_view_spec.js
@@ -144,7 +144,7 @@ describe('DropdownContentsCreateView', () => {
const titleInputEl = wrapper.find('.dropdown-input').findComponent(GlFormInput);
expect(titleInputEl.exists()).toBe(true);
- expect(titleInputEl.attributes('placeholder')).toBe('Name new label');
+ expect(titleInputEl.attributes('placeholder')).toBe('Label name');
expect(titleInputEl.attributes('autofocus')).toBe('true');
});
diff --git a/spec/frontend/vue_shared/components/page_heading_spec.js b/spec/frontend/vue_shared/components/page_heading_spec.js
index f415f5fb397..ddea94bde5f 100644
--- a/spec/frontend/vue_shared/components/page_heading_spec.js
+++ b/spec/frontend/vue_shared/components/page_heading_spec.js
@@ -14,14 +14,21 @@ describe('Pagination links component', () => {
`;
+ const headingTemplate = `
+
+ Heading with custom elements here
+
+ `;
+
describe('Ordered Layout', () => {
let wrapper;
- const createWrapper = () => {
+ const createWrapper = (scopedSlots = {}) => {
wrapper = shallowMountExtended(PageHeading, {
scopedSlots: {
actions: actionsTemplate,
description: descriptionTemplate,
+ ...scopedSlots,
},
propsData: {
heading: 'Page heading',
@@ -52,6 +59,12 @@ describe('Pagination links component', () => {
expect(description().text()).toBe('Description go here');
expect(description().classes()).toEqual(['gl-w-full', 'gl-mt-2', 'gl-text-secondary']);
});
+
+ it('renders the heading slot if provided', () => {
+ createWrapper({ heading: headingTemplate });
+
+ expect(heading().text()).toBe('Heading with custom elements here');
+ });
});
});
});
diff --git a/spec/frontend/work_items/components/shared/work_item_sidebar_dropdown_widget_spec.js b/spec/frontend/work_items/components/shared/work_item_sidebar_dropdown_widget_spec.js
index 84caf77c658..d716530185c 100644
--- a/spec/frontend/work_items/components/shared/work_item_sidebar_dropdown_widget_spec.js
+++ b/spec/frontend/work_items/components/shared/work_item_sidebar_dropdown_widget_spec.js
@@ -229,4 +229,30 @@ describe('WorkItemSidebarDropdownWidget component', () => {
expect(findCollapsibleListbox().props('toggleText')).toBe('No iteration');
});
});
+
+ describe('watcher', () => {
+ describe('when createdLabelId prop is updated', () => {
+ it('appends itself to the selected items list', async () => {
+ createComponent({
+ isEditing: true,
+ itemValue: ['gid://gitlab/Label/11', 'gid://gitlab/Label/22'],
+ multiSelect: true,
+ });
+ await nextTick();
+
+ expect(findCollapsibleListbox().props('selected')).toEqual([
+ 'gid://gitlab/Label/11',
+ 'gid://gitlab/Label/22',
+ ]);
+
+ await wrapper.setProps({ createdLabelId: 'gid://gitlab/Label/33' });
+
+ expect(findCollapsibleListbox().props('selected')).toEqual([
+ 'gid://gitlab/Label/11',
+ 'gid://gitlab/Label/22',
+ 'gid://gitlab/Label/33',
+ ]);
+ });
+ });
+ });
});
diff --git a/spec/frontend/work_items/components/work_item_labels_spec.js b/spec/frontend/work_items/components/work_item_labels_spec.js
index 1a571f7252d..2a4d90ce7cc 100644
--- a/spec/frontend/work_items/components/work_item_labels_spec.js
+++ b/spec/frontend/work_items/components/work_item_labels_spec.js
@@ -1,5 +1,5 @@
-import { GlLabel } from '@gitlab/ui';
-import Vue from 'vue';
+import { GlDisclosureDropdown, GlLabel } from '@gitlab/ui';
+import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -9,6 +9,7 @@ import {
TRACKING_CATEGORY_SHOW,
I18N_WORK_ITEM_ERROR_FETCHING_LABELS,
} from '~/work_items/constants';
+import DropdownContentsCreateView from '~/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue';
import groupLabelsQuery from '~/sidebar/components/labels/labels_select_widget/graphql/group_labels.query.graphql';
import projectLabelsQuery from '~/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
@@ -32,6 +33,7 @@ Vue.use(VueApollo);
const workItemId = 'gid://gitlab/WorkItem/1';
describe('WorkItemLabels component', () => {
+ /** @type {import('helpers/vue_test_utils_helper').ExtendedWrapper} */
let wrapper;
const label1Id = mockLabels[0].id;
@@ -85,8 +87,10 @@ describe('WorkItemLabels component', () => {
[updateWorkItemMutation, updateWorkItemMutationHandler],
]),
provide: {
+ canAdminLabel: true,
isGroup,
issuesListPath: 'test-project-path/issues',
+ labelsManagePath: 'test-project-path/labels',
},
propsData: {
workItemId,
@@ -101,8 +105,10 @@ describe('WorkItemLabels component', () => {
const findWorkItemSidebarDropdownWidget = () =>
wrapper.findComponent(WorkItemSidebarDropdownWidget);
const findAllLabels = () => wrapper.findAllComponents(GlLabel);
+ const findDisclosureDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
const findRegularLabel = () => findAllLabels().at(0);
const findLabelWithDescription = () => findAllLabels().at(2);
+ const findDropdownContentsCreateView = () => wrapper.findComponent(DropdownContentsCreateView);
const showDropdown = () => {
findWorkItemSidebarDropdownWidget().vm.$emit('dropdownShown');
@@ -144,6 +150,7 @@ describe('WorkItemLabels component', () => {
headerText: 'Select labels',
resetButtonLabel: 'Clear',
multiSelect: true,
+ showFooter: true,
itemValue: [],
});
expect(findAllLabels()).toHaveLength(0);
@@ -414,4 +421,72 @@ describe('WorkItemLabels component', () => {
expect(groupLabelsQueryHandler).toHaveBeenCalled();
});
});
+
+ describe('creating project label', () => {
+ beforeEach(async () => {
+ createComponent();
+
+ wrapper.findByTestId('create-project-label').vm.$emit('click');
+ await nextTick();
+ });
+
+ describe('when "Create project label" button is clicked', () => {
+ it('renders "Create label" dropdown', () => {
+ expect(findDisclosureDropdown().props()).toMatchObject({
+ block: true,
+ startOpened: true,
+ toggleText: 'No labels',
+ });
+ expect(findDropdownContentsCreateView().props()).toEqual({
+ attrWorkspacePath: 'test-project-path',
+ fullPath: 'test-project-path',
+ labelCreateType: 'project',
+ searchKey: '',
+ workspaceType: 'project',
+ });
+ });
+ });
+
+ describe('when "hideCreateView" event is emitted', () => {
+ it('hides dropdown', async () => {
+ expect(findDisclosureDropdown().exists()).toBe(true);
+ expect(findDropdownContentsCreateView().exists()).toBe(true);
+
+ findDropdownContentsCreateView().vm.$emit('hideCreateView');
+ await nextTick();
+
+ expect(findDisclosureDropdown().exists()).toBe(false);
+ expect(findDropdownContentsCreateView().exists()).toBe(false);
+ });
+ });
+
+ describe('when "labelCreated" event is emitted', () => {
+ it('updates "createdLabelId" value and hides dropdown', async () => {
+ expect(findWorkItemSidebarDropdownWidget().props('createdLabelId')).toBe(undefined);
+ expect(findDisclosureDropdown().exists()).toBe(true);
+ expect(findDropdownContentsCreateView().exists()).toBe(true);
+
+ findDropdownContentsCreateView().vm.$emit('labelCreated', {
+ id: 'gid://gitlab/Label/55',
+ name: 'New label',
+ });
+ await nextTick();
+
+ expect(findWorkItemSidebarDropdownWidget().props('createdLabelId')).toBe(
+ 'gid://gitlab/Label/55',
+ );
+ expect(findDisclosureDropdown().exists()).toBe(false);
+ expect(findDropdownContentsCreateView().exists()).toBe(false);
+ });
+ });
+ });
+
+ it('renders "Manage project labels" link in dropdown', () => {
+ createComponent();
+
+ expect(wrapper.findByTestId('manage-project-labels').text()).toBe('Manage project labels');
+ expect(wrapper.findByTestId('manage-project-labels').attributes('href')).toBe(
+ 'test-project-path/labels',
+ );
+ });
});
diff --git a/spec/frontend/work_items/router_spec.js b/spec/frontend/work_items/router_spec.js
index 4298edf1e7c..8d9aafe4fa8 100644
--- a/spec/frontend/work_items/router_spec.js
+++ b/spec/frontend/work_items/router_spec.js
@@ -42,6 +42,7 @@ describe('Work items router', () => {
apolloProvider: createMockApollo(handlers),
router,
provide: {
+ canAdminLabel: true,
fullPath: 'full-path',
groupPath: '',
isGroup: false,
@@ -50,6 +51,7 @@ describe('Work items router', () => {
hasIterationsFeature: false,
hasOkrsFeature: false,
hasIssuableHealthStatusFeature: false,
+ labelsManagePath: 'test-project-path/labels',
reportAbusePath: '/report/abuse/path',
},
stubs: {
diff --git a/spec/helpers/work_items_helper_spec.rb b/spec/helpers/work_items_helper_spec.rb
index 535c4bfe8ae..39e6ff57346 100644
--- a/spec/helpers/work_items_helper_spec.rb
+++ b/spec/helpers/work_items_helper_spec.rb
@@ -4,36 +4,55 @@ require "spec_helper"
RSpec.describe WorkItemsHelper, feature_category: :team_planning do
include Devise::Test::ControllerHelpers
+
describe '#work_items_show_data' do
- subject(:work_items_show_data) { helper.work_items_show_data(project) }
+ describe 'with project context' do
+ let_it_be(:project) { build(:project) }
- let_it_be(:project) { build(:project) }
+ before do
+ allow(helper).to receive(:can?).and_return(true)
+ end
- it 'returns the expected data properties' do
- expect(work_items_show_data).to include(
- {
- full_path: project.full_path,
- group_path: nil,
- issues_list_path: project_issues_path(project),
- register_path: new_user_registration_path(redirect_to_referer: 'yes'),
- sign_in_path: user_session_path(redirect_to_referer: 'yes'),
- new_comment_template_paths:
- [{ text: "Your comment templates", href: profile_comment_templates_path }].to_json,
- report_abuse_path: add_category_abuse_reports_path
- }
- )
+ it 'returns the expected data properties' do
+ expect(helper.work_items_show_data(project)).to include(
+ {
+ can_admin_label: 'true',
+ full_path: project.full_path,
+ group_path: nil,
+ issues_list_path: project_issues_path(project),
+ labels_manage_path: project_labels_path(project),
+ register_path: new_user_registration_path(redirect_to_referer: 'yes'),
+ sign_in_path: user_session_path(redirect_to_referer: 'yes'),
+ new_comment_template_paths:
+ [{ text: "Your comment templates", href: profile_comment_templates_path }].to_json,
+ report_abuse_path: add_category_abuse_reports_path,
+ default_branch: project.default_branch_or_main
+ }
+ )
+ end
+
+ describe 'when project has parent group' do
+ let_it_be(:group_project) { build(:project, group: build(:group)) }
+
+ it 'returns the expected data properties' do
+ expect(helper.work_items_show_data(group_project)).to include(
+ {
+ group_path: group_project.group.full_path
+ }
+ )
+ end
+ end
end
- context 'when project is under a group' do
- let(:group) { build(:group) }
- let(:group_project) { build(:project, group: group) }
-
- subject(:work_items_show_data) { helper.work_items_show_data(group_project) }
+ context 'with group context' do
+ let_it_be(:group) { build(:group) }
it 'returns the expected group_path' do
- expect(work_items_show_data).to include(
+ expect(helper.work_items_show_data(group)).to include(
{
- group_path: group_project.group.full_path
+ issues_list_path: issues_group_path(group),
+ labels_manage_path: group_labels_path(group),
+ default_branch: nil
}
)
end
@@ -75,10 +94,12 @@ RSpec.describe WorkItemsHelper, feature_category: :team_planning do
subject(:work_items_list_data) { helper.work_items_list_data(group, current_user) }
- it 'returns expected data' do
+ before do
allow(helper).to receive(:current_user).and_return(current_user)
allow(helper).to receive(:can?).and_return(true)
+ end
+ it 'returns expected data' do
expect(work_items_list_data).to include(
{
full_path: group.full_path,
diff --git a/spec/lib/gitlab/ci/pipeline/chain/ensure_resource_groups_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/ensure_resource_groups_spec.rb
index 4417f9fa15e..ddd7796cba3 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/ensure_resource_groups_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/ensure_resource_groups_spec.rb
@@ -80,24 +80,10 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::EnsureResourceGroups do
context "when there are nested variables" do
let(:resource_group_key) { '${NESTED_VAR}_GROUP' }
- context "when the ci_expand_nested_resource_group_variables feature flag is not true" do
- before do
- stub_feature_flags(ci_expand_nested_resource_group_variables: false)
- end
-
- it 'does not expand nested variables before creating the group' do
- expect { subject }.to change { Ci::ResourceGroup.count }.by(1)
- expect(project.resource_groups.find_by_key('${VAR}ST_GROUP')).to be_present
- expect(job.options[:resource_group_key]).to be_nil
- end
- end
-
- context "when the ci_expand_nested_resource_group_variables feature flag is true" do
- it 'expands all of the nested variables before creating the group' do
- expect { subject }.to change { Ci::ResourceGroup.count }.by(1)
- expect(project.resource_groups.find_by_key('TEST_GROUP')).to be_present
- expect(job.options[:resource_group_key]).to be_nil
- end
+ it 'expands all of the nested variables before creating the group' do
+ expect { subject }.to change { Ci::ResourceGroup.count }.by(1)
+ expect(project.resource_groups.find_by_key('TEST_GROUP')).to be_present
+ expect(job.options[:resource_group_key]).to be_nil
end
end
end
diff --git a/spec/support/shared_examples/features/sidebar/sidebar_labels_shared_examples.rb b/spec/support/shared_examples/features/sidebar/sidebar_labels_shared_examples.rb
index d03da192d74..e50ac079d55 100644
--- a/spec/support/shared_examples/features/sidebar/sidebar_labels_shared_examples.rb
+++ b/spec/support/shared_examples/features/sidebar/sidebar_labels_shared_examples.rb
@@ -98,7 +98,7 @@ RSpec.shared_examples 'labels sidebar widget' do
it 'creates new label' do
page.within(labels_widget) do
- fill_in 'Name new label', with: 'wontfix'
+ fill_in 'Label name', with: 'wontfix'
click_link 'Magenta-pink'
click_button 'Create'
@@ -108,7 +108,7 @@ RSpec.shared_examples 'labels sidebar widget' do
it 'shows error message if label title is taken' do
page.within(labels_widget) do
- fill_in 'Name new label', with: development.title
+ fill_in 'Label name', with: development.title
click_link 'Magenta-pink'
click_button 'Create'
diff --git a/spec/support/shared_examples/features/work_items_shared_examples.rb b/spec/support/shared_examples/features/work_items_shared_examples.rb
index f8127e51155..7f64c23c20c 100644
--- a/spec/support/shared_examples/features/work_items_shared_examples.rb
+++ b/spec/support/shared_examples/features/work_items_shared_examples.rb
@@ -197,63 +197,54 @@ RSpec.shared_examples 'work items assignees' do
end
RSpec.shared_examples 'work items labels' do
- let(:label_title_selector) { '[data-testid="labels-title"]' }
- let(:labels_input_selector) { '[data-testid="work-item-labels-input"]' }
- let(:work_item_labels_selector) { '[data-testid="work-item-labels"]' }
+ it 'adds and removes a label' do
+ within_testid 'work-item-labels' do
+ expect(page).not_to have_css '.gl-label', text: label.title
- it 'successfully applies the label by searching' do
- expect(work_item.reload.labels).not_to include(label)
+ click_button 'Edit'
+ select_listbox_item(label.title)
+ click_button 'Apply'
- find_and_click_edit(work_item_labels_selector)
+ expect(page).to have_css '.gl-label', text: label.title
- select_listbox_item(label.title)
+ click_button 'Edit'
+ click_button 'Clear'
- find("body").click
- wait_for_all_requests
-
- expect(work_item.reload.labels).to include(label)
- within(work_item_labels_selector) do
- expect(page).to have_link(label.title)
+ expect(page).not_to have_css '.gl-label', text: label.title
end
end
- it 'successfully removes all users on clear all button click' do
- expect(work_item.reload.labels).not_to include(label)
-
- find_and_click_edit(work_item_labels_selector)
-
- select_listbox_item(label.title)
-
- find("body").click
- wait_for_requests
-
- expect(work_item.reload.labels).to include(label)
-
- find_and_click_edit(work_item_labels_selector)
-
- find_and_click_clear(work_item_labels_selector)
- wait_for_all_requests
-
- expect(work_item.reload.labels).not_to include(label)
- end
-
- it 'updates the assignee in real-time' do
+ it 'updates the assigned labels in real-time when another user updates the label' do
using_session :other_session do
visit work_items_path
- expect(work_item.reload.labels).not_to include(label)
+
+ expect(page).not_to have_css '.gl-label', text: label.title
end
- find_and_click_edit(work_item_labels_selector)
+ within_testid 'work-item-labels' do
+ click_button 'Edit'
+ select_listbox_item(label.title)
+ click_button 'Apply'
- select_listbox_item(label.title)
+ expect(page).to have_css '.gl-label', text: label.title
+ end
- find("body").click
- wait_for_all_requests
-
- expect(work_item.reload.labels).to include(label)
+ expect(page).to have_css '.gl-label', text: label.title
using_session :other_session do
- expect(work_item.reload.labels).to include(label)
+ expect(page).to have_css '.gl-label', text: label.title
+ end
+ end
+
+ it 'creates, auto-selects, and adds new label' do
+ within_testid 'work-item-labels' do
+ click_button 'Edit'
+ click_button 'Create project label'
+ send_keys 'Quintessence'
+ click_button 'Create'
+ click_button 'Apply'
+
+ expect(page).to have_css '.gl-label', text: 'Quintessence'
end
end
end
diff --git a/spec/views/projects/issues/_related_issues.html.haml_spec.rb b/spec/views/projects/issues/_related_issues.html.haml_spec.rb
index 0dbca032c4b..b5ef57b1ce7 100644
--- a/spec/views/projects/issues/_related_issues.html.haml_spec.rb
+++ b/spec/views/projects/issues/_related_issues.html.haml_spec.rb
@@ -30,7 +30,7 @@ RSpec.describe 'projects/issues/_related_issues.html.haml', feature_category: :t
render
expect(rendered).to have_selector(
- ".js-related-issues-root[data-report-abuse-path=\"#{add_category_abuse_reports_path}\"]"
+ ".js-related-issues-root[data-wi-report-abuse-path=\"#{add_category_abuse_reports_path}\"]"
)
end
end