diff --git a/.rubocop_todo/gitlab/bounded_contexts.yml b/.rubocop_todo/gitlab/bounded_contexts.yml index cb68ed0d877..0db494d6011 100644 --- a/.rubocop_todo/gitlab/bounded_contexts.yml +++ b/.rubocop_todo/gitlab/bounded_contexts.yml @@ -2836,7 +2836,6 @@ Gitlab/BoundedContexts: - 'ee/app/models/board_user_preference.rb' - 'ee/app/models/burndown.rb' - 'ee/app/models/click_house_model.rb' - - 'ee/app/models/compliance_management/compliance_framework.rb' - 'ee/app/models/compliance_management/compliance_framework/project_settings.rb' - 'ee/app/models/compliance_management/compliance_framework/security_policy.rb' - 'ee/app/models/compliance_management/framework.rb' diff --git a/app/assets/javascripts/environments/components/environment_folder.vue b/app/assets/javascripts/environments/components/environment_folder.vue index 43cb5354baa..57583afc0ba 100644 --- a/app/assets/javascripts/environments/components/environment_folder.vue +++ b/app/assets/javascripts/environments/components/environment_folder.vue @@ -1,5 +1,5 @@ @@ -233,6 +235,7 @@ export default { + + 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 = ` + + `; + 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