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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-12-01 00:15:15 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-12-01 00:15:15 +0300
commitab37c8f6370868a8316992745589167517d422b7 (patch)
tree58bdce1e126d189d874b50a0dbc3bbd3bfed064a
parent4534d890f1e1d198804e9e2ff0da76e2308ebe23 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/ci/gitlab-com/danger-review.gitlab-ci.yml2
-rw-r--r--CHANGELOG.md57
-rw-r--r--app/assets/javascripts/blob_edit/blob_bundle.js2
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js19
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue4
-rw-r--r--app/assets/javascripts/ci/pipeline_mini_graph/legacy_job_item.vue1
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue2
-rw-r--r--app/assets/javascripts/ci/reports/components/report_section.vue1
-rw-r--r--app/assets/javascripts/editor/extensions/source_editor_security_policy_schema_ext.js84
-rw-r--r--app/assets/javascripts/issues/show/components/header_actions.vue4
-rw-r--r--app/assets/javascripts/organizations/mock_data.js1
-rw-r--r--app/assets/javascripts/organizations/settings/general/components/advanced_settings.vue26
-rw-r--r--app/assets/javascripts/organizations/settings/general/components/app.vue4
-rw-r--r--app/assets/javascripts/organizations/settings/general/components/change_url.vue128
-rw-r--r--app/assets/javascripts/organizations/settings/general/graphql/mutations/update_organization.mutation.graphql1
-rw-r--r--app/assets/javascripts/organizations/shared/components/new_edit_form.vue61
-rw-r--r--app/assets/javascripts/organizations/shared/components/organization_url_field.vue58
-rw-r--r--app/assets/javascripts/organizations/shared/constants.js11
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/checks/draft.stories.js74
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/checks/draft.vue169
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/checks/i18n.js4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/checks/message.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/merge_checks.stories.js4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/draft.query.graphql3
-rw-r--r--app/controllers/projects/tags_controller.rb2
-rw-r--r--app/finders/packages/composer/packages_finder.rb8
-rw-r--r--app/finders/packages/group_packages_finder.rb19
-rw-r--r--app/graphql/types/permission_types/base_permission_type.rb2
-rw-r--r--app/helpers/blob_helper.rb1
-rw-r--r--app/helpers/groups_helper.rb11
-rw-r--r--app/models/ability.rb14
-rw-r--r--app/models/bulk_import.rb2
-rw-r--r--app/models/bulk_imports/entity.rb2
-rw-r--r--app/models/concerns/protected_branch_access.rb2
-rw-r--r--app/models/integrations/jira.rb4
-rw-r--r--app/models/pages_domain.rb5
-rw-r--r--app/models/user.rb6
-rw-r--r--app/policies/ci/pipeline_schedule_policy.rb3
-rw-r--r--app/policies/issue_policy.rb1
-rw-r--r--app/services/ci/pipeline_schedules/base_save_service.rb6
-rw-r--r--app/services/ci/pipeline_schedules/update_service.rb6
-rw-r--r--app/services/members/creator_service.rb24
-rw-r--r--app/services/projects/lfs_pointers/lfs_link_service.rb2
-rw-r--r--app/views/groups/_invite_members_modal.html.haml2
-rw-r--r--config/feature_flags/development/restrict_pipeline_cancellation_by_role.yml8
-rw-r--r--doc/administration/environment_variables.md1
-rw-r--r--doc/api/dependency_list_export.md14
-rw-r--r--doc/architecture/blueprints/new_diffs.md319
-rw-r--r--doc/architecture/blueprints/new_diffs/index.md316
-rw-r--r--doc/ci/pipelines/settings.md2
-rw-r--r--doc/gitlab-basics/add-file.md10
-rw-r--r--doc/topics/git/rollback_commits.md10
-rw-r--r--doc/topics/git/useful_git_commands.md67
-rw-r--r--doc/user/organization/index.md14
-rw-r--r--lib/bulk_imports/pipeline/runner.rb7
-rw-r--r--lib/gitlab/analytics/cycle_analytics/request_params.rb1
-rw-r--r--lib/gitlab/bitbucket_import/importers/issues_importer.rb1
-rw-r--r--lib/gitlab/bitbucket_import/importers/issues_notes_importer.rb1
-rw-r--r--lib/gitlab/bitbucket_import/importers/pull_requests_importer.rb1
-rw-r--r--lib/gitlab/bitbucket_import/importers/pull_requests_notes_importer.rb1
-rw-r--r--lib/gitlab/checks/branch_check.rb4
-rw-r--r--lib/gitlab/regex.rb4
-rw-r--r--locale/gitlab.pot47
-rw-r--r--package.json2
-rw-r--r--scripts/review_apps/base-config.yaml4
-rw-r--r--spec/controllers/projects/pipeline_schedules_controller_spec.rb4
-rw-r--r--spec/controllers/projects/tags_controller_spec.rb12
-rw-r--r--spec/features/projects/pages/user_adds_domain_spec.rb1
-rw-r--r--spec/finders/packages/composer/packages_finder_spec.rb15
-rw-r--r--spec/finders/packages/group_packages_finder_spec.rb24
-rw-r--r--spec/frontend/blob_edit/edit_blob_spec.js29
-rw-r--r--spec/frontend/editor/source_editor_security_policy_schema_ext_spec.js181
-rw-r--r--spec/frontend/issues/show/components/header_actions_spec.js18
-rw-r--r--spec/frontend/organizations/settings/general/components/advanced_settings_spec.js25
-rw-r--r--spec/frontend/organizations/settings/general/components/app_spec.js7
-rw-r--r--spec/frontend/organizations/settings/general/components/change_url_spec.js163
-rw-r--r--spec/frontend/organizations/shared/components/new_edit_form_spec.js28
-rw-r--r--spec/frontend/organizations/shared/components/organization_url_field_spec.js66
-rw-r--r--spec/frontend/vue_merge_request_widget/components/checks/draft_spec.js196
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/work_in_progress_spec.js3
-rw-r--r--spec/helpers/groups_helper_spec.rb63
-rw-r--r--spec/lib/bulk_imports/pipeline/runner_spec.rb2
-rw-r--r--spec/lib/gitlab/bitbucket_import/importers/issues_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/bitbucket_import/importers/issues_notes_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/bitbucket_import/importers/pull_requests_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/bitbucket_import/importers/pull_requests_notes_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/checks/branch_check_spec.rb34
-rw-r--r--spec/models/ability_spec.rb39
-rw-r--r--spec/models/bulk_import_spec.rb4
-rw-r--r--spec/models/bulk_imports/entity_spec.rb9
-rw-r--r--spec/models/integrations/jira_spec.rb2
-rw-r--r--spec/models/pages_domain_spec.rb23
-rw-r--r--spec/policies/ci/pipeline_schedule_policy_spec.rb338
-rw-r--r--spec/policies/issue_policy_spec.rb30
-rw-r--r--spec/services/ci/pipeline_schedules/update_service_spec.rb56
-rw-r--r--spec/services/projects/lfs_pointers/lfs_link_service_spec.rb18
-rw-r--r--spec/support/shared_examples/analytics/cycle_analytics/request_params_examples.rb4
-rw-r--r--spec/workers/bulk_imports/pipeline_worker_spec.rb2
-rw-r--r--spec/workers/bulk_imports/stuck_import_worker_spec.rb43
-rw-r--r--yarn.lock8
102 files changed, 2438 insertions, 697 deletions
diff --git a/.gitlab/ci/gitlab-com/danger-review.gitlab-ci.yml b/.gitlab/ci/gitlab-com/danger-review.gitlab-ci.yml
index 8625ca1f379..d5388f394f1 100644
--- a/.gitlab/ci/gitlab-com/danger-review.gitlab-ci.yml
+++ b/.gitlab/ci/gitlab-com/danger-review.gitlab-ci.yml
@@ -1,6 +1,6 @@
include:
- project: gitlab-org/quality/pipeline-common
- ref: 7.10.4
+ ref: 7.13.2
file:
- /ci/danger-review.yml
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c3f065fe489..b5d0aa9bee7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,28 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 16.6.1 (2023-11-30)
+
+### Fixed (3 changes)
+
+- [Revert "Merge branch 'sc1-release-goredis' into 'master'"](gitlab-org/security/gitlab@9589d80224cae8723bea3180418061363deeddd9)
+- [Truncate verification failure message to 255](gitlab-org/security/gitlab@d3c363a1e644525c386e670abca295181a9ae450) **GitLab Enterprise Edition**
+- [Prefer custom sort order with search in users API](gitlab-org/security/gitlab@3c9b46eb086ebfa595083452f82ddd19db586e5b)
+
+### Security (11 changes)
+
+- [Validate adding members with higher role](gitlab-org/security/gitlab@e55b3d8e5f3cf86fa5b124b0c85d3c70e94056b0) ([merge request](gitlab-org/security/gitlab!3713))
+- [Enforce ref protection on pipeline schedule updates](gitlab-org/security/gitlab@a4565e7ddc064035a622c0f645bdcf583f8d9945) ([merge request](gitlab-org/security/gitlab!3703))
+- [Update mermaid version for DOS security fixes](gitlab-org/security/gitlab@baec50f7af8077e77cf3124ac695ecb12d2d0028) ([merge request](gitlab-org/security/gitlab!3707))
+- [Prevent guest users from being able to add emojis in confidential issues](gitlab-org/security/gitlab@7700354a9e5bd11b8db8e6b116d6708c9ef15e72) ([merge request](gitlab-org/security/gitlab!3699))
+- [Do not run ssl cert validation if key has errors](gitlab-org/security/gitlab@a585a7ad29319b9cdaa6086287251ac34b0cd2be) ([merge request](gitlab-org/security/gitlab!3702))
+- [Ensure access is checked when loading releases associated with tags](gitlab-org/security/gitlab@68cb75d412db5e1fe97823f21cd848299cb1c969) ([merge request](gitlab-org/security/gitlab!3701))
+- [XSS and ReDoS in Markdown via Banzai pipeline of Jira](gitlab-org/security/gitlab@4ab2701284c928a392b5390977e4daed30b1b39f) ([merge request](gitlab-org/security/gitlab!3697))
+- [Prevent branch names starting with SHA-1 and SHA-256 values](gitlab-org/security/gitlab@cc65b6c8c94b1b647995fe5f2d6afd23cc621f12) ([merge request](gitlab-org/security/gitlab!3698))
+- [Filter out projects with disabled package registry in Composer finder](gitlab-org/security/gitlab@576f1ee9a3b612a579f987471e59dcd4820f5bd4) ([merge request](gitlab-org/security/gitlab!3684))
+- [Check max role for user for group access to protected ref](gitlab-org/security/gitlab@1e39ee42f24588675336da5b95a9863ee46b33c4) ([merge request](gitlab-org/security/gitlab!3700))
+- [Treat security policy bots as external](gitlab-org/security/gitlab@487e39c72883c71f5a4149191c9580017b0babd2) ([merge request](gitlab-org/security/gitlab!3678))
+
## 16.6.0 (2023-11-15)
### Added (117 changes)
@@ -548,6 +570,22 @@ entry.
- [Remove pubsub migration helper for actioncable](gitlab-org/gitlab@763ca1305db6f1c9cf6700b8497494a81926d742) ([merge request](gitlab-org/gitlab!133066))
- [Use partitioned table for CommitStatus](gitlab-org/gitlab@063826e042778995fae13928a2fb5de2c8855b45) ([merge request](gitlab-org/gitlab!134489))
+## 16.5.3 (2023-11-30)
+
+### Security (11 changes)
+
+- [Validate adding members with higher role](gitlab-org/security/gitlab@4159a01ca7dfca9856a0ce404fcba8459382b104) ([merge request](gitlab-org/security/gitlab!3714))
+- [Enforce ref protection on pipeline schedule updates](gitlab-org/security/gitlab@4bafe829109bedb1d31f1c28eccafa425083c297) ([merge request](gitlab-org/security/gitlab!3656))
+- [Update mermaid version for DOS security fixes](gitlab-org/security/gitlab@641557519046d680bf8916a60b66c3d6020b1b88) ([merge request](gitlab-org/security/gitlab!3673))
+- [Prevent guest users from being able to add emojis in confidential issues](gitlab-org/security/gitlab@f6fe0644a285e323b0469510a69c8d01d7fbe2a7) ([merge request](gitlab-org/security/gitlab!3690))
+- [Do not run ssl cert validation if key has errors](gitlab-org/security/gitlab@dcd5a3dcafc8ec943b78b43b8898201b5a9c4de5) ([merge request](gitlab-org/security/gitlab!3661))
+- [Ensure access is checked when loading releases associated with tags](gitlab-org/security/gitlab@1d1a454147e80ea27cee382743cfff9e9041d0fe) ([merge request](gitlab-org/security/gitlab!3695))
+- [XSS and ReDoS in Markdown via Banzai pipeline of Jira](gitlab-org/security/gitlab@13cae16669e25b1f7a889ca3fdc5d08c5a6d28a2) ([merge request](gitlab-org/security/gitlab!3691))
+- [Prevent branch names starting with SHA-1 and SHA-256 values](gitlab-org/security/gitlab@bd18a249dbae6dc362dc5ecad26c61eb69407d78) ([merge request](gitlab-org/security/gitlab!3687))
+- [Filter out projects with disabled package registry in Composer finder](gitlab-org/security/gitlab@1d7e1de18c0ce2bf380f44aa777566dd61919a25) ([merge request](gitlab-org/security/gitlab!3682))
+- [Check max role for user for group access to protected ref](gitlab-org/security/gitlab@d3eef816a353bb0a4fb611a91c1cf0af6d9006bf) ([merge request](gitlab-org/security/gitlab!3646))
+- [Treat security policy bots as external](gitlab-org/security/gitlab@f16c6f2b80bd70d04a304b0441da2642dd32abe5) ([merge request](gitlab-org/security/gitlab!3676))
+
## 16.5.2 (2023-11-14)
### Fixed (4 changes)
@@ -1243,6 +1281,25 @@ entry.
- [Alias read_namespace to access_namespace and move usages to new ability](gitlab-org/gitlab@61cdb4127143162a9bf9182f9c3c2d8421ee447f) by @Taucher2003 ([merge request](gitlab-org/gitlab!126625))
- [Remove `custom_roles_on_groups` feature flag](gitlab-org/gitlab@ddb4b4399b8bb82793410005c5778a002ae409b9) ([merge request](gitlab-org/gitlab!132187)) **GitLab Enterprise Edition**
+## 16.4.3 (2023-11-30)
+
+### Fixed (1 change)
+
+- [Fix assign security check permission checks](gitlab-org/security/gitlab@68b0fe3e41199a47e5851f3f00412ba18cc61a27) **GitLab Enterprise Edition**
+
+### Security (10 changes)
+
+- [Enforce ref protection on pipeline schedule updates](gitlab-org/security/gitlab@222b8d02d95e6c33ef26bfbb69718fa73daf31bc) ([merge request](gitlab-org/security/gitlab!3657))
+- [Update mermaid version for DOS security fixes](gitlab-org/security/gitlab@91f6263eb4697e9aebe059aee46ccfe1974d481c) ([merge request](gitlab-org/security/gitlab!3672))
+- [Prevent guest users from being able to add emojis in confidential issues](gitlab-org/security/gitlab@cc233c603bc595ef60f1b7ea2fcd69ab6113a374) ([merge request](gitlab-org/security/gitlab!3689))
+- [Do not run ssl cert validation if key has errors](gitlab-org/security/gitlab@ce234f97638d9182c22636301eccae87e7af854a) ([merge request](gitlab-org/security/gitlab!3662))
+- [Ensure access is checked when loading releases associated with tags](gitlab-org/security/gitlab@fead41322a5cf79513b5e3375fb2372ca936ef10) ([merge request](gitlab-org/security/gitlab!3696))
+- [XSS and ReDoS in Markdown via Banzai pipeline of Jira](gitlab-org/security/gitlab@7d9d64aa7123287c495b6be291a9b00dc60f179e) ([merge request](gitlab-org/security/gitlab!3692))
+- [Prevent branch names starting with SHA-1 and SHA-256 values](gitlab-org/security/gitlab@f51d428a6961bf77661cffffd50face4d02c6f43) ([merge request](gitlab-org/security/gitlab!3688))
+- [Filter out projects with disabled package registry in Composer finder](gitlab-org/security/gitlab@844ddc2028fd7389beee440034a1e83a42693ba2) ([merge request](gitlab-org/security/gitlab!3683))
+- [Check max role for user for group access to protected ref](gitlab-org/security/gitlab@1f6036ab1e227d013c0d42210a9c08ac7ff231c6) ([merge request](gitlab-org/security/gitlab!3643))
+- [Treat security policy bots as external](gitlab-org/security/gitlab@b0cf61131f21381978509ab2698b9da57522e726) ([merge request](gitlab-org/security/gitlab!3677))
+
## 16.4.2 (2023-10-30)
### Fixed (4 changes)
diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js
index b3bd23e49f8..be96c83aea2 100644
--- a/app/assets/javascripts/blob_edit/blob_bundle.js
+++ b/app/assets/javascripts/blob_edit/blob_bundle.js
@@ -40,6 +40,7 @@ export default () => {
const filePath = `${editBlobForm.data('blobFilename')}`;
const currentAction = $('.js-file-title').data('currentAction');
const projectId = editBlobForm.data('project-id');
+ const projectPath = editBlobForm.data('project-path');
const isMarkdown = editBlobForm.data('is-markdown');
const previewMarkdownPath = editBlobForm.data('previewMarkdownPath');
const commitButton = $('.js-commit-button');
@@ -54,6 +55,7 @@ export default () => {
filePath,
currentAction,
projectId,
+ projectPath,
isMarkdown,
previewMarkdownPath,
});
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index 007fbd29e82..78ccacd9f57 100644
--- a/app/assets/javascripts/blob_edit/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -24,6 +24,10 @@ export default class EditBlob {
this.fetchMarkdownExtension();
}
+ if (this.options.filePath === '.gitlab/security-policies/policy.yml') {
+ this.fetchSecurityPolicyExtension(this.options.projectPath);
+ }
+
this.initModePanesAndLinks();
this.initFilepathForm();
this.initSoftWrap();
@@ -54,6 +58,20 @@ export default class EditBlob {
addEditorMarkdownListeners(this.editor);
}
+ async fetchSecurityPolicyExtension(projectPath) {
+ try {
+ const { SecurityPolicySchemaExtension } = await import(
+ '~/editor/extensions/source_editor_security_policy_schema_ext'
+ );
+ this.editor.use([{ definition: SecurityPolicySchemaExtension }]);
+ this.editor.registerSecurityPolicySchema(projectPath);
+ } catch (e) {
+ createAlert({
+ message: `${BLOB_EDITOR_ERROR}: ${e}`,
+ });
+ }
+ }
+
configureMonacoEditor() {
const editorEl = document.getElementById('editor');
const fileContentEl = document.getElementById('file-content');
@@ -64,6 +82,7 @@ export default class EditBlob {
this.editor = rootEditor.createInstance({
el: editorEl,
blobContent: editorEl.innerText,
+ blobPath: this.options.filePath,
});
this.editor.use([
{ definition: ToolbarExtension },
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue b/app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue
index 21e21d54758..0064dc51d97 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue
@@ -105,9 +105,6 @@ export default {
branchesData() {
return this.availableBranches.map((branch) => ({
text: branch,
- extraAttrs: {
- 'data-qa-selector': 'branch_menu_item_button',
- },
value: branch,
}));
},
@@ -211,7 +208,6 @@ export default {
<gl-collapsible-listbox
v-model="currentBranch"
v-gl-tooltip.hover
- data-qa-selector="branch_selector_button"
searchable
:items="branchesData"
:title="$options.i18n.dropdownHeader"
diff --git a/app/assets/javascripts/ci/pipeline_mini_graph/legacy_job_item.vue b/app/assets/javascripts/ci/pipeline_mini_graph/legacy_job_item.vue
index c7fe9724485..4238f0e3872 100644
--- a/app/assets/javascripts/ci/pipeline_mini_graph/legacy_job_item.vue
+++ b/app/assets/javascripts/ci/pipeline_mini_graph/legacy_job_item.vue
@@ -161,7 +161,6 @@ export default {
:tooltip-text="jobActionTooltipText"
:link="status.action.path"
:action-icon="status.action.icon"
- data-qa-selector="action_button"
/>
</div>
</template>
diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue
index 5444e66cbdf..44a377144a5 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue
@@ -391,7 +391,6 @@ export default {
:placeholder="s__('CiVariables|Input variable key')"
:class="$options.formElementClasses"
data-testid="pipeline-form-ci-variable-key"
- data-qa-selector="ci_variable_key_field"
@change="addEmptyVariable(variable)"
/>
@@ -411,7 +410,6 @@ export default {
class="gl-mb-3 gl-h-7!"
:no-resize="false"
data-testid="pipeline-form-ci-variable-value"
- data-qa-selector="ci_variable_value_field"
@change="resetVariable(index)"
/>
diff --git a/app/assets/javascripts/ci/reports/components/report_section.vue b/app/assets/javascripts/ci/reports/components/report_section.vue
index fd6c6cca6b7..4a6a5e6e221 100644
--- a/app/assets/javascripts/ci/reports/components/report_section.vue
+++ b/app/assets/javascripts/ci/reports/components/report_section.vue
@@ -207,7 +207,6 @@ export default {
>
<gl-button
data-testid="report-section-expand-button"
- data-qa-selector="expand_report_button"
category="tertiary"
size="small"
:icon="isExpanded ? 'chevron-lg-up' : 'chevron-lg-down'"
diff --git a/app/assets/javascripts/editor/extensions/source_editor_security_policy_schema_ext.js b/app/assets/javascripts/editor/extensions/source_editor_security_policy_schema_ext.js
new file mode 100644
index 00000000000..e3b9d273efd
--- /dev/null
+++ b/app/assets/javascripts/editor/extensions/source_editor_security_policy_schema_ext.js
@@ -0,0 +1,84 @@
+import { registerSchema } from '~/ide/utils';
+import axios from '~/lib/utils/axios_utils';
+import { getBaseURL, joinPaths } from '~/lib/utils/url_utility';
+
+export const getSecurityPolicyListUrl = ({ namespacePath, namespaceType = 'group' }) => {
+ const isGroup = namespaceType === 'group';
+ return joinPaths(
+ getBaseURL(),
+ isGroup ? 'groups' : '',
+ namespacePath,
+ '-',
+ 'security',
+ 'policies',
+ );
+};
+
+export const getSecurityPolicySchemaUrl = ({ namespacePath, namespaceType }) => {
+ const policyListUrl = getSecurityPolicyListUrl({ namespacePath, namespaceType });
+ return joinPaths(policyListUrl, 'schema');
+};
+
+export const getSinglePolicySchema = async ({ namespacePath, namespaceType, policyType }) => {
+ try {
+ const { data: schemaForMultiplePolicies } = await axios.get(
+ getSecurityPolicySchemaUrl({ namespacePath, namespaceType }),
+ );
+ return {
+ $id: schemaForMultiplePolicies.$id,
+ title: schemaForMultiplePolicies.title,
+ description: schemaForMultiplePolicies.description,
+ type: schemaForMultiplePolicies.type,
+ properties: {
+ type: {
+ type: 'string',
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ description: 'Specifies the type of policy to be enforced.',
+ enum: policyType,
+ },
+ ...schemaForMultiplePolicies.properties[policyType].items.properties,
+ },
+ };
+ } catch {
+ return {};
+ }
+};
+
+export class SecurityPolicySchemaExtension {
+ static get extensionName() {
+ return 'SecurityPolicySchema';
+ }
+ // eslint-disable-next-line class-methods-use-this
+ provides() {
+ return {
+ registerSecurityPolicyEditorSchema: async (instance, options) => {
+ const { namespacePath, namespaceType, policyType } = options;
+ const singlePolicySchema = await getSinglePolicySchema({
+ namespacePath,
+ namespaceType,
+ policyType,
+ });
+ const modelFileName = instance.getModel().uri.path.split('/').pop();
+
+ registerSchema({
+ uri: getSecurityPolicySchemaUrl({ namespacePath, namespaceType }),
+ schema: singlePolicySchema,
+ fileMatch: [modelFileName],
+ });
+ },
+
+ registerSecurityPolicySchema: (instance, projectPath) => {
+ const uri = getSecurityPolicySchemaUrl({
+ namespacePath: projectPath,
+ namespaceType: 'project',
+ });
+ const modelFileName = instance.getModel().uri.path.split('/').pop();
+
+ registerSchema({
+ uri,
+ fileMatch: [modelFileName],
+ });
+ },
+ };
+ }
+}
diff --git a/app/assets/javascripts/issues/show/components/header_actions.vue b/app/assets/javascripts/issues/show/components/header_actions.vue
index 32df19dfe44..c362175a99a 100644
--- a/app/assets/javascripts/issues/show/components/header_actions.vue
+++ b/app/assets/javascripts/issues/show/components/header_actions.vue
@@ -352,7 +352,7 @@ export default {
<template v-if="isMrSidebarMoved">
<gl-disclosure-dropdown-item
:data-clipboard-text="issuableReference"
- button-class="js-copy-reference"
+ class="js-copy-reference"
data-testid="copy-reference"
@action="copyReference"
><template #list-item>{{
@@ -453,7 +453,7 @@ export default {
<template v-if="isMrSidebarMoved">
<gl-disclosure-dropdown-item
:data-clipboard-text="issuableReference"
- button-class="js-copy-reference"
+ class="js-copy-reference"
data-testid="copy-reference"
@action="copyReference"
><template #list-item>{{
diff --git a/app/assets/javascripts/organizations/mock_data.js b/app/assets/javascripts/organizations/mock_data.js
index 7080170958b..e9bc5f0686b 100644
--- a/app/assets/javascripts/organizations/mock_data.js
+++ b/app/assets/javascripts/organizations/mock_data.js
@@ -306,6 +306,7 @@ export const updateOrganizationResponse = {
organization: {
id: 'gid://gitlab/Organizations/1',
name: 'Default updated',
+ webUrl: 'http://127.0.0.1:3000/-/organizations/default',
},
errors: [],
};
diff --git a/app/assets/javascripts/organizations/settings/general/components/advanced_settings.vue b/app/assets/javascripts/organizations/settings/general/components/advanced_settings.vue
new file mode 100644
index 00000000000..879e7b230a1
--- /dev/null
+++ b/app/assets/javascripts/organizations/settings/general/components/advanced_settings.vue
@@ -0,0 +1,26 @@
+<script>
+import { s__, __ } from '~/locale';
+import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
+import ChangeUrl from './change_url.vue';
+
+export default {
+ name: 'AdvancedSettings',
+ components: { SettingsBlock, ChangeUrl },
+ i18n: {
+ settingsBlock: {
+ title: __('Advanced'),
+ description: s__('Organization|Perform advanced options such as deleting the organization.'),
+ },
+ },
+};
+</script>
+
+<template>
+ <settings-block slide-animated>
+ <template #title>{{ $options.i18n.settingsBlock.title }}</template>
+ <template #description>{{ $options.i18n.settingsBlock.description }}</template>
+ <template #default>
+ <change-url />
+ </template>
+ </settings-block>
+</template>
diff --git a/app/assets/javascripts/organizations/settings/general/components/app.vue b/app/assets/javascripts/organizations/settings/general/components/app.vue
index 134fcc17b54..ba8ab5a09fd 100644
--- a/app/assets/javascripts/organizations/settings/general/components/app.vue
+++ b/app/assets/javascripts/organizations/settings/general/components/app.vue
@@ -1,14 +1,16 @@
<script>
import OrganizationSettings from './organization_settings.vue';
+import AdvancedSettings from './advanced_settings.vue';
export default {
name: 'OrganizationSettingsGeneralApp',
- components: { OrganizationSettings },
+ components: { OrganizationSettings, AdvancedSettings },
};
</script>
<template>
<div>
<organization-settings />
+ <advanced-settings />
</div>
</template>
diff --git a/app/assets/javascripts/organizations/settings/general/components/change_url.vue b/app/assets/javascripts/organizations/settings/general/components/change_url.vue
new file mode 100644
index 00000000000..6b291dd95cb
--- /dev/null
+++ b/app/assets/javascripts/organizations/settings/general/components/change_url.vue
@@ -0,0 +1,128 @@
+<script>
+import { GlFormFields, GlButton, GlForm, GlCard } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { visitUrlWithAlerts, joinPaths } from '~/lib/utils/url_utility';
+import { createAlert } from '~/alert';
+import OrganizationUrlField from '~/organizations/shared/components/organization_url_field.vue';
+import { FORM_FIELD_PATH, FORM_FIELD_PATH_VALIDATORS } from '~/organizations/shared/constants';
+import updateOrganizationMutation from '../graphql/mutations/update_organization.mutation.graphql';
+
+export default {
+ name: 'OrganizationSettings',
+ components: { OrganizationUrlField, GlFormFields, GlButton, GlForm, GlCard },
+ inject: ['organization'],
+ i18n: {
+ cardHeaderTitle: s__('Organization|Change organization URL'),
+ cardHeaderDescription: s__(
+ "Organization|Changing an organization's URL can have unintended side effects.",
+ ),
+ submitButtonText: s__('Organization|Change organization URL'),
+ errorMessage: s__(
+ 'Organization|An error occurred changing your organization URL. Please try again.',
+ ),
+ successAlertMessage: s__('Organization|Organization URL successfully changed.'),
+ },
+ formId: 'change-organization-url-form',
+ fields: {
+ [FORM_FIELD_PATH]: {
+ label: s__('Organization|Organization URL'),
+ validators: FORM_FIELD_PATH_VALIDATORS,
+ groupAttrs: {
+ class: 'gl-w-full',
+ labelSrOnly: true,
+ },
+ },
+ },
+ data() {
+ return {
+ formValues: {
+ path: this.organization.path,
+ },
+ loading: false,
+ };
+ },
+ computed: {
+ isSubmitButtonDisabled() {
+ return this.formValues.path === this.organization.path;
+ },
+ },
+ methods: {
+ async onSubmit(formValues) {
+ this.loading = true;
+ try {
+ const {
+ data: {
+ updateOrganization: { errors, organization },
+ },
+ } = await this.$apollo.mutate({
+ mutation: updateOrganizationMutation,
+ variables: {
+ id: this.organization.id,
+ path: formValues.path,
+ },
+ });
+
+ if (errors.length) {
+ // TODO: handle errors when using real API after https://gitlab.com/gitlab-org/gitlab/-/issues/419608 is complete.
+ return;
+ }
+
+ visitUrlWithAlerts(joinPaths(organization.webUrl, '/settings/general'), [
+ {
+ id: 'organization-url-successfully-changed',
+ message: this.$options.i18n.successAlertMessage,
+ variant: 'info',
+ },
+ ]);
+ } catch (error) {
+ createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true });
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-card
+ class="gl-new-card"
+ header-class="gl-new-card-header gl-flex-direction-column"
+ body-class="gl-new-card-body gl-px-5 gl-py-4"
+ >
+ <template #header>
+ <div class="gl-new-card-title-wrapper">
+ <h4 class="gl-new-card-title">{{ $options.i18n.cardHeaderTitle }}</h4>
+ </div>
+ <p class="gl-new-card-description">{{ $options.i18n.cardHeaderDescription }}</p>
+ </template>
+ <gl-form :id="$options.formId">
+ <gl-form-fields
+ v-model="formValues"
+ :form-id="$options.formId"
+ :fields="$options.fields"
+ @submit="onSubmit"
+ >
+ <template #input(path)="{ id, value, validation, input, blur }">
+ <organization-url-field
+ :id="id"
+ :value="value"
+ :validation="validation"
+ @input="input"
+ @blur="blur"
+ />
+ </template>
+ </gl-form-fields>
+ <div class="gl-display-flex gl-gap-3">
+ <gl-button
+ type="submit"
+ variant="danger"
+ class="js-no-auto-disable"
+ :loading="loading"
+ :disabled="isSubmitButtonDisabled"
+ >{{ $options.i18n.submitButtonText }}</gl-button
+ >
+ </div>
+ </gl-form>
+ </gl-card>
+</template>
diff --git a/app/assets/javascripts/organizations/settings/general/graphql/mutations/update_organization.mutation.graphql b/app/assets/javascripts/organizations/settings/general/graphql/mutations/update_organization.mutation.graphql
index b571a523260..f24875752f5 100644
--- a/app/assets/javascripts/organizations/settings/general/graphql/mutations/update_organization.mutation.graphql
+++ b/app/assets/javascripts/organizations/settings/general/graphql/mutations/update_organization.mutation.graphql
@@ -3,6 +3,7 @@ mutation updateOrganization($input: LocalUpdateOrganizationInput!) {
organization {
id
name
+ webUrl
}
errors
}
diff --git a/app/assets/javascripts/organizations/shared/components/new_edit_form.vue b/app/assets/javascripts/organizations/shared/components/new_edit_form.vue
index 8aaa680036f..c5bb16b944a 100644
--- a/app/assets/javascripts/organizations/shared/components/new_edit_form.vue
+++ b/app/assets/javascripts/organizations/shared/components/new_edit_form.vue
@@ -1,18 +1,15 @@
<script>
-import {
- GlForm,
- GlFormFields,
- GlButton,
- GlFormInputGroup,
- GlFormInput,
- GlInputGroupText,
- GlTruncate,
-} from '@gitlab/ui';
+import { GlForm, GlFormFields, GlButton } from '@gitlab/ui';
import { formValidators } from '@gitlab/ui/dist/utils';
import { s__, __ } from '~/locale';
import { slugify } from '~/lib/utils/text_utility';
-import { joinPaths } from '~/lib/utils/url_utility';
-import { FORM_FIELD_NAME, FORM_FIELD_ID, FORM_FIELD_PATH } from '../constants';
+import {
+ FORM_FIELD_NAME,
+ FORM_FIELD_ID,
+ FORM_FIELD_PATH,
+ FORM_FIELD_PATH_VALIDATORS,
+} from '../constants';
+import OrganizationUrlField from './organization_url_field.vue';
export default {
name: 'NewEditForm',
@@ -20,17 +17,13 @@ export default {
GlForm,
GlFormFields,
GlButton,
- GlFormInputGroup,
- GlFormInput,
- GlInputGroupText,
- GlTruncate,
+ OrganizationUrlField,
},
i18n: {
cancel: __('Cancel'),
- pathPlaceholder: s__('Organization|my-organization'),
},
formId: 'new-organization-form',
- inject: ['organizationsPath', 'rootUrl'],
+ inject: ['organizationsPath'],
props: {
loading: {
type: Boolean,
@@ -71,9 +64,6 @@ export default {
};
},
computed: {
- baseUrl() {
- return joinPaths(this.rootUrl, this.organizationsPath, '/');
- },
fields() {
const fields = {
[FORM_FIELD_NAME]: {
@@ -103,13 +93,7 @@ export default {
},
[FORM_FIELD_PATH]: {
label: s__('Organization|Organization URL'),
- validators: [
- formValidators.required(s__('Organization|Organization URL is required.')),
- formValidators.factory(
- s__('Organization|Organization URL must be a minimum of two characters.'),
- (val) => val.length >= 2,
- ),
- ],
+ validators: FORM_FIELD_PATH_VALIDATORS,
groupAttrs: {
class: 'gl-w-full',
},
@@ -156,22 +140,13 @@ export default {
@submit="$emit('submit', formValues)"
>
<template #input(path)="{ id, value, validation, input, blur }">
- <gl-form-input-group>
- <template #prepend>
- <gl-input-group-text class="organization-root-path">
- <gl-truncate :text="baseUrl" position="middle" />
- </gl-input-group-text>
- </template>
- <gl-form-input
- v-bind="validation"
- :id="id"
- :value="value"
- :placeholder="$options.i18n.pathPlaceholder"
- class="gl-h-auto! gl-md-form-input-lg"
- @input="onPathInput($event, input)"
- @blur="blur"
- />
- </gl-form-input-group>
+ <organization-url-field
+ :id="id"
+ :value="value"
+ :validation="validation"
+ @input="onPathInput($event, input)"
+ @blur="blur"
+ />
</template>
</gl-form-fields>
<div class="gl-display-flex gl-gap-3">
diff --git a/app/assets/javascripts/organizations/shared/components/organization_url_field.vue b/app/assets/javascripts/organizations/shared/components/organization_url_field.vue
new file mode 100644
index 00000000000..d36f62477e6
--- /dev/null
+++ b/app/assets/javascripts/organizations/shared/components/organization_url_field.vue
@@ -0,0 +1,58 @@
+<script>
+import { GlFormInputGroup, GlFormInput, GlInputGroupText, GlTruncate } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { joinPaths } from '~/lib/utils/url_utility';
+
+export default {
+ name: 'OrganizationUrlField',
+ components: {
+ GlFormInputGroup,
+ GlFormInput,
+ GlInputGroupText,
+ GlTruncate,
+ },
+ i18n: {
+ pathPlaceholder: s__('Organization|my-organization'),
+ },
+ formId: 'new-organization-form',
+ inject: ['organizationsPath', 'rootUrl'],
+ props: {
+ id: {
+ type: String,
+ required: true,
+ },
+ value: {
+ type: String,
+ required: true,
+ },
+ validation: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ baseUrl() {
+ return joinPaths(this.rootUrl, this.organizationsPath, '/');
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-form-input-group>
+ <template #prepend>
+ <gl-input-group-text class="organization-root-path">
+ <gl-truncate :text="baseUrl" position="middle" />
+ </gl-input-group-text>
+ </template>
+ <gl-form-input
+ v-bind="validation"
+ :id="id"
+ :value="value"
+ :placeholder="$options.i18n.pathPlaceholder"
+ class="gl-h-auto! gl-md-form-input-lg"
+ @input="$emit('input', $event)"
+ @blur="$emit('blur', $event)"
+ />
+ </gl-form-input-group>
+</template>
diff --git a/app/assets/javascripts/organizations/shared/constants.js b/app/assets/javascripts/organizations/shared/constants.js
index 010613bc9fd..7287d84f99f 100644
--- a/app/assets/javascripts/organizations/shared/constants.js
+++ b/app/assets/javascripts/organizations/shared/constants.js
@@ -1,3 +1,14 @@
+import { formValidators } from '@gitlab/ui/dist/utils';
+import { s__ } from '~/locale';
+
export const FORM_FIELD_NAME = 'name';
export const FORM_FIELD_ID = 'id';
export const FORM_FIELD_PATH = 'path';
+
+export const FORM_FIELD_PATH_VALIDATORS = [
+ formValidators.required(s__('Organization|Organization URL is required.')),
+ formValidators.factory(
+ s__('Organization|Organization URL is too short (minimum is 2 characters).'),
+ (val) => val.length >= 2,
+ ),
+];
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js b/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js
index a8d2736531a..24bc7017e06 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js
@@ -1,6 +1,7 @@
export const COMPONENTS = {
conflict: () => import('./conflicts.vue'),
discussions_not_resolved: () => import('./unresolved_discussions.vue'),
+ draft_status: () => import('./draft.vue'),
need_rebase: () => import('./rebase.vue'),
default: () => import('./message.vue'),
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/draft.stories.js b/app/assets/javascripts/vue_merge_request_widget/components/checks/draft.stories.js
new file mode 100644
index 00000000000..537c975652f
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/draft.stories.js
@@ -0,0 +1,74 @@
+import createMockApollo from 'helpers/mock_apollo_helper';
+import draftStateQuery from '../../queries/states/draft.query.graphql';
+import removeDraftMutation from '../../queries/toggle_draft.mutation.graphql';
+import Draft from './draft.vue';
+
+const defaultRender = ({ apolloProvider, check, mr }) => ({
+ components: { Draft },
+ apolloProvider,
+ data() {
+ return { mr, check };
+ },
+ template: '<draft :check="check" :mr="mr" />',
+});
+
+const Template = ({ userPermissionUpdateMergeRequest }) => {
+ const requestHandlers = [
+ [
+ draftStateQuery,
+ () =>
+ Promise.resolve({
+ data: {
+ project: {
+ id: '1',
+ mergeRequest: {
+ draft: false,
+ id: '2',
+ title: 'MR title',
+ mergeableDiscussionsState: true,
+ userPermissions: {
+ updateMergeRequest: userPermissionUpdateMergeRequest,
+ },
+ },
+ },
+ },
+ }),
+ ],
+ [
+ removeDraftMutation,
+ () =>
+ Promise.resolve({
+ data: {
+ mergeRequestSetDraft: {
+ mergeRequest: {
+ draft: false,
+ id: '2',
+ title: 'MR title',
+ mergeableDiscussionsState: true,
+ },
+ errors: [],
+ },
+ },
+ }),
+ ],
+ ];
+ const apolloProvider = createMockApollo(requestHandlers);
+
+ return defaultRender({
+ apolloProvider,
+ check: {
+ identifier: 'draft_status',
+ status: 'FAILED',
+ },
+ });
+};
+
+export const Default = Template.bind({});
+Default.args = {
+ userPermissionUpdateMergeRequest: true,
+};
+
+export default {
+ title: 'vue_merge_request_widget/merge_checks/draft',
+ component: Draft,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/draft.vue b/app/assets/javascripts/vue_merge_request_widget/components/checks/draft.vue
new file mode 100644
index 00000000000..dbe0d2ac243
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/draft.vue
@@ -0,0 +1,169 @@
+<script>
+import { produce } from 'immer';
+
+import { createAlert } from '~/alert';
+import MergeRequest from '~/merge_request';
+
+import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
+
+import draftStateQuery from '../../queries/states/draft.query.graphql';
+import removeDraftMutation from '../../queries/toggle_draft.mutation.graphql';
+
+import ActionButtons from '../action_buttons.vue';
+import MergeChecksMessage from './message.vue';
+
+import { DRAFT_CHECK_READY, DRAFT_CHECK_ERROR } from './i18n';
+
+export default {
+ name: 'MergeChecksDraft',
+ components: {
+ MergeChecksMessage,
+ ActionButtons,
+ },
+ mixins: [mergeRequestQueryVariablesMixin],
+ apollo: {
+ state: {
+ query: draftStateQuery,
+ variables() {
+ return this.mergeRequestQueryVariables;
+ },
+ update: (data) => data?.project?.mergeRequest,
+ },
+ },
+ props: {
+ mr: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ check: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ state: {},
+ isMutating: false,
+ };
+ },
+ computed: {
+ networking() {
+ return this.isLoading || this.isMutating;
+ },
+ isLoading() {
+ return this.$apollo.queries.state.loading;
+ },
+ userCanUpdateMergeRequest() {
+ return this.state.userPermissions.updateMergeRequest;
+ },
+ showTertiaryButton() {
+ return !this.networking && this.userCanUpdateMergeRequest;
+ },
+ tertiaryActionsButtons() {
+ return [
+ {
+ text: DRAFT_CHECK_READY,
+ category: 'default',
+ testId: 'mark-as-ready-button',
+ onClick: () => this.removeDraft(),
+ },
+ ];
+ },
+ },
+ methods: {
+ removeDraft() {
+ const { mergeRequestQueryVariables } = this;
+
+ this.isMutating = true;
+
+ this.$apollo
+ .mutate({
+ mutation: removeDraftMutation,
+ variables: {
+ ...mergeRequestQueryVariables,
+ draft: false,
+ },
+ update(
+ store,
+ {
+ data: {
+ mergeRequestSetDraft: {
+ errors,
+ mergeRequest: { mergeableDiscussionsState, draft, title },
+ },
+ },
+ },
+ ) {
+ if (errors?.length) {
+ createAlert({
+ message: DRAFT_CHECK_ERROR,
+ });
+
+ return;
+ }
+
+ const sourceData = store.readQuery({
+ query: draftStateQuery,
+ variables: mergeRequestQueryVariables,
+ });
+
+ const data = produce(sourceData, (draftState) => {
+ draftState.project.mergeRequest.mergeableDiscussionsState = mergeableDiscussionsState;
+ draftState.project.mergeRequest.draft = draft;
+ draftState.project.mergeRequest.title = title;
+ });
+
+ store.writeQuery({
+ query: draftStateQuery,
+ data,
+ variables: mergeRequestQueryVariables,
+ });
+ },
+ optimisticResponse: {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ __typename: 'Mutation',
+ mergeRequestSetDraft: {
+ __typename: 'MergeRequestSetWipPayload',
+ errors: [],
+ mergeRequest: {
+ __typename: 'MergeRequest',
+ id: this.mr.issuableId,
+ mergeableDiscussionsState: true,
+ title: this.mr.title,
+ draft: false,
+ },
+ },
+ },
+ })
+ .then(
+ ({
+ data: {
+ mergeRequestSetDraft: {
+ mergeRequest: { title },
+ },
+ },
+ }) => {
+ MergeRequest.toggleDraftStatus(title, true);
+ },
+ )
+ .catch(() =>
+ createAlert({
+ message: DRAFT_CHECK_ERROR,
+ }),
+ )
+ .finally(() => {
+ this.isMutating = false;
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <merge-checks-message :check="check">
+ <template #failed>
+ <action-buttons v-if="showTertiaryButton" :tertiary-buttons="tertiaryActionsButtons" />
+ </template>
+ </merge-checks-message>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/i18n.js b/app/assets/javascripts/vue_merge_request_widget/components/checks/i18n.js
new file mode 100644
index 00000000000..de504af5fcc
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/i18n.js
@@ -0,0 +1,4 @@
+import { __, s__ } from '~/locale';
+
+export const DRAFT_CHECK_ERROR = __('Something went wrong. Please try again.');
+export const DRAFT_CHECK_READY = s__('mrWidgetDraftCheck|Mark as ready');
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/message.vue b/app/assets/javascripts/vue_merge_request_widget/components/checks/message.vue
index e91c76e7ff0..7f21445559a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/checks/message.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/message.vue
@@ -8,7 +8,7 @@ const ICON_NAMES = {
success: 'success',
};
-const FAILURE_REASONS = {
+export const FAILURE_REASONS = {
broken_status: __('Cannot merge the source into the target branch, due to a conflict.'),
ci_must_pass: __('Pipeline must succeed.'),
conflict: __('Merge conflicts must be resolved.'),
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.stories.js b/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.stories.js
index 77dc5b1d0da..a1171fe5d25 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.stories.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.stories.js
@@ -41,6 +41,10 @@ const Template = ({ canMerge, failed, pushToSourceBranch }) => {
identifier: 'CONFLICT',
status: failed ? 'FAILED' : 'SUCCESS',
},
+ {
+ identifier: 'DRAFT_STATUS',
+ status: failed ? 'FAILED' : 'SUCCESS',
+ },
],
},
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue
index 3be09eef5ad..d85ba5374d3 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue
@@ -386,7 +386,6 @@ export default {
category="tertiary"
data-testid="toggle-button"
size="small"
- data-qa-selector="expand_report_button"
@click="toggleCollapsed"
/>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/draft.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/draft.query.graphql
index 54f2233439f..c1190a07ef8 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/states/draft.query.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/draft.query.graphql
@@ -2,7 +2,10 @@ query mrUserPermission($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
id
mergeRequest(iid: $iid) {
+ draft
id
+ mergeableDiscussionsState
+ title
userPermissions {
updateMergeRequest
}
diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb
index 3c1735c728c..d3e38774aaa 100644
--- a/app/controllers/projects/tags_controller.rb
+++ b/app/controllers/projects/tags_controller.rb
@@ -29,7 +29,7 @@ class Projects::TagsController < Projects::ApplicationController
tag_names = @tags.map(&:name)
@tags_pipelines = @project.ci_pipelines.latest_successful_for_refs(tag_names)
- @releases = project.releases.where(tag: tag_names)
+ @releases = ReleasesFinder.new(project, current_user, tag: tag_names).execute
@tag_pipeline_statuses = Ci::CommitStatusesFinder.new(@project, @repository, current_user, @tags).execute
rescue Gitlab::Git::CommandError => e
diff --git a/app/finders/packages/composer/packages_finder.rb b/app/finders/packages/composer/packages_finder.rb
index b5a1b19216f..1581c48dd74 100644
--- a/app/finders/packages/composer/packages_finder.rb
+++ b/app/finders/packages/composer/packages_finder.rb
@@ -2,14 +2,12 @@
module Packages
module Composer
class PackagesFinder < Packages::GroupPackagesFinder
- def initialize(current_user, group, params = {})
- @current_user = current_user
- @group = group
- @params = params
+ def initialize(current_user, group, params = { package_type: :composer, with_package_registry_enabled: true })
+ super(current_user, group, params)
end
def execute
- packages_for_group_projects(installable_only: true).composer.preload_composer
+ packages_for_group_projects(installable_only: true).preload_composer
end
end
end
diff --git a/app/finders/packages/group_packages_finder.rb b/app/finders/packages/group_packages_finder.rb
index 3a068252d5c..3b211882fa0 100644
--- a/app/finders/packages/group_packages_finder.rb
+++ b/app/finders/packages/group_packages_finder.rb
@@ -40,14 +40,17 @@ module Packages
# access to packages is ruled by:
# - project is public or the current user has access to it with at least the reporter level
# - the repository feature is available to the current_user
- if current_user.is_a?(DeployToken)
- current_user.accessible_projects
- else
- ::Project
- .in_namespace(groups)
- .public_or_visible_to_user(current_user, Gitlab::Access::REPORTER)
- .with_feature_available_for_user(:repository, current_user)
- end
+ projects = if current_user.is_a?(DeployToken)
+ current_user.accessible_projects
+ else
+ ::Project
+ .in_namespace(groups)
+ .public_or_visible_to_user(current_user, Gitlab::Access::REPORTER)
+ .with_feature_available_for_user(:repository, current_user)
+ end
+
+ projects = projects.with_package_registry_enabled if params[:with_package_registry_enabled]
+ projects
end
def groups
diff --git a/app/graphql/types/permission_types/base_permission_type.rb b/app/graphql/types/permission_types/base_permission_type.rb
index 3c0e68bdaf2..3a8519e8196 100644
--- a/app/graphql/types/permission_types/base_permission_type.rb
+++ b/app/graphql/types/permission_types/base_permission_type.rb
@@ -30,7 +30,7 @@ module Types
def self.define_field_resolver_method(ability)
unless respond_to?(ability)
define_method ability.to_sym do |*args|
- Ability.allowed?(context[:current_user], ability, object, args.to_h)
+ Ability.allowed?(context[:current_user], ability, object, **args.to_h)
end
end
end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 8c199aefd81..f8e43674033 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -151,6 +151,7 @@ module BlobHelper
'assets-prefix' => Gitlab::Application.config.assets.prefix,
'blob-filename' => @blob && @blob.path,
'project-id' => project.id,
+ 'project-path': project.full_path,
'is-markdown' => @blob && @blob.path && Gitlab::MarkupHelper.gitlab_markdown?(@blob.path),
'preview-markdown-path' => preview_markdown_path(project)
}
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 2477a00757b..21d0e7856f8 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -207,6 +207,17 @@ module GroupsHelper
new_group_custom_emoji_path(group)
end
+ def access_level_roles_user_can_assign(group)
+ return {} unless current_user
+ return group.access_level_roles if current_user.can_admin_all_resources?
+
+ max_access_level = group.highest_group_member(current_user)&.access_level
+
+ return {} unless max_access_level
+
+ GroupMember.access_level_roles.select { |_k, v| v <= max_access_level }
+ end
+
private
def group_title_link(group, hidable: false, show_avatar: false, for_dropdown: false)
diff --git a/app/models/ability.rb b/app/models/ability.rb
index f1db4be8eb4..6ea48f2d668 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -70,13 +70,13 @@ class Ability
end
end
- def allowed?(user, ability, subject = :global, opts = {})
+ def allowed?(user, ability, subject = :global, **opts)
if subject.is_a?(Hash)
opts = subject
subject = :global
end
- policy = policy_for(user, subject)
+ policy = policy_for(user, subject, **opts.slice(:cache))
# https://gitlab.com/gitlab-org/gitlab/-/issues/421150#note_1638311666
if ability == :read_namespace && Feature.enabled?(:log_read_namespace_usages, Feature.current_request)
@@ -109,8 +109,14 @@ class Ability
# See Support::AbilityCheck and Support::PermissionsCheck.
end
- def policy_for(user, subject = :global)
- DeclarativePolicy.policy_for(user, subject, cache: ::Gitlab::SafeRequestStore.storage)
+ # We cache in the request store by default. This can lead to unexpected
+ # results if abilities are re-checked after objects are modified and the
+ # check depends on the modified attributes. In such cases, you should pass
+ # `cache: false` for the second check to ensure all rules get re-evaluated.
+ def policy_for(user, subject = :global, cache: true)
+ policy_cache = cache ? ::Gitlab::SafeRequestStore.storage : {}
+
+ DeclarativePolicy.policy_for(user, subject, cache: policy_cache)
end
# This method is something of a band-aid over the problem. The problem is
diff --git a/app/models/bulk_import.rb b/app/models/bulk_import.rb
index 80e0412ad19..da0d919c947 100644
--- a/app/models/bulk_import.rb
+++ b/app/models/bulk_import.rb
@@ -16,7 +16,7 @@ class BulkImport < ApplicationRecord
enum source_type: { gitlab: 0 }
- scope :stale, -> { where('created_at < ?', 24.hours.ago).where(status: [0, 1]) }
+ scope :stale, -> { where('updated_at < ?', 24.hours.ago).where(status: [0, 1]) }
scope :order_by_created_at, ->(direction) { order(created_at: direction) }
state_machine :status, initial: :created do
diff --git a/app/models/bulk_imports/entity.rb b/app/models/bulk_imports/entity.rb
index 7521cb7c27c..8d5f1231bea 100644
--- a/app/models/bulk_imports/entity.rb
+++ b/app/models/bulk_imports/entity.rb
@@ -54,7 +54,7 @@ class BulkImports::Entity < ApplicationRecord
enum source_type: { group_entity: 0, project_entity: 1 }
scope :by_user_id, ->(user_id) { joins(:bulk_import).where(bulk_imports: { user_id: user_id }) }
- scope :stale, -> { where('created_at < ?', 24.hours.ago).where(status: [0, 1]) }
+ scope :stale, -> { where('updated_at < ?', 24.hours.ago).where(status: [0, 1]) }
scope :by_bulk_import_id, ->(bulk_import_id) { where(bulk_import_id: bulk_import_id) }
scope :order_by_created_at, ->(direction) { order(created_at: direction) }
diff --git a/app/models/concerns/protected_branch_access.rb b/app/models/concerns/protected_branch_access.rb
index 8156090fd9c..6a7fdce62fb 100644
--- a/app/models/concerns/protected_branch_access.rb
+++ b/app/models/concerns/protected_branch_access.rb
@@ -10,3 +10,5 @@ module ProtectedBranchAccess
delegate :project, to: :protected_branch
end
end
+
+ProtectedBranchAccess.prepend_mod_with('ProtectedBranchAccess')
diff --git a/app/models/integrations/jira.rb b/app/models/integrations/jira.rb
index 22367ee336d..bf49dbca294 100644
--- a/app/models/integrations/jira.rb
+++ b/app/models/integrations/jira.rb
@@ -401,9 +401,9 @@ module Integrations
private
def jira_issue_match_regex
- return /\b#{jira_issue_prefix}(?<issue>#{Gitlab::Regex.jira_issue_key_regex})/ if jira_issue_regex.blank?
+ jira_regex = jira_issue_regex.presence || Gitlab::Regex.jira_issue_key_regex.source
- Gitlab::UntrustedRegexp.new("\\b#{jira_issue_prefix}(?P<issue>#{jira_issue_regex})")
+ Gitlab::UntrustedRegexp.new("\\b#{jira_issue_prefix}(?P<issue>#{jira_regex})")
end
def parse_project_from_issue_key(issue_key)
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
index cabd3924fd6..33de5aa21aa 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -35,10 +35,11 @@ class PagesDomain < ApplicationRecord
validates :verification_code, presence: true, allow_blank: false
validate :validate_pages_domain
+ validate :max_certificate_key_length, if: ->(domain) { domain.key.present? }
validate :validate_matching_key, if: ->(domain) { domain.certificate.present? || domain.key.present? }
- validate :validate_intermediates, if: ->(domain) { domain.certificate.present? && domain.certificate_changed? }
+ # validate_intermediates must run after key validations to skip expensive SSL validation when there is a key error
+ validate :validate_intermediates, if: ->(domain) { domain.certificate.present? && domain.certificate_changed? && errors[:key].blank? }
validate :validate_custom_domain_count_per_project, on: :create
- validate :max_certificate_key_length, if: ->(domain) { domain.key.present? }
attribute :auto_ssl_enabled, default: -> { ::Gitlab::LetsEncrypt.enabled? }
attribute :wildcard, default: false
diff --git a/app/models/user.rb b/app/models/user.rb
index 925fd295611..fd15ee035ea 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1307,9 +1307,11 @@ class User < MainClusterwide::ApplicationRecord
several_namespaces? || admin
end
- def can?(action, subject = :global)
- Ability.allowed?(self, action, subject)
+ # rubocop: disable Style/ArgumentsForwarding -- https://gitlab.com/gitlab-org/gitlab/-/issues/433045
+ def can?(action, subject = :global, **opts)
+ Ability.allowed?(self, action, subject, **opts)
end
+ # rubocop: enable Style/ArgumentsForwarding
def confirm_deletion_with_password?
!password_automatically_set? && allow_password_authentication?
diff --git a/app/policies/ci/pipeline_schedule_policy.rb b/app/policies/ci/pipeline_schedule_policy.rb
index cbc60c4a30a..9e558cd91c1 100644
--- a/app/policies/ci/pipeline_schedule_policy.rb
+++ b/app/policies/ci/pipeline_schedule_policy.rb
@@ -25,7 +25,7 @@ module Ci
rule { can?(:create_pipeline) }.enable :play_pipeline_schedule
- rule { can?(:admin_pipeline) | (can?(:update_build) & owner_of_schedule) }.policy do
+ rule { can?(:admin_pipeline) | (owner_of_schedule & can?(:update_build)) }.policy do
enable :admin_pipeline_schedule
enable :read_pipeline_schedule_variables
end
@@ -45,6 +45,7 @@ module Ci
rule { protected_ref }.policy do
prevent :play_pipeline_schedule
prevent :create_pipeline_schedule
+ prevent :update_pipeline_schedule
end
private
diff --git a/app/policies/issue_policy.rb b/app/policies/issue_policy.rb
index 683c53d8d78..c95cde86e38 100644
--- a/app/policies/issue_policy.rb
+++ b/app/policies/issue_policy.rb
@@ -60,6 +60,7 @@ class IssuePolicy < IssuablePolicy
rule { ~can?(:read_issue) }.policy do
prevent :create_note
prevent :read_note
+ prevent :award_emoji
end
rule { locked }.policy do
diff --git a/app/services/ci/pipeline_schedules/base_save_service.rb b/app/services/ci/pipeline_schedules/base_save_service.rb
index 45d70e5a65d..e6f633498e9 100644
--- a/app/services/ci/pipeline_schedules/base_save_service.rb
+++ b/app/services/ci/pipeline_schedules/base_save_service.rb
@@ -23,7 +23,11 @@ module Ci
attr_reader :project, :user, :params, :schedule
def allowed_to_save?
- user.can?(self.class::AUTHORIZE, schedule)
+ # Disable cache because the same ability may already have been checked
+ # for the same records with different attributes. For example, we do not
+ # want an unauthorized user to change an unprotected ref to a protected
+ # ref.
+ user.can?(self.class::AUTHORIZE, schedule, cache: false)
end
def forbidden_to_save
diff --git a/app/services/ci/pipeline_schedules/update_service.rb b/app/services/ci/pipeline_schedules/update_service.rb
index 2fd1173ecce..76b2121c4e1 100644
--- a/app/services/ci/pipeline_schedules/update_service.rb
+++ b/app/services/ci/pipeline_schedules/update_service.rb
@@ -12,6 +12,12 @@ module Ci
@params = params
end
+ def execute
+ return forbidden_to_save unless allowed_to_save?
+
+ super
+ end
+
private
def authorize_message
diff --git a/app/services/members/creator_service.rb b/app/services/members/creator_service.rb
index 22d8b30db18..d7bf073d8e9 100644
--- a/app/services/members/creator_service.rb
+++ b/app/services/members/creator_service.rb
@@ -156,12 +156,13 @@ module Members
end
def commit_member
- if can_commit_member?
- assign_member_attributes
- commit_changes
- else
- add_commit_error
- end
+ return add_commit_error unless can_commit_member?
+
+ assign_member_attributes
+
+ return add_member_role_error if member_role_too_high?
+
+ commit_changes
end
def can_commit_member?
@@ -175,6 +176,11 @@ module Members
end
end
+ # overridden in Members::Groups::CreatorService
+ def member_role_too_high?
+ false
+ end
+
def can_create_new_member?
raise NotImplementedError
end
@@ -240,6 +246,12 @@ module Members
member.errors.add(:base, msg)
end
+ def add_member_role_error
+ msg = _("the member access level can't be higher than the current user's one")
+
+ member.errors.add(:base, msg)
+ end
+
def find_or_build_member
@member = builder.new(source, invitee, existing_members).execute
end
diff --git a/app/services/projects/lfs_pointers/lfs_link_service.rb b/app/services/projects/lfs_pointers/lfs_link_service.rb
index f8f03d481af..852f5e0222e 100644
--- a/app/services/projects/lfs_pointers/lfs_link_service.rb
+++ b/app/services/projects/lfs_pointers/lfs_link_service.rb
@@ -6,7 +6,7 @@ module Projects
class LfsLinkService < BaseService
TooManyOidsError = Class.new(StandardError)
- MAX_OIDS = 100_000
+ MAX_OIDS = ENV.fetch('GITLAB_LFS_MAX_OID_TO_FETCH', 100_000).to_i
BATCH_SIZE = 1000
# Accept an array of oids to link
diff --git a/app/views/groups/_invite_members_modal.html.haml b/app/views/groups/_invite_members_modal.html.haml
index d53190948fd..c60e7a78b1d 100644
--- a/app/views/groups/_invite_members_modal.html.haml
+++ b/app/views/groups/_invite_members_modal.html.haml
@@ -1,7 +1,7 @@
- return unless can_admin_group_member?(group)
.js-invite-members-modal{ data: { is_project: 'false',
- access_levels: group.access_level_roles.to_json,
+ access_levels: access_level_roles_user_can_assign(group).to_json,
reload_page_on_submit: current_path?('group_members#index').to_s,
help_link: help_page_url('user/permissions'),
is_signup_enabled: signup_enabled?.to_s,
diff --git a/config/feature_flags/development/restrict_pipeline_cancellation_by_role.yml b/config/feature_flags/development/restrict_pipeline_cancellation_by_role.yml
deleted file mode 100644
index 0ef8a5d38db..00000000000
--- a/config/feature_flags/development/restrict_pipeline_cancellation_by_role.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: restrict_pipeline_cancellation_by_role
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135047
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/429699
-milestone: '16.6'
-type: development
-group: group::pipeline execution
-default_enabled: false
diff --git a/doc/administration/environment_variables.md b/doc/administration/environment_variables.md
index 260095d12ac..e060a1917e1 100644
--- a/doc/administration/environment_variables.md
+++ b/doc/administration/environment_variables.md
@@ -39,6 +39,7 @@ You can use the following environment variables to override certain values:
| `RAILS_ENV` | string | The Rails environment; can be one of `production`, `development`, `staging`, or `test`. |
| `GITLAB_RAILS_CACHE_DEFAULT_TTL_SECONDS` | integer | The default TTL used for entries stored in the Rails-cache. Default is `28800`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/95042) in 15.3. |
| `GITLAB_CI_CONFIG_FETCH_TIMEOUT_SECONDS` | integer | Timeout for resolving remote includes in CI config in seconds. Must be between `0` and `60`. Default is `30`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/116383) in 15.11. |
+| `GITLAB_LFS_MAX_OID_TO_FETCH` | integer | Sets the maximum number of LFS objects to link. Default is `100,000`. |
## Adding more variables
diff --git a/doc/api/dependency_list_export.md b/doc/api/dependency_list_export.md
index db43ea238c1..e7a1a06d56f 100644
--- a/doc/api/dependency_list_export.md
+++ b/doc/api/dependency_list_export.md
@@ -8,18 +8,10 @@ info: To determine the technical writer assigned to the Stage/Group associated w
Every call to this endpoint requires authentication.
-## Create a pipeline-level dependency list export **(EXPERIMENT)**
+## Create a pipeline-level dependency list export
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/333463) in GitLab 16.4 [with a flag](../administration/feature_flags.md) named `merge_sbom_api`. Enabled by default. This feature is an [Experiment](../policy/experiment-beta-support.md#experiment).
-
-FLAG:
-On self-managed GitLab, by default this feature is available.
-To hide the feature, an administrator can [disable the feature flag](../administration/feature_flags.md) named `merge_sbom_api`.
-On GitLab.com, this feature is available.
-
-WARNING:
-This feature is an [Experiment](../policy/experiment-beta-support.md#experiment)
-and subject to change without notice.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/333463) in GitLab 16.4 [with a flag](../administration/feature_flags.md) named `merge_sbom_api`. Enabled by default.
+> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/425312) in GitLab 16.7. Feature flag `merge_sbom_api` removed.
Create a new CycloneDX JSON export for all the project dependencies detected in a pipeline.
diff --git a/doc/architecture/blueprints/new_diffs.md b/doc/architecture/blueprints/new_diffs.md
index 402c81a895b..d5768f6c914 100644
--- a/doc/architecture/blueprints/new_diffs.md
+++ b/doc/architecture/blueprints/new_diffs.md
@@ -1,316 +1,11 @@
---
-status: proposed
-creation-date: "2023-10-10"
-authors: [ "@iamphill" ]
-coach: [ "@ntepluhina" ]
-approvers: [ ]
-owning-stage: "~devops::create"
-participating-stages: []
+redirect_to: 'new_diffs/index.md'
+remove_date: '2024-02-29'
---
-<!-- Blueprints often contain forward-looking statements -->
-<!-- vale gitlab.FutureTense = NO -->
+This document was moved to [another location](new_diffs/index.md).
-# New diffs
-
-## Summary
-
-Diffs at GitLab are spread across several places with each area using their own method. We are aiming
-to develop a single, performant way for diffs to be rendered across the application. Our aim here is
-to improve all areas of diff rendering, from the backend creation of diffs to the frontend rendering
-the diffs.
-
-## Motivation
-
-### Goals
-
-- improved perceived performance
-- improved maintainability
-- consistent coverage of all scenarios
-
-### Non-Goals
-
-<!--
-Listing non-goals helps to focus discussion and make progress. This section is
-optional.
-
-- What is out of scope for this blueprint?
--->
-
-### Priority of Goals
-
-In an effort to provide guidance on which goals are more important than others to assist in making
-consistent choices, despite all goals being important, we defined the following order.
-
-**Perceived performance** is above **improved maintainability** is above **consistent coverage**.
-
-Examples:
-
-- a proposal improves maintainability at the cost of perceived performance: ❌ we should consider an alternative.
-- a proposal removes a feature from certain contexts, hurting coverage, and has no impact on perceived performance or maintainability: ❌ we should re-consider.
-- a proposal improves perceived performance but removes features from certain contexts of usage: ✅ it's valid and should be discussed with Product/UX.
-- a proposal guarantees consistent coverage and has no impact on perceived performance or maintainability: ✅ it's valid.
-
-In essence, we'll strive to meet every goal at each decision but prioritise the higher ones.
-
-## Proposal
-
-<!--
-This is where we get down to the specifics of what the proposal actually is,
-but keep it simple! This should have enough detail that reviewers can
-understand exactly what you're proposing, but should not include things like
-API designs or implementation. The "Design Details" section below is for the
-real nitty-gritty.
-
-You might want to consider including the pros and cons of the proposed solution so that they can be
-compared with the pros and cons of alternatives.
--->
-
-### Accessibility
-
-New diffs should be displayed in a way that is compliant with [Web Content Accessibility Guidelines 2.1](https://www.w3.org/TR/WCAG21/) level AA for web-based content and [Authoring Tool Accessibility Guidelines 2.0](https://www.w3.org/TR/ATAG20/) level AA for user interface.
-
-## Design and implementation details
-
-### Workspace & Artifacts
-
-- We will store implementation details like metrics, budgets, and development & architectural patterns here in the docs
-- We will store large bodies of research, the results of audits, etc. in the [wiki](https://gitlab.com/gitlab-com/create-stage/new-diffs/-/wikis/home) of the [New Diffs project](https://gitlab.com/gitlab-com/create-stage/new-diffs)
-- We will store audio & video recordings on the public YouTube channel in the Code Review / New Diffs playlist
-- We will store drafts, meeting notes, and other temporary documents in public Google docs
-
-### Definitions
-
-#### Maintainability
-
-Maintainable projects are _simple_ projects.
-
-Simplicity is the opposite of complexity. This uses a definition of simple and complex [described by Rich Hickey in "Simple Made Easy"](https://www.infoq.com/presentations/Simple-Made-Easy/) (Strange Loop, 2011).
-
-- Maintainable code is simple (single task, single concept, separate from other things).
-- Maintainable projects expand on simple code by having simple structure (folders define classes of behaviors, e.g. you can be assured that a component directory will never initiate a network call, because that would be conflating visual display with data access)
-- Maintainable applications flow out of simple organization and simple code. The old saying is a cluttered desk is representative of a cluttered mind. Rigorous discipline on simplicity will be represented in our output (the product). By being strict about working simply, we will naturally produce applications where our users can more easily reason about their behavior.
-
-#### Done
-
-GitLab has an existing [definition of done](/ee/development/contributing/merge_request_workflow.md#definition-of-done) which is geared primarily toward identifying when an MR is ready to be merged.
-
-In addition to the items in the GitLab definition of done, work on new diffs should also adhere to the following requirements:
-
-- Meets or exceeds all metrics
- - Meets or exceeds our minimum accessibility metrics (these are explicitly not part of our defined priorities, because they are non-negotiable)
-- All work is fully documented for engineers (user documentation is a requirement of the standard definition of done)
-
-### Metrics
-
-To measure our success, we need to set meaningful metrics. These metrics should meaningfully and positively impact the end user.
-
-1. Meets or exceeds [WCAG 2.2 AA](https://www.w3.org/TR/WCAG22/).
-1. Meets or exceeds [ATAG 2.0 AA](https://www.w3.org/TR/ATAG20/).
-1. The new Diffs app loads less than or equal to 300 KiB of JavaScript (compressed / "across-the-wire")<sup>1</sup>.
-1. The new Diffs app loads less than or equal to 150 KiB of markup, images, styles, fonts, etc. (compressed / "across-the-wire")<sup>1</sup>.
-1. The new Diffs app can load and execute in total isolation from the rest of the GitLab product:
- 1. "Execute" means the app can load, display data, and allows user interaction ("read-only").
- 1. If a part of the application is only used in merge requests or diffs, it is considered part of the Diffs application.
- 1. If a part of the application must be brought in from the rest of the product, it is not considered part of the Diffs load (as defined in metrics 3 and 4).
- 1. If a part of the application must be brought in from the rest of the product, it may not block functionality of the Diffs application.
- 1. If a part of the application must be brought in from the rest of the product, it must be loaded asynchronously.
- 1. If a part of the application meets 5.1-5.5 _(such as: the Markdown editor is loaded asynchronously when the user would like to leave a comment on a diff)_ and its inclusion causes a budget overflow:
- - It must be added to a list of documented exceptions that we accept are out of bounds and out of our control.
- - The exceptions list should be addressed on a regular basis to determine the ongoing value of overflowing our budget.
-
----
-<sup>1</sup>: [The Performance Inequality Gap, 2023](https://infrequently.org/2022/12/performance-baseline-2023/)
-
-### Front end
-
-Ideally, we would meet our definition of done and our accountability metrics on our first try.
-We also need to continue to stay within those boundaries as we move forward. To ensure this,
-we need to design an application architecture that:
-
-1. Is:
- 1. Scalable.
- 1. Malleable.
- 1. Flexible.
-1. Considers itself a mission-critical part of the overall GitLab product.
-1. Treats itself as a complex, unique application with concerns that cannot be addressed
- as side effects of other parts of the product.
-1. Can handle data access/format changes without making UI changes.
-1. Can handle UI changes without making data access/format changes.
-1. Provides a hookable, inspectable API and avoids code coupling.
-1. Separates:
- - State and application data.
- - Application behavior and UI.
- - Data access and network access.
-
-#### High-level implementation
-
-(See [New Diffs: Technical Architecture Design](https://gitlab.com/gitlab-org/gitlab/-/issues/431276) for nicer visuals of this chart)
-
-```mermaid
-flowchart TB
- classDef sticky fill:#d0cabf, color:black
- stickyMetricsA>"Metrics 3, 4, & 5 apply to<br>the entire front end application"]
-
- stickyMetricsA -.- fe
- fe
-
- Socket((WebSocket))
-
- be
-
-subgraph fe [Front End]
- stickyMetricsB>"Metrics 1 & 2 apply<br>to all UI elements"]
- stickyInbound>"All data is formatted precisely<br>how the UI needs to interact with it"]
- stickyOutbound>"All data is formatted precisely<br>how the back end expects it"]
- stickyIdb>"Long-term.
-
- e.g. diffs, MRs, emoji, notes, drafts, user-only data<br>like file reviews, collapse states, etc."]
- stickySession>"Session-term.
-
- e.g. selected tab, scroll position,<br>temporary changes to user settings, etc."]
-
- Events([Event Hub])
- UI[UI]
- uiState((Local State))
- Logic[Application Logic]
- Normalizer[Data Normalizer]
- Inbound{{Inbound Contract}}
- Outbound{{Outbound Contract}}
- Data[Data Access]
- idb((indexedDB))
- session((sessionStorage))
- Network[Network Access]
-end
-
-subgraph be [Back End]
- stickyApi>"A large list of defined actions a<br>Diffs/Merge Request UI could perform.
-
- e.g.: <code>mergeRequest:notes:saveDraft</code> or<br><code>mergeRequest:changeStatus</code> (with <br><code>status: 'draft'</code> or <code>status: 'ready'</code>, etc.).
-
- Must not expose any implementation detail,<br>like models, storage structure, etc."]
- API[Activities API]
- unk[\"?"/]
-
- API -.- stickyApi
-end
-
- %% Make stickies look like paper sort of?
- class stickyMetricsA,stickyMetricsB,stickyInbound,stickyOutbound,stickyIdb,stickySession,stickyApi sticky
-
- UI <--> uiState
- stickyMetricsB -.- UI
- Network ~~~ stickyMetricsB
-
- Logic <--> Normalizer
-
- Normalizer --> Outbound
- Outbound --> Data
- Inbound --> Normalizer
- Data --> Inbound
-
- Inbound -.- stickyInbound
- Outbound -.- stickyOutbound
-
- Data <--> idb
- Data <--> session
- idb -.- stickyIdb
- session -.- stickySession
-
- Events <--> UI
- Events <--> Logic
- Events <--> Data
- Events <--> Network
-
- Network --> Socket --> API --> unk
-```
-
-<!--
-This section should contain enough information that the specifics of your
-change are understandable. This may include API specs (though not always
-required) or even code snippets. If there's any ambiguity about HOW your
-proposal will be implemented, this is the place to discuss them.
-
-If you are not sure how many implementation details you should include in the
-blueprint, the rule of thumb here is to provide enough context for people to
-understand the proposal. As you move forward with the implementation, you may
-need to add more implementation details to the blueprint, as those may become
-an important context for important technical decisions made along the way. A
-blueprint is also a register of such technical decisions. If a technical
-decision requires additional context before it can be made, you probably should
-document this context in a blueprint. If it is a small technical decision that
-can be made in a merge request by an author and a maintainer, you probably do
-not need to document it here. The impact a technical decision will have is
-another helpful information - if a technical decision is very impactful,
-documenting it, along with associated implementation details, is advisable.
-
-If it's helpful to include workflow diagrams or any other related images.
-Diagrams authored in GitLab flavored markdown are preferred. In cases where
-that is not feasible, images should be placed under `images/` in the same
-directory as the `index.md` for the proposal.
--->
-
-#### HTML structure
-
-The HTML structure of a diff should have support for assistive technology.
-For this reason, a table could be a preferred solution as it allows to indicate
-logical relationship between the presented data and is easier to navigate for
-screen reader users with keyboard. Labeled columns will make sure that information
-such as line numbers can be associated with the edited piece of code.
-
-Possible structure could include:
-
-```html
-<table>
- <caption class="gl-sr-only">Changes for file index.js. 10 lines changed: 5 deleted, 5 added.</caption>
- <tr hidden>
- <th>Original line number: </th>
- <th>Diff line number: </th>
- <th>Line change:</th>
- </tr>
- <tr>
- <td>1234</td>
- <td></td>
- <td>.tree-time-ago ,</td>
- </tr>
- […]
-</table>
-```
-
-See [WAI tutorial on tables](https://www.w3.org/WAI/tutorials/tables) for
-more implementation guidelines.
-
-Each file table should include a short summary of changes that will read out:
-
-- total number of lines changed,
-- number of added lines,
-- number of removed lines.
-
-The summary of the table content can be placed either within `<caption>` element, or before the table within an element referred as `aria-describedby`.
-See <abbr>WAI</abbr> (Web Accessibility Initiative) for more information on both approaches:
-
-- [Nesting summary inside the <caption> element](https://www.w3.org/WAI/tutorials/tables/caption-summary/#nesting-summary-inside-the-caption-element)
-- [Using aria-describedby to provide a table summary](https://www.w3.org/WAI/tutorials/tables/caption-summary/#using-aria-describedby-to-provide-a-table-summary)
-
-However, if such a structure will compromise other functional aspects of displaying a diff,
-more generic elements together with ARIA support can be used.
-
-#### Visual indicators
-
-It is important that each visual indicator should have a screen reader text
-denoting the meaning of that indicator. When needed, use `gl-sr-only` or `gl-sr-only-focusable`
-class to make the element accessible by screen readers, but not by sighted users.
-
-Some of the visual indicators that require alternatives for assistive technology are:
-
-- `+` or red highlighting to be read as `added`
-- `-` or green highlighting to be read as `removed`
-
-## Alternative Solutions
-
-<!--
-It might be a good idea to include a list of alternative solutions or paths considered, although it is not required. Include pros and cons for
-each alternative solution/path.
-
-"Do nothing" and its pros and cons could be included in the list too.
--->
+<!-- This redirect file can be deleted after <2024-02-29>. -->
+<!-- Redirects that point to other docs in the same project expire in three months. -->
+<!-- Redirects that point to docs in a different project or site (for example, link is not relative and starts with `https:`) expire in one year. -->
+<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html -->
diff --git a/doc/architecture/blueprints/new_diffs/index.md b/doc/architecture/blueprints/new_diffs/index.md
new file mode 100644
index 00000000000..2aabfe4c618
--- /dev/null
+++ b/doc/architecture/blueprints/new_diffs/index.md
@@ -0,0 +1,316 @@
+---
+status: proposed
+creation-date: "2023-10-10"
+authors: [ "@thomasrandolph", "@patrickbajao", "@igor.drozdov", "@jerasmus", "@iamphill" ]
+coach: [ "@ntepluhina" ]
+approvers: [ ]
+owning-stage: "~devops::create"
+participating-stages: []
+---
+
+<!-- Blueprints often contain forward-looking statements -->
+<!-- vale gitlab.FutureTense = NO -->
+
+# New diffs
+
+## Summary
+
+Diffs at GitLab are spread across several places with each area using their own method. We are aiming
+to develop a single, performant way for diffs to be rendered across the application. Our aim here is
+to improve all areas of diff rendering, from the backend creation of diffs to the frontend rendering
+the diffs.
+
+## Motivation
+
+### Goals
+
+- improved perceived performance
+- improved maintainability
+- consistent coverage of all scenarios
+
+### Non-Goals
+
+<!--
+Listing non-goals helps to focus discussion and make progress. This section is
+optional.
+
+- What is out of scope for this blueprint?
+-->
+
+### Priority of Goals
+
+In an effort to provide guidance on which goals are more important than others to assist in making
+consistent choices, despite all goals being important, we defined the following order.
+
+**Perceived performance** is above **improved maintainability** is above **consistent coverage**.
+
+Examples:
+
+- a proposal improves maintainability at the cost of perceived performance: ❌ we should consider an alternative.
+- a proposal removes a feature from certain contexts, hurting coverage, and has no impact on perceived performance or maintainability: ❌ we should re-consider.
+- a proposal improves perceived performance but removes features from certain contexts of usage: ✅ it's valid and should be discussed with Product/UX.
+- a proposal guarantees consistent coverage and has no impact on perceived performance or maintainability: ✅ it's valid.
+
+In essence, we'll strive to meet every goal at each decision but prioritise the higher ones.
+
+## Proposal
+
+<!--
+This is where we get down to the specifics of what the proposal actually is,
+but keep it simple! This should have enough detail that reviewers can
+understand exactly what you're proposing, but should not include things like
+API designs or implementation. The "Design Details" section below is for the
+real nitty-gritty.
+
+You might want to consider including the pros and cons of the proposed solution so that they can be
+compared with the pros and cons of alternatives.
+-->
+
+### Accessibility
+
+New diffs should be displayed in a way that is compliant with [Web Content Accessibility Guidelines 2.1](https://www.w3.org/TR/WCAG21/) level AA for web-based content and [Authoring Tool Accessibility Guidelines 2.0](https://www.w3.org/TR/ATAG20/) level AA for user interface.
+
+## Design and implementation details
+
+### Workspace & Artifacts
+
+- We will store implementation details like metrics, budgets, and development & architectural patterns here in the docs
+- We will store large bodies of research, the results of audits, etc. in the [wiki](https://gitlab.com/gitlab-com/create-stage/new-diffs/-/wikis/home) of the [New Diffs project](https://gitlab.com/gitlab-com/create-stage/new-diffs)
+- We will store audio & video recordings on the public YouTube channel in the Code Review / New Diffs playlist
+- We will store drafts, meeting notes, and other temporary documents in public Google docs
+
+### Definitions
+
+#### Maintainability
+
+Maintainable projects are _simple_ projects.
+
+Simplicity is the opposite of complexity. This uses a definition of simple and complex [described by Rich Hickey in "Simple Made Easy"](https://www.infoq.com/presentations/Simple-Made-Easy/) (Strange Loop, 2011).
+
+- Maintainable code is simple (single task, single concept, separate from other things).
+- Maintainable projects expand on simple code by having simple structure (folders define classes of behaviors, e.g. you can be assured that a component directory will never initiate a network call, because that would be conflating visual display with data access)
+- Maintainable applications flow out of simple organization and simple code. The old saying is a cluttered desk is representative of a cluttered mind. Rigorous discipline on simplicity will be represented in our output (the product). By being strict about working simply, we will naturally produce applications where our users can more easily reason about their behavior.
+
+#### Done
+
+GitLab has an existing [definition of done](/ee/development/contributing/merge_request_workflow.md#definition-of-done) which is geared primarily toward identifying when an MR is ready to be merged.
+
+In addition to the items in the GitLab definition of done, work on new diffs should also adhere to the following requirements:
+
+- Meets or exceeds all metrics
+ - Meets or exceeds our minimum accessibility metrics (these are explicitly not part of our defined priorities, because they are non-negotiable)
+- All work is fully documented for engineers (user documentation is a requirement of the standard definition of done)
+
+### Metrics
+
+To measure our success, we need to set meaningful metrics. These metrics should meaningfully and positively impact the end user.
+
+1. Meets or exceeds [WCAG 2.2 AA](https://www.w3.org/TR/WCAG22/).
+1. Meets or exceeds [ATAG 2.0 AA](https://www.w3.org/TR/ATAG20/).
+1. The new Diffs app loads less than or equal to 300 KiB of JavaScript (compressed / "across-the-wire")<sup>1</sup>.
+1. The new Diffs app loads less than or equal to 150 KiB of markup, images, styles, fonts, etc. (compressed / "across-the-wire")<sup>1</sup>.
+1. The new Diffs app can load and execute in total isolation from the rest of the GitLab product:
+ 1. "Execute" means the app can load, display data, and allows user interaction ("read-only").
+ 1. If a part of the application is only used in merge requests or diffs, it is considered part of the Diffs application.
+ 1. If a part of the application must be brought in from the rest of the product, it is not considered part of the Diffs load (as defined in metrics 3 and 4).
+ 1. If a part of the application must be brought in from the rest of the product, it may not block functionality of the Diffs application.
+ 1. If a part of the application must be brought in from the rest of the product, it must be loaded asynchronously.
+ 1. If a part of the application meets 5.1-5.5 _(such as: the Markdown editor is loaded asynchronously when the user would like to leave a comment on a diff)_ and its inclusion causes a budget overflow:
+ - It must be added to a list of documented exceptions that we accept are out of bounds and out of our control.
+ - The exceptions list should be addressed on a regular basis to determine the ongoing value of overflowing our budget.
+
+---
+<sup>1</sup>: [The Performance Inequality Gap, 2023](https://infrequently.org/2022/12/performance-baseline-2023/)
+
+### Front end
+
+Ideally, we would meet our definition of done and our accountability metrics on our first try.
+We also need to continue to stay within those boundaries as we move forward. To ensure this,
+we need to design an application architecture that:
+
+1. Is:
+ 1. Scalable.
+ 1. Malleable.
+ 1. Flexible.
+1. Considers itself a mission-critical part of the overall GitLab product.
+1. Treats itself as a complex, unique application with concerns that cannot be addressed
+ as side effects of other parts of the product.
+1. Can handle data access/format changes without making UI changes.
+1. Can handle UI changes without making data access/format changes.
+1. Provides a hookable, inspectable API and avoids code coupling.
+1. Separates:
+ - State and application data.
+ - Application behavior and UI.
+ - Data access and network access.
+
+#### High-level implementation
+
+(See [New Diffs: Technical Architecture Design](https://gitlab.com/gitlab-org/gitlab/-/issues/431276) for nicer visuals of this chart)
+
+```mermaid
+flowchart TB
+ classDef sticky fill:#d0cabf, color:black
+ stickyMetricsA>"Metrics 3, 4, & 5 apply to<br>the entire front end application"]
+
+ stickyMetricsA -.- fe
+ fe
+
+ Socket((WebSocket))
+
+ be
+
+subgraph fe [Front End]
+ stickyMetricsB>"Metrics 1 & 2 apply<br>to all UI elements"]
+ stickyInbound>"All data is formatted precisely<br>how the UI needs to interact with it"]
+ stickyOutbound>"All data is formatted precisely<br>how the back end expects it"]
+ stickyIdb>"Long-term.
+
+ e.g. diffs, MRs, emoji, notes, drafts, user-only data<br>like file reviews, collapse states, etc."]
+ stickySession>"Session-term.
+
+ e.g. selected tab, scroll position,<br>temporary changes to user settings, etc."]
+
+ Events([Event Hub])
+ UI[UI]
+ uiState((Local State))
+ Logic[Application Logic]
+ Normalizer[Data Normalizer]
+ Inbound{{Inbound Contract}}
+ Outbound{{Outbound Contract}}
+ Data[Data Access]
+ idb((indexedDB))
+ session((sessionStorage))
+ Network[Network Access]
+end
+
+subgraph be [Back End]
+ stickyApi>"A large list of defined actions a<br>Diffs/Merge Request UI could perform.
+
+ e.g.: <code>mergeRequest:notes:saveDraft</code> or<br><code>mergeRequest:changeStatus</code> (with <br><code>status: 'draft'</code> or <code>status: 'ready'</code>, etc.).
+
+ Must not expose any implementation detail,<br>like models, storage structure, etc."]
+ API[Activities API]
+ unk[\"?"/]
+
+ API -.- stickyApi
+end
+
+ %% Make stickies look like paper sort of?
+ class stickyMetricsA,stickyMetricsB,stickyInbound,stickyOutbound,stickyIdb,stickySession,stickyApi sticky
+
+ UI <--> uiState
+ stickyMetricsB -.- UI
+ Network ~~~ stickyMetricsB
+
+ Logic <--> Normalizer
+
+ Normalizer --> Outbound
+ Outbound --> Data
+ Inbound --> Normalizer
+ Data --> Inbound
+
+ Inbound -.- stickyInbound
+ Outbound -.- stickyOutbound
+
+ Data <--> idb
+ Data <--> session
+ idb -.- stickyIdb
+ session -.- stickySession
+
+ Events <--> UI
+ Events <--> Logic
+ Events <--> Data
+ Events <--> Network
+
+ Network --> Socket --> API --> unk
+```
+
+<!--
+This section should contain enough information that the specifics of your
+change are understandable. This may include API specs (though not always
+required) or even code snippets. If there's any ambiguity about HOW your
+proposal will be implemented, this is the place to discuss them.
+
+If you are not sure how many implementation details you should include in the
+blueprint, the rule of thumb here is to provide enough context for people to
+understand the proposal. As you move forward with the implementation, you may
+need to add more implementation details to the blueprint, as those may become
+an important context for important technical decisions made along the way. A
+blueprint is also a register of such technical decisions. If a technical
+decision requires additional context before it can be made, you probably should
+document this context in a blueprint. If it is a small technical decision that
+can be made in a merge request by an author and a maintainer, you probably do
+not need to document it here. The impact a technical decision will have is
+another helpful information - if a technical decision is very impactful,
+documenting it, along with associated implementation details, is advisable.
+
+If it's helpful to include workflow diagrams or any other related images.
+Diagrams authored in GitLab flavored markdown are preferred. In cases where
+that is not feasible, images should be placed under `images/` in the same
+directory as the `index.md` for the proposal.
+-->
+
+#### HTML structure
+
+The HTML structure of a diff should have support for assistive technology.
+For this reason, a table could be a preferred solution as it allows to indicate
+logical relationship between the presented data and is easier to navigate for
+screen reader users with keyboard. Labeled columns will make sure that information
+such as line numbers can be associated with the edited piece of code.
+
+Possible structure could include:
+
+```html
+<table>
+ <caption class="gl-sr-only">Changes for file index.js. 10 lines changed: 5 deleted, 5 added.</caption>
+ <tr hidden>
+ <th>Original line number: </th>
+ <th>Diff line number: </th>
+ <th>Line change:</th>
+ </tr>
+ <tr>
+ <td>1234</td>
+ <td></td>
+ <td>.tree-time-ago ,</td>
+ </tr>
+ […]
+</table>
+```
+
+See [WAI tutorial on tables](https://www.w3.org/WAI/tutorials/tables) for
+more implementation guidelines.
+
+Each file table should include a short summary of changes that will read out:
+
+- total number of lines changed,
+- number of added lines,
+- number of removed lines.
+
+The summary of the table content can be placed either within `<caption>` element, or before the table within an element referred as `aria-describedby`.
+See <abbr>WAI</abbr> (Web Accessibility Initiative) for more information on both approaches:
+
+- [Nesting summary inside the <caption> element](https://www.w3.org/WAI/tutorials/tables/caption-summary/#nesting-summary-inside-the-caption-element)
+- [Using aria-describedby to provide a table summary](https://www.w3.org/WAI/tutorials/tables/caption-summary/#using-aria-describedby-to-provide-a-table-summary)
+
+However, if such a structure will compromise other functional aspects of displaying a diff,
+more generic elements together with ARIA support can be used.
+
+#### Visual indicators
+
+It is important that each visual indicator should have a screen reader text
+denoting the meaning of that indicator. When needed, use `gl-sr-only` or `gl-sr-only-focusable`
+class to make the element accessible by screen readers, but not by sighted users.
+
+Some of the visual indicators that require alternatives for assistive technology are:
+
+- `+` or red highlighting to be read as `added`
+- `-` or green highlighting to be read as `removed`
+
+## Alternative Solutions
+
+<!--
+It might be a good idea to include a list of alternative solutions or paths considered, although it is not required. Include pros and cons for
+each alternative solution/path.
+
+"Do nothing" and its pros and cons could be included in the list too.
+-->
diff --git a/doc/ci/pipelines/settings.md b/doc/ci/pipelines/settings.md
index 4768c04c748..31a2b74f555 100644
--- a/doc/ci/pipelines/settings.md
+++ b/doc/ci/pipelines/settings.md
@@ -105,7 +105,7 @@ For more information, see [Deployment safety](../environments/deployment_safety.
## Restrict roles that can cancel pipelines or jobs **(PREMIUM ALL)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/410634) in GitLab 16.7 [with a flag](../../administration/feature_flags.md) named `restrict_pipeline_cancellation_by_role`. Disabled by default.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/137301) in GitLab 16.7.
You can customize which roles have permission to cancel pipelines or jobs.
diff --git a/doc/gitlab-basics/add-file.md b/doc/gitlab-basics/add-file.md
index b989f5fa05e..1b013472aa2 100644
--- a/doc/gitlab-basics/add-file.md
+++ b/doc/gitlab-basics/add-file.md
@@ -86,6 +86,16 @@ repository.
To create a merge request, copy the link sent back from the remote
repository and paste it into a browser window.
+## Add a file to the last commit
+
+```shell
+git add <filename>
+git commit --amend
+```
+
+Append `--no-edit` to the `commit` command if you do not want to edit the commit
+message.
+
## Related topics
- [Add file from the UI](../user/project/repository/index.md#add-a-file-from-the-ui)
diff --git a/doc/topics/git/rollback_commits.md b/doc/topics/git/rollback_commits.md
index 88999015c69..5e48a366963 100644
--- a/doc/topics/git/rollback_commits.md
+++ b/doc/topics/git/rollback_commits.md
@@ -9,7 +9,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
In Git, if you make a mistake, you can undo or roll back your changes.
For more details, see [Undo options](numerous_undo_possibilities_in_git/index.md).
-## Undo commits
+## Undo commits by removing them
- Undo your last commit and put everything back in the staging area:
@@ -37,7 +37,7 @@ For more details, see [Undo options](numerous_undo_possibilities_in_git/index.md
git reset --hard HEAD^^
```
-## Git reset sample workflow
+### Git reset sample workflow
1. Edit file again `edit_this_file.rb`.
1. Check status.
@@ -63,6 +63,12 @@ git pull origin master
git push origin master
```
+## Undo commits with a new replacement commit
+
+```shell
+git revert <commit-sha>
+```
+
## The difference between `git revert` and `git reset`
- The `git reset` command removes the commit. The `git revert` command removes the changes but leaves the commit.
diff --git a/doc/topics/git/useful_git_commands.md b/doc/topics/git/useful_git_commands.md
index 6cc87a650b9..cd01cd0d495 100644
--- a/doc/topics/git/useful_git_commands.md
+++ b/doc/topics/git/useful_git_commands.md
@@ -9,75 +9,14 @@ type: reference
The following commands are frequently used.
-## Remotes
+## Add another URL to a remote
-### Add another URL to a remote, so both remotes get updated on each push
+Add another URL to a remote, so both remotes get updated on each push:
```shell
git remote set-url --add <remote_name> <remote_url>
```
-### Revert a file to HEAD state and remove changes
-
-To revert changes to a file, you can use either:
-
-- `git checkout <filename>`
-- `git reset --hard <filename>`
-
-### Undo a previous commit by creating a new replacement commit
-
-```shell
-git revert <commit-sha>
-```
-
-### Create a new message for last commit
-
-```shell
-git commit --amend
-```
-
-### Create a new message for older commits
-
-WARNING:
-Changing commit history can disrupt others' work if they have cloned, forked, or have active branches.
-Only amend pushed commits if you're sure it's safe.
-To learn more, see [Git rebase and force push](git_rebase.md).
-
-```shell
-git rebase -i HEAD~n
-```
-
-Replace `n` with the number of commits you want to go back.
-
-This opens your text editor with a list of commits.
-In the editor, replace `pick` with `reword` for each commit you want to change the message:
-
-```shell
-reword 1fc6c95 original commit message
-pick 6b2481b another commit message
-pick 5c1291b another commit message
-```
-
-After saving and closing the file, you can update each message in a new editor window.
-
-After updating your commits, you must push them to the repository.
-As this rewrites history, a force push is required.
-To prevent unintentional overwrites, use `--force-with-lease`:
-
-```shell
-git push --force-with-lease
-```
-
-### Add a file to the last commit
-
-```shell
-git add <filename>
-git commit --amend
-```
-
-Append `--no-edit` to the `commit` command if you do not want to edit the commit
-message.
-
## Refs and Log
### Use reflog to show the log of reference changes to HEAD
@@ -190,4 +129,4 @@ questions that you know someone might ask.
Each scenario can be a third-level heading, for example `### Getting error message X`.
If you have none to add when creating a doc, leave this section in place
-but commented out to help encourage others to add to it in the future. --> \ No newline at end of file
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/user/organization/index.md b/doc/user/organization/index.md
index 6213bdfc3d8..9458b4a4e73 100644
--- a/doc/user/organization/index.md
+++ b/doc/user/organization/index.md
@@ -42,17 +42,25 @@ To view the organizations you have access to:
## Create an organization
1. On the left sidebar, at the top, select **Create new** (**{plus}**) and **New organization**.
-1. In the **Organization name** field, enter a name for the organization.
-1. In the **Organization URL** field, enter a path for the organization.
+1. In the **Organization name** text box, enter a name for the organization.
+1. In the **Organization URL** text box, enter a path for the organization.
1. Select **Create organization**.
## Edit an organization's name
1. On the left sidebar, select **Organizations** (**{organization}**) and find the organization you want to edit.
1. Select **Settings > General**.
-1. Update the **Organization name** field.
+1. In the **Organization name** text box, edit the name.
1. Select **Save changes**.
+## Change an organization's URL
+
+1. On the left sidebar, select **Organizations** (**{organization}**) and find organization whose URL you want to change.
+1. Select **Settings > General**.
+1. Expand the **Advanced** section.
+1. In the **Organization URL** text box, edit the URL.
+1. Select **Change organization URL**.
+
## Manage groups and projects
1. On the left sidebar, select **Organizations** (**{organization}**) and find the organization you want to manage.
diff --git a/lib/bulk_imports/pipeline/runner.rb b/lib/bulk_imports/pipeline/runner.rb
index e2a14c35e79..e1da7c301e9 100644
--- a/lib/bulk_imports/pipeline/runner.rb
+++ b/lib/bulk_imports/pipeline/runner.rb
@@ -16,6 +16,8 @@ module BulkImports
if extracted_data
extracted_data.each_with_index do |entry, index|
+ refresh_entity_and_import if index % 1000 == 0
+
raw_entry = entry.dup
next if already_processed?(raw_entry, index)
@@ -193,6 +195,11 @@ module BulkImports
payload.stringify_keys.merge(context)
end
+
+ def refresh_entity_and_import
+ context.entity.touch
+ context.bulk_import.touch
+ end
end
end
end
diff --git a/lib/gitlab/analytics/cycle_analytics/request_params.rb b/lib/gitlab/analytics/cycle_analytics/request_params.rb
index e284ed76749..8ee80826537 100644
--- a/lib/gitlab/analytics/cycle_analytics/request_params.rb
+++ b/lib/gitlab/analytics/cycle_analytics/request_params.rb
@@ -121,6 +121,7 @@ module Gitlab
attrs[:enable_customizable_stages] = 'false'
attrs[:can_edit] = 'false'
attrs[:enable_projects_filter] = 'false'
+ attrs[:enable_vsd_link] = 'false'
attrs[:default_stages] = Gitlab::Analytics::CycleAnalytics::DefaultStages.all.map do |stage_params|
::Analytics::CycleAnalytics::StagePresenter.new(stage_params)
end.to_json
diff --git a/lib/gitlab/bitbucket_import/importers/issues_importer.rb b/lib/gitlab/bitbucket_import/importers/issues_importer.rb
index 8ab82ddb0be..678cb4e129d 100644
--- a/lib/gitlab/bitbucket_import/importers/issues_importer.rb
+++ b/lib/gitlab/bitbucket_import/importers/issues_importer.rb
@@ -33,6 +33,7 @@ module Gitlab
job_waiter
rescue StandardError => e
track_import_failure!(project, exception: e)
+ job_waiter
end
private
diff --git a/lib/gitlab/bitbucket_import/importers/issues_notes_importer.rb b/lib/gitlab/bitbucket_import/importers/issues_notes_importer.rb
index 03dcc645f07..ecc41cc5436 100644
--- a/lib/gitlab/bitbucket_import/importers/issues_notes_importer.rb
+++ b/lib/gitlab/bitbucket_import/importers/issues_notes_importer.rb
@@ -22,6 +22,7 @@ module Gitlab
job_waiter
rescue StandardError => e
track_import_failure!(project, exception: e)
+ job_waiter
end
private
diff --git a/lib/gitlab/bitbucket_import/importers/pull_requests_importer.rb b/lib/gitlab/bitbucket_import/importers/pull_requests_importer.rb
index 1c7ce7f2f3a..eedb89c2d49 100644
--- a/lib/gitlab/bitbucket_import/importers/pull_requests_importer.rb
+++ b/lib/gitlab/bitbucket_import/importers/pull_requests_importer.rb
@@ -26,6 +26,7 @@ module Gitlab
job_waiter
rescue StandardError => e
track_import_failure!(project, exception: e)
+ job_waiter
end
private
diff --git a/lib/gitlab/bitbucket_import/importers/pull_requests_notes_importer.rb b/lib/gitlab/bitbucket_import/importers/pull_requests_notes_importer.rb
index a1b0c2a5afe..1dc3c6fbfc1 100644
--- a/lib/gitlab/bitbucket_import/importers/pull_requests_notes_importer.rb
+++ b/lib/gitlab/bitbucket_import/importers/pull_requests_notes_importer.rb
@@ -22,6 +22,7 @@ module Gitlab
job_waiter
rescue StandardError => e
track_import_failure!(project, exception: e)
+ job_waiter
end
private
diff --git a/lib/gitlab/checks/branch_check.rb b/lib/gitlab/checks/branch_check.rb
index b675eca826a..3bedc483e75 100644
--- a/lib/gitlab/checks/branch_check.rb
+++ b/lib/gitlab/checks/branch_check.rb
@@ -13,7 +13,7 @@ module Gitlab
create_protected_branch: 'You are not allowed to create protected branches on this project.',
invalid_commit_create_protected_branch: 'You can only use an existing protected branch ref as the basis of a new protected branch.',
non_web_create_protected_branch: 'You can only create protected branches using the web interface and API.',
- prohibited_hex_branch_name: 'You cannot create a branch with a 40-character hexadecimal branch name.',
+ prohibited_hex_branch_name: 'You cannot create a branch with a SHA-1 or SHA-256 branch name.',
invalid_branch_name: 'You cannot create a branch with an invalid name.'
}.freeze
@@ -43,7 +43,7 @@ module Gitlab
def prohibited_branch_checks
return if deletion?
- if %r{\A#{Gitlab::Git::Commit::RAW_FULL_SHA_PATTERN}(-/|/|\z)}o.match?(branch_name)
+ if %r{\A#{Gitlab::Git::Commit::RAW_FULL_SHA_PATTERN}}o.match?(branch_name)
raise GitAccess::ForbiddenError, ERROR_MESSAGES[:prohibited_hex_branch_name]
end
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index 6ac37986d5c..2de0a8d7b41 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -241,10 +241,8 @@ module Gitlab
# Based on Jira's project key format
# https://confluence.atlassian.com/adminjiraserver073/changing-the-project-key-format-861253229.html
- # Avoids linking CVE IDs (https://cve.mitre.org/cve/identifiers/syntaxchange.html#new) as Jira issues.
- # CVE IDs use the format of CVE-YYYY-NNNNNNN
def jira_issue_key_regex(expression_escape: '\b')
- /#{expression_escape}(?!CVE-\d+-\d+)[A-Z][A-Z_0-9]+-\d+/
+ /#{expression_escape}([A-Z][A-Z_0-9]+-\d+)/
end
def jira_issue_key_project_key_extraction_regex
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index d6f02d193e3..dfe81524531 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -16451,6 +16451,9 @@ msgstr ""
msgid "Dependencies|There was a problem fetching the licenses for this group."
msgstr ""
+msgid "Dependencies|There was an error fetching the projects for this group. Please try again later."
+msgstr ""
+
msgid "Dependencies|This group exceeds the maximum number of sub-groups of 600. We cannot accurately display a project list at this time. Please access a sub-group dependency list to view this information or see the %{linkStart}dependency list help %{linkEnd} page to learn more."
msgstr ""
@@ -28726,6 +28729,9 @@ msgstr ""
msgid "Load more users"
msgstr ""
+msgid "Load new file"
+msgstr ""
+
msgid "Loading"
msgstr ""
@@ -33591,6 +33597,9 @@ msgstr ""
msgid "Organization|A group is a collection of several projects. If you organize your projects under a group, it works like a folder."
msgstr ""
+msgid "Organization|An error occurred changing your organization URL. Please try again."
+msgstr ""
+
msgid "Organization|An error occurred creating an organization. Please try again."
msgstr ""
@@ -33609,6 +33618,12 @@ msgstr ""
msgid "Organization|An error occurred updating your organization. Please try again."
msgstr ""
+msgid "Organization|Change organization URL"
+msgstr ""
+
+msgid "Organization|Changing an organization's URL can have unintended side effects."
+msgstr ""
+
msgid "Organization|Choose what organization you want to see by default."
msgstr ""
@@ -33657,7 +33672,10 @@ msgstr ""
msgid "Organization|Organization URL is required."
msgstr ""
-msgid "Organization|Organization URL must be a minimum of two characters."
+msgid "Organization|Organization URL is too short (minimum is 2 characters)."
+msgstr ""
+
+msgid "Organization|Organization URL successfully changed."
msgstr ""
msgid "Organization|Organization name"
@@ -33684,6 +33702,9 @@ msgstr ""
msgid "Organization|Organizations"
msgstr ""
+msgid "Organization|Perform advanced options such as deleting the organization."
+msgstr ""
+
msgid "Organization|Public - The organization can be accessed without any authentication."
msgstr ""
@@ -42251,9 +42272,6 @@ msgstr ""
msgid "ScanExecutionPolicy|DAST site profiles"
msgstr ""
-msgid "ScanExecutionPolicy|Execute a YAML code block"
-msgstr ""
-
msgid "ScanExecutionPolicy|If there are any conflicting variables with the local pipeline configuration (Ex, gitlab-ci.yml) then variables defined here will take precedence. %{linkStart}Learn more%{linkEnd}."
msgstr ""
@@ -42278,6 +42296,9 @@ msgstr ""
msgid "ScanExecutionPolicy|Only one variable can be added at a time."
msgstr ""
+msgid "ScanExecutionPolicy|Run CI/CD code"
+msgstr ""
+
msgid "ScanExecutionPolicy|Run a %{scan} scan with the following options:"
msgstr ""
@@ -43167,6 +43188,12 @@ msgstr ""
msgid "SecurityOrchestration|%{cadence} on %{branches}%{branchExceptionsString}"
msgstr ""
+msgid "SecurityOrchestration|%{fileName} loaded succeeded."
+msgstr ""
+
+msgid "SecurityOrchestration|%{fileName} loading failed. Please try again."
+msgstr ""
+
msgid "SecurityOrchestration|%{licenses} and %{lastLicense}"
msgstr ""
@@ -43386,6 +43413,9 @@ msgstr ""
msgid "SecurityOrchestration|License Scan"
msgstr ""
+msgid "SecurityOrchestration|Load CI/CD code from file"
+msgstr ""
+
msgid "SecurityOrchestration|Logic error"
msgstr ""
@@ -43439,6 +43469,9 @@ msgstr ""
msgid "SecurityOrchestration|Override the following project settings:"
msgstr ""
+msgid "SecurityOrchestration|Overwrite the current CI/CD code with the new file's content?"
+msgstr ""
+
msgid "SecurityOrchestration|Policies"
msgstr ""
@@ -57681,6 +57714,9 @@ msgstr ""
msgid "mrWidgetCommitsAdded|The changes were not merged into %{targetBranch}."
msgstr ""
+msgid "mrWidgetDraftCheck|Mark as ready"
+msgstr ""
+
msgid "mrWidgetNothingToMerge|Merge request contains no changes"
msgstr ""
@@ -58562,6 +58598,9 @@ msgstr ""
msgid "the following issues"
msgstr ""
+msgid "the member access level can't be higher than the current user's one"
+msgstr ""
+
msgid "the wiki"
msgstr ""
diff --git a/package.json b/package.json
index e5d76331d42..d4f37deea8a 100644
--- a/package.json
+++ b/package.json
@@ -165,7 +165,7 @@
"marked-bidi": "^1.0.3",
"mathjax": "3",
"mdurl": "^1.0.1",
- "mermaid": "10.6.0",
+ "mermaid": "10.6.1",
"micromatch": "^4.0.5",
"minimatch": "^3.0.4",
"monaco-editor": "^0.30.1",
diff --git a/scripts/review_apps/base-config.yaml b/scripts/review_apps/base-config.yaml
index 414ec77a186..70941b264c5 100644
--- a/scripts/review_apps/base-config.yaml
+++ b/scripts/review_apps/base-config.yaml
@@ -91,10 +91,10 @@ gitlab:
# Based on https://console.cloud.google.com/monitoring/metrics-explorer;duration=P14D?pageState=%7B%22xyChart%22:%7B%22constantLines%22:%5B%5D,%22dataSets%22:%5B%7B%22plotType%22:%22LINE%22,%22targetAxis%22:%22Y1%22,%22timeSeriesFilter%22:%7B%22aggregations%22:%5B%7B%22crossSeriesReducer%22:%22REDUCE_NONE%22,%22groupByFields%22:%5B%5D,%22perSeriesAligner%22:%22ALIGN_RATE%22%7D,%7B%22crossSeriesReducer%22:%22REDUCE_NONE%22,%22groupByFields%22:%5B%5D,%22perSeriesAligner%22:%22ALIGN_MEAN%22%7D%5D,%22apiSource%22:%22DEFAULT_CLOUD%22,%22crossSeriesReducer%22:%22REDUCE_NONE%22,%22filter%22:%22metric.type%3D%5C%22kubernetes.io%2Fcontainer%2Fcpu%2Fcore_usage_time%5C%22%20resource.type%3D%5C%22k8s_container%5C%22%20resource.label.%5C%22container_name%5C%22%3D%5C%22sidekiq%5C%22%22,%22groupByFields%22:%5B%5D,%22minAlignmentPeriod%22:%2260s%22,%22perSeriesAligner%22:%22ALIGN_RATE%22,%22secondaryCrossSeriesReducer%22:%22REDUCE_NONE%22,%22secondaryGroupByFields%22:%5B%5D%7D%7D%5D,%22options%22:%7B%22mode%22:%22STATS%22%7D,%22y1Axis%22:%7B%22label%22:%22%22,%22scale%22:%22LINEAR%22%7D%7D%7D&project=gitlab-review-apps
cpu: 400m
# Based on https://console.cloud.google.com/monitoring/metrics-explorer;duration=P14D?pageState=%7B%22xyChart%22:%7B%22constantLines%22:%5B%5D,%22dataSets%22:%5B%7B%22plotType%22:%22LINE%22,%22targetAxis%22:%22Y1%22,%22timeSeriesFilter%22:%7B%22aggregations%22:%5B%7B%22crossSeriesReducer%22:%22REDUCE_NONE%22,%22groupByFields%22:%5B%5D,%22perSeriesAligner%22:%22ALIGN_MEAN%22%7D%5D,%22apiSource%22:%22DEFAULT_CLOUD%22,%22crossSeriesReducer%22:%22REDUCE_NONE%22,%22filter%22:%22metric.type%3D%5C%22kubernetes.io%2Fcontainer%2Fmemory%2Fused_bytes%5C%22%20resource.type%3D%5C%22k8s_container%5C%22%20resource.label.%5C%22container_name%5C%22%3D%5C%22sidekiq%5C%22%22,%22groupByFields%22:%5B%5D,%22minAlignmentPeriod%22:%2260s%22,%22perSeriesAligner%22:%22ALIGN_MEAN%22%7D%7D%5D,%22options%22:%7B%22mode%22:%22STATS%22%7D,%22y1Axis%22:%7B%22label%22:%22%22,%22scale%22:%22LINEAR%22%7D%7D%7D&project=gitlab-review-apps
- memory: 1300Mi
+ memory: 1500Mi
limits:
cpu: 700m
- memory: 1800Mi
+ memory: 2000Mi
hpa:
cpu:
targetAverageValue: 650m
diff --git a/spec/controllers/projects/pipeline_schedules_controller_spec.rb b/spec/controllers/projects/pipeline_schedules_controller_spec.rb
index cd828c956a0..7cd4f43d4da 100644
--- a/spec/controllers/projects/pipeline_schedules_controller_spec.rb
+++ b/spec/controllers/projects/pipeline_schedules_controller_spec.rb
@@ -65,6 +65,10 @@ RSpec.describe Projects::PipelineSchedulesController, feature_category: :continu
create(:protected_branch, *branch_access_levels, name: ref_name, project: project)
end
+ after do
+ ProtectedBranches::CacheService.new(project).refresh
+ end
+
it { expect { go }.to try(maintainer_accessible, :maintainer).of(project) }
it { expect { go }.to try(developer_accessible, :developer).of(project) }
end
diff --git a/spec/controllers/projects/tags_controller_spec.rb b/spec/controllers/projects/tags_controller_spec.rb
index 3d1f8c12022..cab0778bd13 100644
--- a/spec/controllers/projects/tags_controller_spec.rb
+++ b/spec/controllers/projects/tags_controller_spec.rb
@@ -52,6 +52,18 @@ RSpec.describe Projects::TagsController do
expect(assigns(:releases)).not_to include(invalid_release)
end
+ context 'when releases are private' do
+ before do
+ project.project_feature.update!(releases_access_level: ProjectFeature::PRIVATE)
+ end
+
+ it 'does not contain release data' do
+ subject
+
+ expect(assigns(:releases)).to be_empty
+ end
+ end
+
context '@tag_pipeline_status' do
context 'when no pipelines exist' do
it 'is empty' do
diff --git a/spec/features/projects/pages/user_adds_domain_spec.rb b/spec/features/projects/pages/user_adds_domain_spec.rb
index 14b01cb63d2..04a9f450b52 100644
--- a/spec/features/projects/pages/user_adds_domain_spec.rb
+++ b/spec/features/projects/pages/user_adds_domain_spec.rb
@@ -155,7 +155,6 @@ RSpec.describe 'User adds pages domain', :js, feature_category: :pages do
click_button 'Save Changes'
expect(page).to have_content('Certificate must be a valid PEM certificate')
- expect(page).to have_content('Certificate misses intermediates')
expect(page).to have_content("Key doesn't match the certificate")
end
end
diff --git a/spec/finders/packages/composer/packages_finder_spec.rb b/spec/finders/packages/composer/packages_finder_spec.rb
index d4328827de3..1701243063b 100644
--- a/spec/finders/packages/composer/packages_finder_spec.rb
+++ b/spec/finders/packages/composer/packages_finder_spec.rb
@@ -1,18 +1,19 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe ::Packages::Composer::PackagesFinder do
+RSpec.describe ::Packages::Composer::PackagesFinder, feature_category: :package_registry do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
- let(:params) { {} }
+ let(:params) { { package_type: :composer } }
describe '#execute' do
let_it_be(:composer_package) { create(:composer_package, project: project) }
let_it_be(:composer_package2) { create(:composer_package, project: project) }
let_it_be(:error_package) { create(:composer_package, :error, project: project) }
let_it_be(:composer_package3) { create(:composer_package) }
+ let_it_be(:nuget_package) { create(:nuget_package, project: project) }
subject { described_class.new(user, group, params).execute }
@@ -21,5 +22,15 @@ RSpec.describe ::Packages::Composer::PackagesFinder do
end
it { is_expected.to match_array([composer_package, composer_package2]) }
+
+ context 'when disabling the package registry for the project' do
+ let(:params) { super().merge(with_package_registry_enabled: true) }
+
+ before do
+ project.update!(package_registry_access_level: 'disabled', packages_enabled: false)
+ end
+
+ it { is_expected.to be_empty }
+ end
end
end
diff --git a/spec/finders/packages/group_packages_finder_spec.rb b/spec/finders/packages/group_packages_finder_spec.rb
index a2698bc0153..d270d026da6 100644
--- a/spec/finders/packages/group_packages_finder_spec.rb
+++ b/spec/finders/packages/group_packages_finder_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::GroupPackagesFinder do
+RSpec.describe Packages::GroupPackagesFinder, feature_category: :package_registry do
using RSpec::Parameterized::TableSyntax
let_it_be(:user) { create(:user) }
@@ -25,6 +25,16 @@ RSpec.describe Packages::GroupPackagesFinder do
it { is_expected.to match_array([send("package_#{package_type}")]) }
end
+ shared_examples 'disabling package registry for project' do
+ let(:params) { super().merge(with_package_registry_enabled: true) }
+
+ before do
+ project.update!(package_registry_access_level: 'disabled', packages_enabled: false)
+ end
+
+ it { is_expected.to match_array(packages_returned) }
+ end
+
def self.package_types
@package_types ||= Packages::Package.package_types.keys
end
@@ -117,6 +127,10 @@ RSpec.describe Packages::GroupPackagesFinder do
let(:user) { deploy_token_for_group }
it { is_expected.to match_array([package1, package2, package4]) }
+
+ it_behaves_like 'disabling package registry for project' do
+ let(:packages_returned) { [package4] }
+ end
end
context 'project deploy token' do
@@ -126,6 +140,11 @@ RSpec.describe Packages::GroupPackagesFinder do
let(:user) { deploy_token_for_project }
it { is_expected.to match_array([package4]) }
+
+ it_behaves_like 'disabling package registry for project' do
+ let(:project) { subproject }
+ let(:packages_returned) { [] }
+ end
end
end
@@ -200,6 +219,9 @@ RSpec.describe Packages::GroupPackagesFinder do
it_behaves_like 'concerning versionless param'
it_behaves_like 'concerning package statuses'
+ it_behaves_like 'disabling package registry for project' do
+ let(:packages_returned) { [] }
+ end
end
context 'group has package of all types' do
diff --git a/spec/frontend/blob_edit/edit_blob_spec.js b/spec/frontend/blob_edit/edit_blob_spec.js
index e58ad4040a9..31be1a86de4 100644
--- a/spec/frontend/blob_edit/edit_blob_spec.js
+++ b/spec/frontend/blob_edit/edit_blob_spec.js
@@ -6,6 +6,7 @@ import EditBlob from '~/blob_edit/edit_blob';
import { SourceEditorExtension } from '~/editor/extensions/source_editor_extension_base';
import { FileTemplateExtension } from '~/editor/extensions/source_editor_file_template_ext';
import { EditorMarkdownExtension } from '~/editor/extensions/source_editor_markdown_ext';
+import { SecurityPolicySchemaExtension } from '~/editor/extensions/source_editor_security_policy_schema_ext';
import { EditorMarkdownPreviewExtension } from '~/editor/extensions/source_editor_markdown_livepreview_ext';
import { ToolbarExtension } from '~/editor/extensions/source_editor_toolbar_ext';
import SourceEditor from '~/editor/source_editor';
@@ -17,6 +18,7 @@ jest.mock('~/editor/extensions/source_editor_file_template_ext');
jest.mock('~/editor/extensions/source_editor_markdown_ext');
jest.mock('~/editor/extensions/source_editor_markdown_livepreview_ext');
jest.mock('~/editor/extensions/source_editor_toolbar_ext');
+jest.mock('~/editor/extensions/source_editor_security_policy_schema_ext');
const PREVIEW_MARKDOWN_PATH = '/foo/bar/preview_markdown';
const defaultExtensions = [
@@ -67,16 +69,18 @@ describe('Blob Editing', () => {
resetHTMLFixture();
});
- const editorInst = (isMarkdown) => {
+ const editorInst = ({ isMarkdown = false, isSecurityPolicy = false }) => {
blobInstance = new EditBlob({
isMarkdown,
previewMarkdownPath: PREVIEW_MARKDOWN_PATH,
+ filePath: isSecurityPolicy ? '.gitlab/security-policies/policy.yml' : '',
+ projectPath: 'path/to/project',
});
return blobInstance;
};
- const initEditor = async (isMarkdown = false) => {
- editorInst(isMarkdown);
+ const initEditor = async ({ isMarkdown = false, isSecurityPolicy = false } = {}) => {
+ editorInst({ isMarkdown, isSecurityPolicy });
await waitForPromises();
};
@@ -93,13 +97,13 @@ describe('Blob Editing', () => {
});
it('loads MarkdownExtension only for the markdown files', async () => {
- await initEditor(true);
+ await initEditor({ isMarkdown: true });
expect(useMock).toHaveBeenCalledTimes(2);
expect(useMock.mock.calls[1]).toEqual([markdownExtensions]);
});
it('correctly handles switching from markdown and un-uses markdown extensions', async () => {
- await initEditor(true);
+ await initEditor({ isMarkdown: true });
expect(unuseMock).not.toHaveBeenCalled();
await emitter.fire({ newLanguage: 'plaintext', oldLanguage: 'markdown' });
expect(unuseMock).toHaveBeenCalledWith(markdownExtensions);
@@ -115,6 +119,19 @@ describe('Blob Editing', () => {
});
});
+ describe('Security Policy Yaml', () => {
+ it('does not load SecurityPolicySchemaExtension by default', async () => {
+ await initEditor();
+ expect(SecurityPolicySchemaExtension).not.toHaveBeenCalled();
+ });
+
+ it('loads SecurityPolicySchemaExtension only for the security policies yml', async () => {
+ await initEditor({ isSecurityPolicy: true });
+ expect(useMock).toHaveBeenCalledTimes(2);
+ expect(useMock.mock.calls[1]).toEqual([[{ definition: SecurityPolicySchemaExtension }]]);
+ });
+ });
+
describe('correctly handles toggling the live-preview panel for different file types', () => {
it.each`
desc | isMarkdown | isPreviewOpened | tabToClick | shouldOpenPreview | shouldClosePreview | expectedDesc
@@ -142,7 +159,7 @@ describe('Blob Editing', () => {
},
},
});
- await initEditor(isMarkdown);
+ await initEditor({ isMarkdown });
blobInstance.markdownLivePreviewOpened = isPreviewOpened;
const elToClick = document.querySelector(`a[href='${tabToClick}']`);
elToClick.dispatchEvent(new Event('click'));
diff --git a/spec/frontend/editor/source_editor_security_policy_schema_ext_spec.js b/spec/frontend/editor/source_editor_security_policy_schema_ext_spec.js
new file mode 100644
index 00000000000..96c876b27c9
--- /dev/null
+++ b/spec/frontend/editor/source_editor_security_policy_schema_ext_spec.js
@@ -0,0 +1,181 @@
+import MockAdapter from 'axios-mock-adapter';
+import { registerSchema } from '~/ide/utils';
+import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import { TEST_HOST } from 'helpers/test_constants';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import {
+ getSecurityPolicyListUrl,
+ getSecurityPolicySchemaUrl,
+ getSinglePolicySchema,
+ SecurityPolicySchemaExtension,
+} from '~/editor/extensions/source_editor_security_policy_schema_ext';
+import SourceEditor from '~/editor/source_editor';
+
+jest.mock('~/ide/utils');
+
+const mockNamespacePath = 'mock-namespace';
+
+const mockSchema = {
+ $id: 1,
+ title: 'mockSchema',
+ description: 'mockDescriptions',
+ type: 'Object',
+ properties: {
+ scan_execution_policy: { items: { properties: { foo: 'bar' } } },
+ scan_result_policy: { items: { properties: { fizz: 'buzz' } } },
+ },
+};
+
+const createMockOutput = (policyType) => ({
+ $id: mockSchema.$id,
+ title: mockSchema.title,
+ description: mockSchema.description,
+ type: mockSchema.type,
+ properties: {
+ type: {
+ type: 'string',
+ description: 'Specifies the type of policy to be enforced.',
+ enum: policyType,
+ },
+ ...mockSchema.properties[policyType].items.properties,
+ },
+});
+
+describe('getSecurityPolicyListUrl', () => {
+ it.each`
+ input | output
+ ${{ namespacePath: '' }} | ${`${TEST_HOST}/groups/-/security/policies`}
+ ${{ namespacePath: 'test', namespaceType: 'group' }} | ${`${TEST_HOST}/groups/test/-/security/policies`}
+ ${{ namespacePath: '', namespaceType: 'project' }} | ${`${TEST_HOST}/-/security/policies`}
+ ${{ namespacePath: 'test', namespaceType: 'project' }} | ${`${TEST_HOST}/test/-/security/policies`}
+ ${{ namespacePath: undefined, namespaceType: 'project' }} | ${`${TEST_HOST}/-/security/policies`}
+ ${{ namespacePath: undefined, namespaceType: 'group' }} | ${`${TEST_HOST}/groups/-/security/policies`}
+ ${{ namespacePath: null, namespaceType: 'project' }} | ${`${TEST_HOST}/-/security/policies`}
+ ${{ namespacePath: null, namespaceType: 'group' }} | ${`${TEST_HOST}/groups/-/security/policies`}
+ `('returns `$output` when passed `$input`', ({ input, output }) => {
+ expect(getSecurityPolicyListUrl(input)).toBe(output);
+ });
+});
+
+describe('getSecurityPolicySchemaUrl', () => {
+ it.each`
+ namespacePath | namespaceType | output
+ ${'test'} | ${'project'} | ${`${TEST_HOST}/test/-/security/policies/schema`}
+ ${'test'} | ${'group'} | ${`${TEST_HOST}/groups/test/-/security/policies/schema`}
+ `(
+ 'returns $output when passed $namespacePath and $namespaceType',
+ ({ namespacePath, namespaceType, output }) => {
+ expect(getSecurityPolicySchemaUrl({ namespacePath, namespaceType })).toBe(output);
+ },
+ );
+});
+
+describe('getSinglePolicySchema', () => {
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it.each`
+ policyType
+ ${'scan_execution_policy'}
+ ${'scan_result_policy'}
+ `('returns the appropriate schema on request success for $policyType', async ({ policyType }) => {
+ mock.onGet().reply(HTTP_STATUS_OK, mockSchema);
+
+ await expect(
+ getSinglePolicySchema({
+ namespacePath: mockNamespacePath,
+ namespaceType: 'project',
+ policyType,
+ }),
+ ).resolves.toStrictEqual(createMockOutput(policyType));
+ });
+
+ it('returns an empty schema on request failure', async () => {
+ await expect(
+ getSinglePolicySchema({
+ namespacePath: mockNamespacePath,
+ namespaceType: 'project',
+ policyType: 'scan_execution_policy',
+ }),
+ ).resolves.toStrictEqual({});
+ });
+
+ it('returns an empty schema on non-existing policy type', async () => {
+ await expect(
+ getSinglePolicySchema({
+ namespacePath: mockNamespacePath,
+ namespaceType: 'project',
+ policyType: 'non_existent_policy',
+ }),
+ ).resolves.toStrictEqual({});
+ });
+});
+
+describe('SecurityPolicySchemaExtension', () => {
+ let mock;
+ let editor;
+ let instance;
+ let editorEl;
+
+ const createMockEditor = ({ blobPath = '.gitlab/security-policies/policy.yml' } = {}) => {
+ setHTMLFixture('<div id="editor"></div>');
+ editorEl = document.getElementById('editor');
+ editor = new SourceEditor();
+ instance = editor.createInstance({ el: editorEl, blobPath, blobContent: '' });
+ instance.use({ definition: SecurityPolicySchemaExtension });
+ };
+
+ beforeEach(() => {
+ createMockEditor();
+ mock = new MockAdapter(axios);
+ mock.onGet().reply(HTTP_STATUS_OK, mockSchema);
+ });
+
+ afterEach(() => {
+ instance.dispose();
+ editorEl.remove();
+ resetHTMLFixture();
+ mock.restore();
+ });
+
+ describe('registerSecurityPolicyEditorSchema', () => {
+ describe('register validations options with monaco for yaml language', () => {
+ it('registers the schema', async () => {
+ const policyType = 'scan_execution_policy';
+ await instance.registerSecurityPolicyEditorSchema({
+ namespacePath: mockNamespacePath,
+ namespaceType: 'project',
+ policyType,
+ });
+
+ expect(registerSchema).toHaveBeenCalledTimes(1);
+ expect(registerSchema).toHaveBeenCalledWith({
+ uri: `${TEST_HOST}/${mockNamespacePath}/-/security/policies/schema`,
+ schema: createMockOutput(policyType),
+ fileMatch: ['policy.yml'],
+ });
+ });
+ });
+ });
+
+ describe('registerSecurityPolicySchema', () => {
+ describe('register validations options with monaco for yaml language', () => {
+ it('registers the schema', async () => {
+ await instance.registerSecurityPolicySchema(mockNamespacePath);
+ expect(registerSchema).toHaveBeenCalledTimes(1);
+ expect(registerSchema).toHaveBeenCalledWith({
+ uri: `${TEST_HOST}/${mockNamespacePath}/-/security/policies/schema`,
+ fileMatch: ['policy.yml'],
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/issues/show/components/header_actions_spec.js b/spec/frontend/issues/show/components/header_actions_spec.js
index d0c2a1a5f1b..33fd9d39feb 100644
--- a/spec/frontend/issues/show/components/header_actions_spec.js
+++ b/spec/frontend/issues/show/components/header_actions_spec.js
@@ -6,13 +6,13 @@ import {
GlModal,
GlButton,
} from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import VueApollo from 'vue-apollo';
import { stubComponent } from 'helpers/stub_component';
import waitForPromises from 'helpers/wait_for_promises';
import { mockTracking } from 'helpers/tracking_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createAlert, VARIANT_SUCCESS } from '~/alert';
import {
STATUS_CLOSED,
@@ -132,11 +132,11 @@ describe('HeaderActions component', () => {
const findDesktopDropdownItems = () =>
findDesktopDropdown().findAllComponents(GlDisclosureDropdownItem);
const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector);
- const findReportAbuseButton = () => wrapper.find(`[data-testid="report-abuse-item"]`);
- const findNotificationWidget = () => wrapper.find(`[data-testid="notification-toggle"]`);
- const findLockIssueWidget = () => wrapper.find(`[data-testid="lock-issue-toggle"]`);
- const findCopyRefenceDropdownItem = () => wrapper.find(`[data-testid="copy-reference"]`);
- const findCopyEmailItem = () => wrapper.find(`[data-testid="copy-email"]`);
+ const findReportAbuseButton = () => wrapper.findByTestId('report-abuse-item');
+ const findNotificationWidget = () => wrapper.findByTestId('notification-toggle');
+ const findLockIssueWidget = () => wrapper.findByTestId('lock-issue-toggle');
+ const findCopyRefenceDropdownItem = () => wrapper.findByTestId('copy-reference');
+ const findCopyEmailItem = () => wrapper.findByTestId('copy-email');
const findModal = () => wrapper.findComponent(GlModal);
@@ -176,7 +176,7 @@ describe('HeaderActions component', () => {
window.gon.current_user_id = 1;
}
- return shallowMount(HeaderActions, {
+ return shallowMountExtended(HeaderActions, {
apolloProvider: createMockApollo(handlers),
store,
provide: {
@@ -625,6 +625,10 @@ describe('HeaderActions component', () => {
expect(toast).toHaveBeenCalledWith('Reference copied');
});
+
+ it('contains copy reference class', () => {
+ expect(findCopyRefenceDropdownItem().classes()).toContain('js-copy-reference');
+ });
});
});
diff --git a/spec/frontend/organizations/settings/general/components/advanced_settings_spec.js b/spec/frontend/organizations/settings/general/components/advanced_settings_spec.js
new file mode 100644
index 00000000000..34793200b0d
--- /dev/null
+++ b/spec/frontend/organizations/settings/general/components/advanced_settings_spec.js
@@ -0,0 +1,25 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import AdvancedSettings from '~/organizations/settings/general/components/advanced_settings.vue';
+import ChangeUrl from '~/organizations/settings/general/components/change_url.vue';
+import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
+
+describe('AdvancedSettings', () => {
+ let wrapper;
+ const createComponent = () => {
+ wrapper = shallowMountExtended(AdvancedSettings);
+ };
+
+ const findSettingsBlock = () => wrapper.findComponent(SettingsBlock);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders settings block', () => {
+ expect(findSettingsBlock().exists()).toBe(true);
+ });
+
+ it('renders `ChangeUrl` component', () => {
+ expect(findSettingsBlock().findComponent(ChangeUrl).exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/organizations/settings/general/components/app_spec.js b/spec/frontend/organizations/settings/general/components/app_spec.js
index 6d75f8a9949..e954b927715 100644
--- a/spec/frontend/organizations/settings/general/components/app_spec.js
+++ b/spec/frontend/organizations/settings/general/components/app_spec.js
@@ -1,8 +1,9 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import OrganizationSettings from '~/organizations/settings/general/components/organization_settings.vue';
+import AdvancedSettings from '~/organizations/settings/general/components/advanced_settings.vue';
import App from '~/organizations/settings/general/components/app.vue';
-describe('OrganizationSettings', () => {
+describe('OrganizationSettingsGeneralApp', () => {
let wrapper;
const createComponent = () => {
@@ -16,4 +17,8 @@ describe('OrganizationSettings', () => {
it('renders `Organization settings` section', () => {
expect(wrapper.findComponent(OrganizationSettings).exists()).toBe(true);
});
+
+ it('renders `Advanced` section', () => {
+ expect(wrapper.findComponent(AdvancedSettings).exists()).toBe(true);
+ });
});
diff --git a/spec/frontend/organizations/settings/general/components/change_url_spec.js b/spec/frontend/organizations/settings/general/components/change_url_spec.js
new file mode 100644
index 00000000000..65289a83b7c
--- /dev/null
+++ b/spec/frontend/organizations/settings/general/components/change_url_spec.js
@@ -0,0 +1,163 @@
+import { GlButton, GlForm } from '@gitlab/ui';
+import VueApollo from 'vue-apollo';
+import Vue, { nextTick } from 'vue';
+
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import ChangeUrl from '~/organizations/settings/general/components/change_url.vue';
+import resolvers from '~/organizations/shared/graphql/resolvers';
+import { updateOrganizationResponse } from '~/organizations/mock_data';
+import { createAlert } from '~/alert';
+import { visitUrlWithAlerts } from '~/lib/utils/url_utility';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+
+jest.mock('~/alert');
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ visitUrlWithAlerts: jest.fn(),
+}));
+jest.useFakeTimers();
+
+Vue.use(VueApollo);
+
+describe('ChangeUrl', () => {
+ let wrapper;
+ let mockApollo;
+
+ const defaultProvide = {
+ organization: {
+ id: 1,
+ name: 'GitLab',
+ path: 'foo-bar',
+ },
+ organizationsPath: '/-/organizations',
+ rootUrl: 'http://127.0.0.1:3000/',
+ };
+
+ const createComponent = ({ mockResolvers = resolvers } = {}) => {
+ mockApollo = createMockApollo([], mockResolvers);
+
+ wrapper = mountExtended(ChangeUrl, {
+ attachTo: document.body,
+ provide: defaultProvide,
+ apolloProvider: mockApollo,
+ });
+ };
+
+ const findSubmitButton = () => wrapper.findComponent(GlButton);
+ const findOrganizationUrlField = () => wrapper.findByLabelText('Organization URL');
+ const submitForm = async () => {
+ await wrapper.findComponent(GlForm).trigger('submit');
+ await nextTick();
+ };
+
+ afterEach(() => {
+ mockApollo = null;
+ });
+
+ it('renders `Organization URL` field', () => {
+ createComponent();
+
+ expect(findOrganizationUrlField().exists()).toBe(true);
+ });
+
+ it('disables submit button until `Organization URL` field is changed', async () => {
+ createComponent();
+
+ expect(findSubmitButton().props('disabled')).toBe(true);
+
+ await findOrganizationUrlField().setValue('foo-bar-baz');
+
+ expect(findSubmitButton().props('disabled')).toBe(false);
+ });
+
+ describe('when form is submitted', () => {
+ it('requires `Organization URL` field', async () => {
+ createComponent();
+
+ await findOrganizationUrlField().setValue('');
+ await submitForm();
+
+ expect(wrapper.findByText('Organization URL is required.').exists()).toBe(true);
+ });
+
+ it('requires `Organization URL` field to be a minimum of two characters', async () => {
+ createComponent();
+
+ await findOrganizationUrlField().setValue('f');
+ await submitForm();
+
+ expect(
+ wrapper.findByText('Organization URL is too short (minimum is 2 characters).').exists(),
+ ).toBe(true);
+ });
+
+ describe('when API is loading', () => {
+ beforeEach(async () => {
+ const mockResolvers = {
+ Mutation: {
+ updateOrganization: jest.fn().mockReturnValueOnce(new Promise(() => {})),
+ },
+ };
+
+ createComponent({ mockResolvers });
+
+ await findOrganizationUrlField().setValue('foo-bar-baz');
+ await submitForm();
+ });
+
+ it('shows submit button as loading', () => {
+ expect(findSubmitButton().props('loading')).toBe(true);
+ });
+ });
+
+ describe('when API request is successful', () => {
+ beforeEach(async () => {
+ createComponent();
+ await findOrganizationUrlField().setValue('foo-bar-baz');
+ await submitForm();
+ jest.runAllTimers();
+ await waitForPromises();
+ });
+
+ it('redirects user to new organization settings page and shows success alert', () => {
+ expect(visitUrlWithAlerts).toHaveBeenCalledWith(
+ `${updateOrganizationResponse.organization.webUrl}/settings/general`,
+ [
+ {
+ id: 'organization-url-successfully-changed',
+ message: 'Organization URL successfully changed.',
+ variant: 'info',
+ },
+ ],
+ );
+ });
+ });
+
+ describe('when API request is not successful', () => {
+ const error = new Error();
+
+ beforeEach(async () => {
+ const mockResolvers = {
+ Mutation: {
+ updateOrganization: jest.fn().mockRejectedValueOnce(error),
+ },
+ };
+
+ createComponent({ mockResolvers });
+ await findOrganizationUrlField().setValue('foo-bar-baz');
+ await submitForm();
+ jest.runAllTimers();
+ await waitForPromises();
+ });
+
+ it('displays error alert', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'An error occurred changing your organization URL. Please try again.',
+ error,
+ captureError: true,
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/organizations/shared/components/new_edit_form_spec.js b/spec/frontend/organizations/shared/components/new_edit_form_spec.js
index 93f022a3259..1fcfc20bf1a 100644
--- a/spec/frontend/organizations/shared/components/new_edit_form_spec.js
+++ b/spec/frontend/organizations/shared/components/new_edit_form_spec.js
@@ -1,6 +1,8 @@
-import { GlButton, GlInputGroupText, GlTruncate } from '@gitlab/ui';
+import { GlButton } from '@gitlab/ui';
+import { nextTick } from 'vue';
import NewEditForm from '~/organizations/shared/components/new_edit_form.vue';
+import OrganizationUrlField from '~/organizations/shared/components/organization_url_field.vue';
import { FORM_FIELD_NAME, FORM_FIELD_ID, FORM_FIELD_PATH } from '~/organizations/shared/constants';
import { mountExtended } from 'helpers/vue_test_utils_helper';
@@ -29,7 +31,12 @@ describe('NewEditForm', () => {
const findNameField = () => wrapper.findByLabelText('Organization name');
const findIdField = () => wrapper.findByLabelText('Organization ID');
- const findUrlField = () => wrapper.findByLabelText('Organization URL');
+ const findUrlField = () => wrapper.findComponent(OrganizationUrlField);
+
+ const setUrlFieldValue = async (value) => {
+ findUrlField().vm.$emit('input', value);
+ await nextTick();
+ };
const submitForm = async () => {
await wrapper.findByRole('button', { name: 'Create organization' }).trigger('click');
};
@@ -43,20 +50,17 @@ describe('NewEditForm', () => {
it('renders `Organization URL` field', () => {
createComponent();
- expect(wrapper.findComponent(GlInputGroupText).findComponent(GlTruncate).props('text')).toBe(
- 'http://127.0.0.1:3000/-/organizations/',
- );
expect(findUrlField().exists()).toBe(true);
});
it('requires `Organization URL` field to be a minimum of two characters', async () => {
createComponent();
- await findUrlField().setValue('f');
+ await setUrlFieldValue('f');
await submitForm();
expect(
- wrapper.findByText('Organization URL must be a minimum of two characters.').exists(),
+ wrapper.findByText('Organization URL is too short (minimum is 2 characters).').exists(),
).toBe(true);
});
@@ -89,7 +93,7 @@ describe('NewEditForm', () => {
it('sets initial values for fields', () => {
expect(findNameField().element.value).toBe('Foo bar');
expect(findIdField().element.value).toBe('1');
- expect(findUrlField().element.value).toBe('foo-bar');
+ expect(findUrlField().props('value')).toBe('foo-bar');
});
});
@@ -116,7 +120,7 @@ describe('NewEditForm', () => {
createComponent();
await findNameField().setValue('Foo bar');
- await findUrlField().setValue('foo-bar');
+ await setUrlFieldValue('foo-bar');
await submitForm();
});
@@ -134,7 +138,7 @@ describe('NewEditForm', () => {
});
it('sets `Organization URL` when typing in `Organization name`', () => {
- expect(findUrlField().element.value).toBe('foo-bar');
+ expect(findUrlField().props('value')).toBe('foo-bar');
});
});
@@ -142,13 +146,13 @@ describe('NewEditForm', () => {
beforeEach(async () => {
createComponent();
- await findUrlField().setValue('foo-bar-baz');
+ await setUrlFieldValue('foo-bar-baz');
await findNameField().setValue('Foo bar');
await submitForm();
});
it('does not modify `Organization URL` when typing in `Organization name`', () => {
- expect(findUrlField().element.value).toBe('foo-bar-baz');
+ expect(findUrlField().props('value')).toBe('foo-bar-baz');
});
});
diff --git a/spec/frontend/organizations/shared/components/organization_url_field_spec.js b/spec/frontend/organizations/shared/components/organization_url_field_spec.js
new file mode 100644
index 00000000000..d854134e596
--- /dev/null
+++ b/spec/frontend/organizations/shared/components/organization_url_field_spec.js
@@ -0,0 +1,66 @@
+import { GlFormInputGroup, GlInputGroupText, GlTruncate, GlFormInput } from '@gitlab/ui';
+
+import OrganizedUrlField from '~/organizations/shared/components/organization_url_field.vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+
+describe('OrganizationUrlField', () => {
+ let wrapper;
+
+ const defaultProvide = {
+ organizationsPath: '/-/organizations',
+ rootUrl: 'http://127.0.0.1:3000/',
+ };
+
+ const defaultPropsData = {
+ id: 'organization-url',
+ value: 'foo-bar',
+ validation: {
+ invalidFeedback: 'Invalid',
+ state: false,
+ },
+ };
+
+ const createComponent = ({ propsData = {} } = {}) => {
+ wrapper = mountExtended(OrganizedUrlField, {
+ attachTo: document.body,
+ provide: defaultProvide,
+ propsData: {
+ ...defaultPropsData,
+ ...propsData,
+ },
+ });
+ };
+
+ const findInputGroup = () => wrapper.findComponent(GlFormInputGroup);
+ const findInput = () => findInputGroup().findComponent(GlFormInput);
+
+ it('renders organization url field with correct props', () => {
+ createComponent();
+
+ expect(
+ findInputGroup().findComponent(GlInputGroupText).findComponent(GlTruncate).props('text'),
+ ).toBe('http://127.0.0.1:3000/-/organizations/');
+ expect(findInput().attributes('id')).toBe(defaultPropsData.id);
+ expect(findInput().vm.$attrs).toMatchObject({
+ value: defaultPropsData.value,
+ invalidFeedback: defaultPropsData.validation.invalidFeedback,
+ state: defaultPropsData.validation.state,
+ });
+ });
+
+ it('emits `input` event', () => {
+ createComponent();
+
+ findInput().vm.$emit('input', 'foo');
+
+ expect(wrapper.emitted('input')).toEqual([['foo']]);
+ });
+
+ it('emits `blur` event', () => {
+ createComponent();
+
+ findInput().vm.$emit('blur', true);
+
+ expect(wrapper.emitted('blur')).toEqual([[true]]);
+ });
+});
diff --git a/spec/frontend/vue_merge_request_widget/components/checks/draft_spec.js b/spec/frontend/vue_merge_request_widget/components/checks/draft_spec.js
new file mode 100644
index 00000000000..cc605c8c83d
--- /dev/null
+++ b/spec/frontend/vue_merge_request_widget/components/checks/draft_spec.js
@@ -0,0 +1,196 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+
+import getStateQueryResponse from 'test_fixtures/graphql/merge_requests/get_state.query.graphql.json';
+
+import { createAlert } from '~/alert';
+
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+
+import MergeRequest from '~/merge_request';
+
+import DraftCheck from '~/vue_merge_request_widget/components/checks/draft.vue';
+import {
+ DRAFT_CHECK_READY,
+ DRAFT_CHECK_ERROR,
+} from '~/vue_merge_request_widget/components/checks/i18n';
+import { FAILURE_REASONS } from '~/vue_merge_request_widget/components/checks/message.vue';
+
+import draftQuery from '~/vue_merge_request_widget/queries/states/draft.query.graphql';
+import getStateQuery from '~/vue_merge_request_widget/queries/get_state.query.graphql';
+import removeDraftMutation from '~/vue_merge_request_widget/queries/toggle_draft.mutation.graphql';
+
+Vue.use(VueApollo);
+
+const TEST_PROJECT_ID = getStateQueryResponse.data.project.id;
+const TEST_MR_ID = getStateQueryResponse.data.project.mergeRequest.id;
+const TEST_MR_IID = '23';
+const TEST_MR_TITLE = 'Test MR Title';
+const TEST_PROJECT_PATH = 'lorem/ipsum';
+
+jest.mock('~/alert');
+jest.mock('~/merge_request', () => ({ toggleDraftStatus: jest.fn() }));
+
+describe('~/vue_merge_request_widget/components/checks/draft.vue', () => {
+ let wrapper;
+ let apolloProvider;
+
+ let draftQuerySpy;
+ let removeDraftMutationSpy;
+
+ const findMarkReadyButton = () => wrapper.findByTestId('mark-as-ready-button');
+
+ const createDraftQueryResponse = (canUpdateMergeRequest) => ({
+ data: {
+ project: {
+ __typename: 'Project',
+ id: TEST_PROJECT_ID,
+ mergeRequest: {
+ __typename: 'MergeRequest',
+ id: TEST_MR_ID,
+ draft: true,
+ title: TEST_MR_TITLE,
+ mergeableDiscussionsState: false,
+ userPermissions: {
+ updateMergeRequest: canUpdateMergeRequest,
+ },
+ },
+ },
+ },
+ });
+ const createRemoveDraftMutationResponse = () => ({
+ data: {
+ mergeRequestSetDraft: {
+ __typename: 'MergeRequestSetWipPayload',
+ errors: [],
+ mergeRequest: {
+ __typename: 'MergeRequest',
+ id: TEST_MR_ID,
+ title: TEST_MR_TITLE,
+ draft: false,
+ mergeableDiscussionsState: true,
+ },
+ },
+ },
+ });
+
+ const createComponent = async () => {
+ wrapper = mountExtended(DraftCheck, {
+ apolloProvider,
+ propsData: {
+ mr: {
+ issuableId: TEST_MR_ID,
+ title: TEST_MR_TITLE,
+ iid: TEST_MR_IID,
+ targetProjectFullPath: TEST_PROJECT_PATH,
+ },
+ check: {
+ identifier: 'draft_status',
+ status: 'FAILED',
+ },
+ },
+ });
+
+ await waitForPromises();
+
+ // why: draft.vue has some coupling that this query has been read before
+ // for some reason this has to happen **after** the component has mounted
+ // or apollo throws errors.
+ apolloProvider.defaultClient.cache.writeQuery({
+ query: getStateQuery,
+ variables: {
+ projectPath: TEST_PROJECT_PATH,
+ iid: TEST_MR_IID,
+ },
+ data: getStateQueryResponse.data,
+ });
+ };
+
+ beforeEach(() => {
+ draftQuerySpy = jest.fn().mockResolvedValue(createDraftQueryResponse(true));
+ removeDraftMutationSpy = jest.fn().mockResolvedValue(createRemoveDraftMutationResponse());
+
+ apolloProvider = createMockApollo([
+ [draftQuery, draftQuerySpy],
+ [removeDraftMutation, removeDraftMutationSpy],
+ ]);
+ });
+
+ describe('when user can update MR', () => {
+ beforeEach(async () => {
+ await createComponent();
+ });
+
+ it('renders text', () => {
+ const message = wrapper.text();
+ expect(message).toContain(FAILURE_REASONS.draft_status);
+ });
+
+ it('renders mark ready button', () => {
+ expect(findMarkReadyButton().text()).toBe(DRAFT_CHECK_READY);
+ });
+
+ it('does not call remove draft mutation', () => {
+ expect(removeDraftMutationSpy).not.toHaveBeenCalled();
+ });
+
+ describe('when mark ready button is clicked', () => {
+ beforeEach(async () => {
+ findMarkReadyButton().vm.$emit('click');
+
+ await waitForPromises();
+ });
+
+ it('calls mutation spy', () => {
+ expect(removeDraftMutationSpy).toHaveBeenCalledWith({
+ draft: false,
+ iid: TEST_MR_IID,
+ projectPath: TEST_PROJECT_PATH,
+ });
+ });
+
+ it('does not create alert', () => {
+ expect(createAlert).not.toHaveBeenCalled();
+ });
+
+ it('calls toggleDraftStatus', () => {
+ expect(MergeRequest.toggleDraftStatus).toHaveBeenCalledWith(TEST_MR_TITLE, true);
+ });
+ });
+
+ describe('when mutation fails and ready button is clicked', () => {
+ beforeEach(async () => {
+ removeDraftMutationSpy.mockRejectedValue(new Error('TEST FAIL'));
+ findMarkReadyButton().vm.$emit('click');
+
+ await waitForPromises();
+ });
+
+ it('creates alert', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: DRAFT_CHECK_ERROR,
+ });
+ });
+
+ it('does not call toggleDraftStatus', () => {
+ expect(MergeRequest.toggleDraftStatus).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('when user cannot update MR', () => {
+ beforeEach(async () => {
+ draftQuerySpy.mockResolvedValue(createDraftQueryResponse(false));
+
+ createComponent();
+
+ await waitForPromises();
+ });
+
+ it('does not render mark ready button', () => {
+ expect(findMarkReadyButton().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/work_in_progress_spec.js b/spec/frontend/vue_merge_request_widget/components/states/work_in_progress_spec.js
index f46829539a8..509b5adb3b2 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/work_in_progress_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/work_in_progress_spec.js
@@ -42,6 +42,9 @@ describe('~/vue_merge_request_widget/components/states/work_in_progress.vue', ()
mergeRequest: {
__typename: 'MergeRequest',
id: TEST_MR_ID,
+ draft: true,
+ title: TEST_MR_TITLE,
+ mergeableDiscussionsState: false,
userPermissions: {
updateMergeRequest: canUpdateMergeRequest,
},
diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb
index 40879568343..2856b8004cf 100644
--- a/spec/helpers/groups_helper_spec.rb
+++ b/spec/helpers/groups_helper_spec.rb
@@ -604,4 +604,67 @@ RSpec.describe GroupsHelper, feature_category: :groups_and_projects do
end
end
end
+
+ describe '#access_level_roles_user_can_assign' do
+ subject { helper.access_level_roles_user_can_assign(group) }
+
+ let_it_be(:group) { create(:group) }
+ let_it_be_with_reload(:user) { create(:user) }
+
+ context 'when user is provided' do
+ before do
+ allow(helper).to receive(:current_user).and_return(user)
+ end
+
+ context 'when a user is a group member' do
+ before do
+ group.add_developer(user)
+ end
+
+ it 'returns only the roles the provided user can assign' do
+ expect(subject).to eq(
+ {
+ 'Guest' => 10,
+ 'Reporter' => 20,
+ 'Developer' => 30
+ }
+ )
+ end
+ end
+
+ context 'when a user is an admin', :enable_admin_mode do
+ before do
+ user.update!(admin: true)
+ end
+
+ it 'returns all roles' do
+ expect(subject).to eq(
+ {
+ 'Guest' => 10,
+ 'Reporter' => 20,
+ 'Developer' => 30,
+ 'Maintainer' => 40,
+ 'Owner' => 50
+ }
+ )
+ end
+ end
+
+ context 'when a user is not a group member' do
+ it 'returns the empty array' do
+ expect(subject).to be_empty
+ end
+ end
+
+ context 'when user is not provided' do
+ before do
+ allow(helper).to receive(:current_user).and_return(nil)
+ end
+
+ it 'returns the empty array' do
+ expect(subject).to be_empty
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/bulk_imports/pipeline/runner_spec.rb b/spec/lib/bulk_imports/pipeline/runner_spec.rb
index 4540408990c..8c1a6a5b6c8 100644
--- a/spec/lib/bulk_imports/pipeline/runner_spec.rb
+++ b/spec/lib/bulk_imports/pipeline/runner_spec.rb
@@ -195,6 +195,8 @@ RSpec.describe BulkImports::Pipeline::Runner, feature_category: :importers do
end
expect(subject).to receive(:on_finish)
+ expect(context.bulk_import).to receive(:touch)
+ expect(context.entity).to receive(:touch)
expect_next_instance_of(Gitlab::Import::Logger) do |logger|
expect(logger).to receive(:info)
diff --git a/spec/lib/gitlab/bitbucket_import/importers/issues_importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importers/issues_importer_spec.rb
index af5a929683e..90987f6d3d4 100644
--- a/spec/lib/gitlab/bitbucket_import/importers/issues_importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importers/issues_importer_spec.rb
@@ -67,7 +67,7 @@ RSpec.describe Gitlab::BitbucketImport::Importers::IssuesImporter, feature_categ
it 'tracks the failure and does not fail' do
expect(Gitlab::Import::ImportFailureService).to receive(:track).once
- importer.execute
+ expect(importer.execute).to be_a(Gitlab::JobWaiter)
end
end
diff --git a/spec/lib/gitlab/bitbucket_import/importers/issues_notes_importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importers/issues_notes_importer_spec.rb
index a04543b0511..84dea203478 100644
--- a/spec/lib/gitlab/bitbucket_import/importers/issues_notes_importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importers/issues_notes_importer_spec.rb
@@ -29,7 +29,7 @@ RSpec.describe Gitlab::BitbucketImport::Importers::IssuesNotesImporter, feature_
it 'tracks the failure and does not fail' do
expect(Gitlab::Import::ImportFailureService).to receive(:track).once
- importer.execute
+ expect(importer.execute).to be_a(Gitlab::JobWaiter)
end
end
diff --git a/spec/lib/gitlab/bitbucket_import/importers/pull_requests_importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importers/pull_requests_importer_spec.rb
index eba7ec92aba..4d72c47d61a 100644
--- a/spec/lib/gitlab/bitbucket_import/importers/pull_requests_importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importers/pull_requests_importer_spec.rb
@@ -49,7 +49,7 @@ RSpec.describe Gitlab::BitbucketImport::Importers::PullRequestsImporter, feature
it 'tracks the failure and does not fail' do
expect(Gitlab::Import::ImportFailureService).to receive(:track).once
- importer.execute
+ expect(importer.execute).to be_a(Gitlab::JobWaiter)
end
end
diff --git a/spec/lib/gitlab/bitbucket_import/importers/pull_requests_notes_importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importers/pull_requests_notes_importer_spec.rb
index 78a08accf82..b4c26ff7add 100644
--- a/spec/lib/gitlab/bitbucket_import/importers/pull_requests_notes_importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importers/pull_requests_notes_importer_spec.rb
@@ -29,7 +29,7 @@ RSpec.describe Gitlab::BitbucketImport::Importers::PullRequestsNotesImporter, fe
it 'tracks the failure and does not fail' do
expect(Gitlab::Import::ImportFailureService).to receive(:track).once
- importer.execute
+ expect(importer.execute).to be_a(Gitlab::JobWaiter)
end
end
diff --git a/spec/lib/gitlab/checks/branch_check_spec.rb b/spec/lib/gitlab/checks/branch_check_spec.rb
index c3d6b9510e5..8772e8dd904 100644
--- a/spec/lib/gitlab/checks/branch_check_spec.rb
+++ b/spec/lib/gitlab/checks/branch_check_spec.rb
@@ -19,39 +19,39 @@ RSpec.describe Gitlab::Checks::BranchCheck, feature_category: :source_code_manag
end
end
- context "prohibited branches check" do
- it "prohibits 40-character hexadecimal branch names" do
+ describe "prohibited branches check" do
+ it "forbids SHA-1 values" do
allow(subject).to receive(:branch_name).and_return("267208abfe40e546f5e847444276f7d43a39503e")
- expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "You cannot create a branch with a 40-character hexadecimal branch name.")
+ expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "You cannot create a branch with a SHA-1 or SHA-256 branch name.")
end
- it "prohibits 40-character hexadecimal branch names as the start of a path" do
- allow(subject).to receive(:branch_name).and_return("267208abfe40e546f5e847444276f7d43a39503e/test")
+ it "forbids SHA-256 values" do
+ allow(subject).to receive(:branch_name).and_return("09b9fd3ea68e9b95a51b693a29568c898e27d1476bbd83c825664f18467fc175")
- expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "You cannot create a branch with a 40-character hexadecimal branch name.")
+ expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "You cannot create a branch with a SHA-1 or SHA-256 branch name.")
end
- it "prohibits 40-character hexadecimal branch names followed by a dash as the start of a path" do
- allow(subject).to receive(:branch_name).and_return("267208abfe40e546f5e847444276f7d43a39503e-/test")
+ it "forbids '{SHA-1}{+anything}' values" do
+ allow(subject).to receive(:branch_name).and_return("267208abfe40e546f5e847444276f7d43a39503e-")
- expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "You cannot create a branch with a 40-character hexadecimal branch name.")
+ expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "You cannot create a branch with a SHA-1 or SHA-256 branch name.")
end
- it "prohibits 64-character hexadecimal branch names" do
- allow(subject).to receive(:branch_name).and_return("09b9fd3ea68e9b95a51b693a29568c898e27d1476bbd83c825664f18467fc175")
+ it "forbids '{SHA-256}{+anything} values" do
+ allow(subject).to receive(:branch_name).and_return("09b9fd3ea68e9b95a51b693a29568c898e27d1476bbd83c825664f18467fc175-")
- expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "You cannot create a branch with a 40-character hexadecimal branch name.")
+ expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "You cannot create a branch with a SHA-1 or SHA-256 branch name.")
end
- it "prohibits 64-character hexadecimal branch names as the start of a path" do
- allow(subject).to receive(:branch_name).and_return("09b9fd3ea68e9b95a51b693a29568c898e27d1476bbd83c825664f18467fc175/test")
+ it "allows SHA-1 values to be appended to the branch name" do
+ allow(subject).to receive(:branch_name).and_return("fix-267208abfe40e546f5e847444276f7d43a39503e")
- expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "You cannot create a branch with a 40-character hexadecimal branch name.")
+ expect { subject.validate! }.not_to raise_error
end
- it "doesn't prohibit a nested hexadecimal in a branch name" do
- allow(subject).to receive(:branch_name).and_return("267208abfe40e546f5e847444276f7d43a39503e-fix")
+ it "allows SHA-256 values to be appended to the branch name" do
+ allow(subject).to receive(:branch_name).and_return("fix-09b9fd3ea68e9b95a51b693a29568c898e27d1476bbd83c825664f18467fc175")
expect { subject.validate! }.not_to raise_error
end
diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb
index e62dddcc28b..726c98ed704 100644
--- a/spec/models/ability_spec.rb
+++ b/spec/models/ability_spec.rb
@@ -3,9 +3,42 @@
require 'spec_helper'
RSpec.describe Ability do
- context 'using a nil subject' do
- it 'has no permissions' do
- expect(described_class.policy_for(nil, nil)).to be_banned
+ describe '#policy_for' do
+ subject(:policy) { described_class.policy_for(user, subject, **options) }
+
+ let(:user) { User.new }
+ let(:subject) { :global }
+ let(:options) { {} }
+
+ context 'using a nil subject' do
+ let(:user) { nil }
+ let(:subject) { nil }
+
+ it 'has no permissions' do
+ expect(policy).to be_banned
+ end
+ end
+
+ context 'with request store', :request_store do
+ before do
+ ::Gitlab::SafeRequestStore.write(:example, :value) # make request store different from {}
+ end
+
+ it 'caches in the request store' do
+ expect(DeclarativePolicy).to receive(:policy_for).with(user, subject, cache: ::Gitlab::SafeRequestStore.storage)
+
+ policy
+ end
+
+ context 'when cache: false' do
+ let(:options) { { cache: false } }
+
+ it 'uses a fresh cache each time' do
+ expect(DeclarativePolicy).to receive(:policy_for).with(user, subject, cache: {})
+
+ policy
+ end
+ end
end
end
diff --git a/spec/models/bulk_import_spec.rb b/spec/models/bulk_import_spec.rb
index ff24f57f7c4..015357214b9 100644
--- a/spec/models/bulk_import_spec.rb
+++ b/spec/models/bulk_import_spec.rb
@@ -7,8 +7,8 @@ RSpec.describe BulkImport, type: :model, feature_category: :importers do
let_it_be(:started_bulk_import) { create(:bulk_import, :started) }
let_it_be(:finished_bulk_import) { create(:bulk_import, :finished) }
let_it_be(:failed_bulk_import) { create(:bulk_import, :failed) }
- let_it_be(:stale_created_bulk_import) { create(:bulk_import, :created, created_at: 3.days.ago) }
- let_it_be(:stale_started_bulk_import) { create(:bulk_import, :started, created_at: 3.days.ago) }
+ let_it_be(:stale_created_bulk_import) { create(:bulk_import, :created, updated_at: 3.days.ago) }
+ let_it_be(:stale_started_bulk_import) { create(:bulk_import, :started, updated_at: 3.days.ago) }
describe 'associations' do
it { is_expected.to belong_to(:user).required }
diff --git a/spec/models/bulk_imports/entity_spec.rb b/spec/models/bulk_imports/entity_spec.rb
index b31afe30003..612fa7acfa3 100644
--- a/spec/models/bulk_imports/entity_spec.rb
+++ b/spec/models/bulk_imports/entity_spec.rb
@@ -191,6 +191,15 @@ RSpec.describe BulkImports::Entity, type: :model, feature_category: :importers d
expect(described_class.by_user_id(user.id)).to contain_exactly(entity_1, entity_2)
end
end
+
+ describe '.stale' do
+ it 'returns entities that are stale' do
+ entity_1 = create(:bulk_import_entity, updated_at: 3.days.ago)
+ create(:bulk_import_entity)
+
+ expect(described_class.stale).to contain_exactly(entity_1)
+ end
+ end
end
describe '.all_human_statuses' do
diff --git a/spec/models/integrations/jira_spec.rb b/spec/models/integrations/jira_spec.rb
index af021c51035..2a3a3ec7f09 100644
--- a/spec/models/integrations/jira_spec.rb
+++ b/spec/models/integrations/jira_spec.rb
@@ -251,7 +251,7 @@ RSpec.describe Integrations::Jira, feature_category: :integrations do
'EXT_EXT-1234' | 'EXT_EXT-1234'
'EXT3_EXT-1234' | 'EXT3_EXT-1234'
'3EXT_EXT-1234' | ''
- 'CVE-2022-123' | ''
+ 'CVE-2022-123' | 'CVE-2022'
'CVE-123' | 'CVE-123'
'abc-JIRA-1234' | 'JIRA-1234'
end
diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb
index 7aa5cf993dc..a9d2552d7b7 100644
--- a/spec/models/pages_domain_spec.rb
+++ b/spec/models/pages_domain_spec.rb
@@ -165,7 +165,7 @@ RSpec.describe PagesDomain, feature_category: :pages do
it "adds error to certificate" do
domain.valid?
- expect(domain.errors.attribute_names).to contain_exactly(:key, :certificate)
+ expect(domain.errors.attribute_names).to contain_exactly(:key)
end
end
@@ -206,10 +206,25 @@ RSpec.describe PagesDomain, feature_category: :pages do
it 'validates the certificate key length' do
valid_domain = build(:pages_domain, :key_length_8192)
expect(valid_domain).to be_valid
+ end
+
+ context 'when the key has more than 8192 bytes' do
+ let(:domain) do
+ build(:pages_domain, :extra_long_key)
+ end
- invalid_domain = build(:pages_domain, :extra_long_key)
- expect(invalid_domain).to be_invalid
- expect(invalid_domain.errors[:key]).to include('Certificate Key is too long. (Max 8192 bytes)')
+ it 'adds a human readable error' do
+ expect(domain).to be_invalid
+ expect(domain.errors[:key]).to include('Certificate Key is too long. (Max 8192 bytes)')
+ end
+
+ it 'does not run SSL key verification' do
+ allow(domain).to receive(:validate_intermediates)
+
+ domain.valid?
+
+ expect(domain).not_to have_received(:validate_intermediates)
+ end
end
end
end
diff --git a/spec/policies/ci/pipeline_schedule_policy_spec.rb b/spec/policies/ci/pipeline_schedule_policy_spec.rb
index 8fc5c6ca296..1d353b9a35e 100644
--- a/spec/policies/ci/pipeline_schedule_policy_spec.rb
+++ b/spec/policies/ci/pipeline_schedule_policy_spec.rb
@@ -6,6 +6,7 @@ RSpec.describe Ci::PipelineSchedulePolicy, :models, :clean_gitlab_redis_cache, f
using RSpec::Parameterized::TableSyntax
let_it_be(:user) { create(:user) }
+ let_it_be(:other_user) { create(:user) }
let_it_be_with_reload(:project) { create(:project, :repository, create_tag: tag_ref_name) }
let_it_be_with_reload(:pipeline_schedule) { create(:ci_pipeline_schedule, :nightly, project: project) }
let_it_be(:tag_ref_name) { "v1.0.0" }
@@ -17,89 +18,180 @@ RSpec.describe Ci::PipelineSchedulePolicy, :models, :clean_gitlab_redis_cache, f
describe 'rules' do
describe 'rules for protected ref' do
context 'for branch' do
+ subject(:policy) { described_class.new(user, pipeline_schedule) }
+
%w[refs/heads/master master].each do |branch_ref|
context "with #{branch_ref}" do
let_it_be(:branch_ref_name) { "master" }
- let_it_be(:branch_pipeline_schedule) do
+ let_it_be(:pipeline_schedule) do
create(:ci_pipeline_schedule, :nightly, project: project, ref: branch_ref)
end
- where(:push_access_level, :merge_access_level, :project_role, :accessible) do
- :no_one_can_push | :no_one_can_merge | :owner | :be_disallowed
- :no_one_can_push | :no_one_can_merge | :maintainer | :be_disallowed
- :no_one_can_push | :no_one_can_merge | :developer | :be_disallowed
- :no_one_can_push | :no_one_can_merge | :reporter | :be_disallowed
- :no_one_can_push | :no_one_can_merge | :guest | :be_disallowed
-
- :maintainers_can_push | :no_one_can_merge | :owner | :be_allowed
- :maintainers_can_push | :no_one_can_merge | :maintainer | :be_allowed
- :maintainers_can_push | :no_one_can_merge | :developer | :be_disallowed
- :maintainers_can_push | :no_one_can_merge | :reporter | :be_disallowed
- :maintainers_can_push | :no_one_can_merge | :guest | :be_disallowed
-
- :developers_can_push | :no_one_can_merge | :owner | :be_allowed
- :developers_can_push | :no_one_can_merge | :maintainer | :be_allowed
- :developers_can_push | :no_one_can_merge | :developer | :be_allowed
- :developers_can_push | :no_one_can_merge | :reporter | :be_disallowed
- :developers_can_push | :no_one_can_merge | :guest | :be_disallowed
-
- :no_one_can_push | :maintainers_can_merge | :owner | :be_allowed
- :no_one_can_push | :maintainers_can_merge | :maintainer | :be_allowed
- :no_one_can_push | :maintainers_can_merge | :developer | :be_disallowed
- :no_one_can_push | :maintainers_can_merge | :reporter | :be_disallowed
- :no_one_can_push | :maintainers_can_merge | :guest | :be_disallowed
-
- :maintainers_can_push | :maintainers_can_merge | :owner | :be_allowed
- :maintainers_can_push | :maintainers_can_merge | :maintainer | :be_allowed
- :maintainers_can_push | :maintainers_can_merge | :developer | :be_disallowed
- :maintainers_can_push | :maintainers_can_merge | :reporter | :be_disallowed
- :maintainers_can_push | :maintainers_can_merge | :guest | :be_disallowed
-
- :developers_can_push | :maintainers_can_merge | :owner | :be_allowed
- :developers_can_push | :maintainers_can_merge | :maintainer | :be_allowed
- :developers_can_push | :maintainers_can_merge | :developer | :be_allowed
- :developers_can_push | :maintainers_can_merge | :reporter | :be_disallowed
- :developers_can_push | :maintainers_can_merge | :guest | :be_disallowed
-
- :no_one_can_push | :developers_can_merge | :owner | :be_allowed
- :no_one_can_push | :developers_can_merge | :maintainer | :be_allowed
- :no_one_can_push | :developers_can_merge | :developer | :be_allowed
- :no_one_can_push | :developers_can_merge | :reporter | :be_disallowed
- :no_one_can_push | :developers_can_merge | :guest | :be_disallowed
-
- :maintainers_can_push | :developers_can_merge | :owner | :be_allowed
- :maintainers_can_push | :developers_can_merge | :maintainer | :be_allowed
- :maintainers_can_push | :developers_can_merge | :developer | :be_allowed
- :maintainers_can_push | :developers_can_merge | :reporter | :be_disallowed
- :maintainers_can_push | :developers_can_merge | :guest | :be_disallowed
-
- :developers_can_push | :developers_can_merge | :owner | :be_allowed
- :developers_can_push | :developers_can_merge | :maintainer | :be_allowed
- :developers_can_push | :developers_can_merge | :developer | :be_allowed
- :developers_can_push | :developers_can_merge | :reporter | :be_disallowed
- :developers_can_push | :developers_can_merge | :guest | :be_disallowed
+ shared_examples_for 'allowed by those who can update the branch' do
+ where(:push_access_level, :merge_access_level, :project_role, :accessible) do
+ :no_one_can_push | :no_one_can_merge | :owner | :be_disallowed
+ :no_one_can_push | :no_one_can_merge | :maintainer | :be_disallowed
+ :no_one_can_push | :no_one_can_merge | :developer | :be_disallowed
+ :no_one_can_push | :no_one_can_merge | :reporter | :be_disallowed
+ :no_one_can_push | :no_one_can_merge | :guest | :be_disallowed
+
+ :maintainers_can_push | :no_one_can_merge | :owner | :be_allowed
+ :maintainers_can_push | :no_one_can_merge | :maintainer | :be_allowed
+ :maintainers_can_push | :no_one_can_merge | :developer | :be_disallowed
+ :maintainers_can_push | :no_one_can_merge | :reporter | :be_disallowed
+ :maintainers_can_push | :no_one_can_merge | :guest | :be_disallowed
+
+ :developers_can_push | :no_one_can_merge | :owner | :be_allowed
+ :developers_can_push | :no_one_can_merge | :maintainer | :be_allowed
+ :developers_can_push | :no_one_can_merge | :developer | :be_allowed
+ :developers_can_push | :no_one_can_merge | :reporter | :be_disallowed
+ :developers_can_push | :no_one_can_merge | :guest | :be_disallowed
+
+ :no_one_can_push | :maintainers_can_merge | :owner | :be_allowed
+ :no_one_can_push | :maintainers_can_merge | :maintainer | :be_allowed
+ :no_one_can_push | :maintainers_can_merge | :developer | :be_disallowed
+ :no_one_can_push | :maintainers_can_merge | :reporter | :be_disallowed
+ :no_one_can_push | :maintainers_can_merge | :guest | :be_disallowed
+
+ :maintainers_can_push | :maintainers_can_merge | :owner | :be_allowed
+ :maintainers_can_push | :maintainers_can_merge | :maintainer | :be_allowed
+ :maintainers_can_push | :maintainers_can_merge | :developer | :be_disallowed
+ :maintainers_can_push | :maintainers_can_merge | :reporter | :be_disallowed
+ :maintainers_can_push | :maintainers_can_merge | :guest | :be_disallowed
+
+ :developers_can_push | :maintainers_can_merge | :owner | :be_allowed
+ :developers_can_push | :maintainers_can_merge | :maintainer | :be_allowed
+ :developers_can_push | :maintainers_can_merge | :developer | :be_allowed
+ :developers_can_push | :maintainers_can_merge | :reporter | :be_disallowed
+ :developers_can_push | :maintainers_can_merge | :guest | :be_disallowed
+
+ :no_one_can_push | :developers_can_merge | :owner | :be_allowed
+ :no_one_can_push | :developers_can_merge | :maintainer | :be_allowed
+ :no_one_can_push | :developers_can_merge | :developer | :be_allowed
+ :no_one_can_push | :developers_can_merge | :reporter | :be_disallowed
+ :no_one_can_push | :developers_can_merge | :guest | :be_disallowed
+
+ :maintainers_can_push | :developers_can_merge | :owner | :be_allowed
+ :maintainers_can_push | :developers_can_merge | :maintainer | :be_allowed
+ :maintainers_can_push | :developers_can_merge | :developer | :be_allowed
+ :maintainers_can_push | :developers_can_merge | :reporter | :be_disallowed
+ :maintainers_can_push | :developers_can_merge | :guest | :be_disallowed
+
+ :developers_can_push | :developers_can_merge | :owner | :be_allowed
+ :developers_can_push | :developers_can_merge | :maintainer | :be_allowed
+ :developers_can_push | :developers_can_merge | :developer | :be_allowed
+ :developers_can_push | :developers_can_merge | :reporter | :be_disallowed
+ :developers_can_push | :developers_can_merge | :guest | :be_disallowed
+ end
+
+ with_them do
+ before do
+ create(:protected_branch, push_access_level, merge_access_level, name: branch_ref_name,
+ project: project)
+ project.add_role(user, project_role)
+ end
+
+ it { expect(policy).to try(accessible, :create_pipeline_schedule) }
+ end
end
- with_them do
- before do
- create(:protected_branch, push_access_level, merge_access_level, name: branch_ref_name,
- project: project)
- project.add_role(user, project_role)
+ shared_examples_for 'only allowed by schedule owners who can update the branch' do
+ where(:push_access_level, :merge_access_level, :schedule_owner, :project_role, :accessible) do
+ :no_one_can_push | :no_one_can_merge | :other_user | :owner | :be_disallowed
+ :no_one_can_push | :no_one_can_merge | :user | :owner | :be_disallowed
+ :no_one_can_push | :no_one_can_merge | :user | :maintainer | :be_disallowed
+ :no_one_can_push | :no_one_can_merge | :user | :developer | :be_disallowed
+ :no_one_can_push | :no_one_can_merge | :user | :reporter | :be_disallowed
+ :no_one_can_push | :no_one_can_merge | :user | :guest | :be_disallowed
+
+ :maintainers_can_push | :no_one_can_merge | :other_user | :owner | :be_disallowed
+ :maintainers_can_push | :no_one_can_merge | :user | :owner | :be_allowed
+ :maintainers_can_push | :no_one_can_merge | :user | :maintainer | :be_allowed
+ :maintainers_can_push | :no_one_can_merge | :user | :developer | :be_disallowed
+ :maintainers_can_push | :no_one_can_merge | :user | :reporter | :be_disallowed
+ :maintainers_can_push | :no_one_can_merge | :user | :guest | :be_disallowed
+
+ :developers_can_push | :no_one_can_merge | :other_user | :owner | :be_disallowed
+ :developers_can_push | :no_one_can_merge | :user | :owner | :be_allowed
+ :developers_can_push | :no_one_can_merge | :user | :maintainer | :be_allowed
+ :developers_can_push | :no_one_can_merge | :user | :developer | :be_allowed
+ :developers_can_push | :no_one_can_merge | :user | :reporter | :be_disallowed
+ :developers_can_push | :no_one_can_merge | :user | :guest | :be_disallowed
+
+ :no_one_can_push | :maintainers_can_merge | :other_user | :owner | :be_disallowed
+ :no_one_can_push | :maintainers_can_merge | :user | :owner | :be_allowed
+ :no_one_can_push | :maintainers_can_merge | :user | :maintainer | :be_allowed
+ :no_one_can_push | :maintainers_can_merge | :user | :developer | :be_disallowed
+ :no_one_can_push | :maintainers_can_merge | :user | :reporter | :be_disallowed
+ :no_one_can_push | :maintainers_can_merge | :user | :guest | :be_disallowed
+
+ :maintainers_can_push | :maintainers_can_merge | :other_user | :owner | :be_disallowed
+ :maintainers_can_push | :maintainers_can_merge | :user | :owner | :be_allowed
+ :maintainers_can_push | :maintainers_can_merge | :user | :maintainer | :be_allowed
+ :maintainers_can_push | :maintainers_can_merge | :user | :developer | :be_disallowed
+ :maintainers_can_push | :maintainers_can_merge | :user | :reporter | :be_disallowed
+ :maintainers_can_push | :maintainers_can_merge | :user | :guest | :be_disallowed
+
+ :developers_can_push | :maintainers_can_merge | :other_user | :owner | :be_disallowed
+ :developers_can_push | :maintainers_can_merge | :user | :owner | :be_allowed
+ :developers_can_push | :maintainers_can_merge | :user | :maintainer | :be_allowed
+ :developers_can_push | :maintainers_can_merge | :user | :developer | :be_allowed
+ :developers_can_push | :maintainers_can_merge | :user | :reporter | :be_disallowed
+ :developers_can_push | :maintainers_can_merge | :user | :guest | :be_disallowed
+
+ :no_one_can_push | :developers_can_merge | :other_user | :owner | :be_disallowed
+ :no_one_can_push | :developers_can_merge | :user | :owner | :be_allowed
+ :no_one_can_push | :developers_can_merge | :user | :maintainer | :be_allowed
+ :no_one_can_push | :developers_can_merge | :user | :developer | :be_allowed
+ :no_one_can_push | :developers_can_merge | :user | :reporter | :be_disallowed
+ :no_one_can_push | :developers_can_merge | :user | :guest | :be_disallowed
+
+ :maintainers_can_push | :developers_can_merge | :other_user | :owner | :be_disallowed
+ :maintainers_can_push | :developers_can_merge | :user | :owner | :be_allowed
+ :maintainers_can_push | :developers_can_merge | :user | :maintainer | :be_allowed
+ :maintainers_can_push | :developers_can_merge | :user | :developer | :be_allowed
+ :maintainers_can_push | :developers_can_merge | :user | :reporter | :be_disallowed
+ :maintainers_can_push | :developers_can_merge | :user | :guest | :be_disallowed
+
+ :developers_can_push | :developers_can_merge | :other_user | :owner | :be_disallowed
+ :developers_can_push | :developers_can_merge | :user | :owner | :be_allowed
+ :developers_can_push | :developers_can_merge | :user | :maintainer | :be_allowed
+ :developers_can_push | :developers_can_merge | :user | :developer | :be_allowed
+ :developers_can_push | :developers_can_merge | :user | :reporter | :be_disallowed
+ :developers_can_push | :developers_can_merge | :user | :guest | :be_disallowed
end
- context 'for create_pipeline_schedule' do
- subject(:policy) { described_class.new(user, new_branch_pipeline_schedule) }
+ with_them do
+ before do
+ create(:protected_branch, push_access_level, merge_access_level, name: branch_ref_name,
+ project: project)
+ project.add_role(user, project_role)
+ project.add_role(other_user, project_role)
- let(:new_branch_pipeline_schedule) { project.pipeline_schedules.new(ref: branch_ref) }
+ pipeline_schedule.owner = schedule_owner == :user ? user : other_user
+ end
- it { expect(policy).to try(accessible, :create_pipeline_schedule) }
+ it { expect(policy).to try(accessible, ability_name) }
end
+ end
- context 'for play_pipeline_schedule' do
- subject(:policy) { described_class.new(user, branch_pipeline_schedule) }
+ describe 'create_pipeline_schedule' do
+ let(:ability_name) { :create_pipeline_schedule }
+ let(:pipeline_schedule) { project.pipeline_schedules.new(ref: branch_ref) }
- it { expect(policy).to try(accessible, :play_pipeline_schedule) }
- end
+ it_behaves_like 'allowed by those who can update the branch'
+ end
+
+ describe 'play_pipeline_schedule' do
+ let(:ability_name) { :play_pipeline_schedule }
+
+ it_behaves_like 'allowed by those who can update the branch'
+ end
+
+ describe 'update_pipeline_schedule' do
+ let(:ability_name) { :update_pipeline_schedule }
+
+ it_behaves_like 'only allowed by schedule owners who can update the branch'
end
end
end
@@ -108,49 +200,97 @@ RSpec.describe Ci::PipelineSchedulePolicy, :models, :clean_gitlab_redis_cache, f
context 'for tag' do
%w[refs/tags/v1.0.0 v1.0.0].each do |tag_ref|
context "with #{tag_ref}" do
- let_it_be(:tag_pipeline_schedule) do
+ let_it_be(:pipeline_schedule) do
create(:ci_pipeline_schedule, :nightly, project: project, ref: tag_ref)
end
- where(:access_level, :project_role, :accessible) do
- :no_one_can_create | :owner | :be_disallowed
- :no_one_can_create | :maintainer | :be_disallowed
- :no_one_can_create | :developer | :be_disallowed
- :no_one_can_create | :reporter | :be_disallowed
- :no_one_can_create | :guest | :be_disallowed
-
- :maintainers_can_create | :owner | :be_allowed
- :maintainers_can_create | :maintainer | :be_allowed
- :maintainers_can_create | :developer | :be_disallowed
- :maintainers_can_create | :reporter | :be_disallowed
- :maintainers_can_create | :guest | :be_disallowed
-
- :developers_can_create | :owner | :be_allowed
- :developers_can_create | :maintainer | :be_allowed
- :developers_can_create | :developer | :be_allowed
- :developers_can_create | :reporter | :be_disallowed
- :developers_can_create | :guest | :be_disallowed
+ subject(:policy) { described_class.new(user, pipeline_schedule) }
+
+ shared_examples_for 'allowed by those who can update the tag' do
+ where(:access_level, :project_role, :accessible) do
+ :no_one_can_create | :owner | :be_disallowed
+ :no_one_can_create | :maintainer | :be_disallowed
+ :no_one_can_create | :developer | :be_disallowed
+ :no_one_can_create | :reporter | :be_disallowed
+ :no_one_can_create | :guest | :be_disallowed
+
+ :maintainers_can_create | :owner | :be_allowed
+ :maintainers_can_create | :maintainer | :be_allowed
+ :maintainers_can_create | :developer | :be_disallowed
+ :maintainers_can_create | :reporter | :be_disallowed
+ :maintainers_can_create | :guest | :be_disallowed
+
+ :developers_can_create | :owner | :be_allowed
+ :developers_can_create | :maintainer | :be_allowed
+ :developers_can_create | :developer | :be_allowed
+ :developers_can_create | :reporter | :be_disallowed
+ :developers_can_create | :guest | :be_disallowed
+ end
+
+ with_them do
+ before do
+ create(:protected_tag, access_level, name: tag_ref_name, project: project)
+ project.add_role(user, project_role)
+ end
+
+ it { expect(policy).to try(accessible, ability_name) }
+ end
end
- with_them do
- before do
- create(:protected_tag, access_level, name: tag_ref_name, project: project)
- project.add_role(user, project_role)
+ shared_examples_for 'only allowed by schedule owners who can update the tag' do
+ where(:access_level, :schedule_owner, :project_role, :accessible) do
+ :no_one_can_create | :other_user | :owner | :be_disallowed
+ :no_one_can_create | :user | :owner | :be_disallowed
+ :no_one_can_create | :user | :maintainer | :be_disallowed
+ :no_one_can_create | :user | :developer | :be_disallowed
+ :no_one_can_create | :user | :reporter | :be_disallowed
+ :no_one_can_create | :user | :guest | :be_disallowed
+
+ :maintainers_can_create | :other_user | :owner | :be_disallowed
+ :maintainers_can_create | :user | :owner | :be_allowed
+ :maintainers_can_create | :user | :maintainer | :be_allowed
+ :maintainers_can_create | :user | :developer | :be_disallowed
+ :maintainers_can_create | :user | :reporter | :be_disallowed
+ :maintainers_can_create | :user | :guest | :be_disallowed
+
+ :developers_can_create | :other_user | :owner | :be_disallowed
+ :developers_can_create | :user | :owner | :be_allowed
+ :developers_can_create | :user | :maintainer | :be_allowed
+ :developers_can_create | :user | :developer | :be_allowed
+ :developers_can_create | :user | :reporter | :be_disallowed
+ :developers_can_create | :user | :guest | :be_disallowed
end
- context 'for create_pipeline_schedule' do
- subject(:policy) { described_class.new(user, new_tag_pipeline_schedule) }
+ with_them do
+ before do
+ create(:protected_tag, access_level, name: tag_ref_name, project: project)
+ project.add_role(user, project_role)
+ project.add_role(other_user, project_role)
- let(:new_tag_pipeline_schedule) { project.pipeline_schedules.new(ref: tag_ref) }
+ pipeline_schedule.owner = schedule_owner == :user ? user : other_user
+ end
- it { expect(policy).to try(accessible, :create_pipeline_schedule) }
+ it { expect(policy).to try(accessible, ability_name) }
end
+ end
- context 'for play_pipeline_schedule' do
- subject(:policy) { described_class.new(user, tag_pipeline_schedule) }
+ describe 'create_pipeline_schedule' do
+ let(:ability_name) { :create_pipeline_schedule }
+ let(:pipeline_schedule) { project.pipeline_schedules.new(ref: tag_ref) }
- it { expect(policy).to try(accessible, :play_pipeline_schedule) }
- end
+ it_behaves_like 'allowed by those who can update the tag'
+ end
+
+ describe 'play_pipeline_schedule' do
+ let(:ability_name) { :play_pipeline_schedule }
+
+ it_behaves_like 'allowed by those who can update the tag'
+ end
+
+ describe 'update_pipeline_schedule' do
+ let(:ability_name) { :update_pipeline_schedule }
+
+ it_behaves_like 'only allowed by schedule owners who can update the tag'
end
end
end
diff --git a/spec/policies/issue_policy_spec.rb b/spec/policies/issue_policy_spec.rb
index c19b7bcf9ea..1d7748ee25a 100644
--- a/spec/policies/issue_policy_spec.rb
+++ b/spec/policies/issue_policy_spec.rb
@@ -146,50 +146,50 @@ RSpec.describe IssuePolicy, feature_category: :team_planning do
let(:confidential_issue_no_assignee) { create(:issue, :confidential, project: project) }
it 'does not allow non-members to read confidential issues' do
- expect(permissions(non_member, confidential_issue)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :admin_issue_relation)
- expect(permissions(non_member, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
+ expect(permissions(non_member, confidential_issue)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :admin_issue_relation, :award_emoji)
+ expect(permissions(non_member, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation, :award_emoji)
end
it 'does not allow guests to read confidential issues' do
- expect(permissions(guest, confidential_issue)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :admin_issue_relation)
- expect(permissions(guest, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
+ expect(permissions(guest, confidential_issue)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :admin_issue_relation, :award_emoji)
+ expect(permissions(guest, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation, :award_emoji)
end
it 'allows reporters to read, update, and admin confidential issues' do
- expect(permissions(reporter, confidential_issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
- expect(permissions(reporter, confidential_issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
+ expect(permissions(reporter, confidential_issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation, :award_emoji)
+ expect(permissions(reporter, confidential_issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation, :award_emoji)
end
it 'allows reporters from group links to read, update, and admin confidential issues' do
- expect(permissions(reporter_from_group_link, confidential_issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
- expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
+ expect(permissions(reporter_from_group_link, confidential_issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation, :award_emoji)
+ expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation, :award_emoji)
end
it 'allows issue authors to read and update their confidential issues' do
- expect(permissions(author, confidential_issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue_relation)
+ expect(permissions(author, confidential_issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue_relation, :award_emoji)
expect(permissions(author, confidential_issue)).to be_disallowed(:admin_issue, :set_issue_metadata, :set_confidentiality)
- expect(permissions(author, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :admin_issue_relation)
- expect(permissions(author, confidential_issue_no_assignee)).to be_disallowed(:admin_issue, :set_issue_metadata, :set_confidentiality)
+ expect(permissions(author, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :admin_issue_relation, :award_emoji)
+ expect(permissions(author, confidential_issue_no_assignee)).to be_disallowed(:admin_issue, :set_issue_metadata, :set_confidentiality, :award_emoji)
end
it 'does not allow issue author to read or update confidential issue moved to an private project' do
confidential_issue.project = create(:project, :private)
- expect(permissions(author, confidential_issue)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
+ expect(permissions(author, confidential_issue)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation, :award_emoji)
end
it 'allows issue assignees to read and update their confidential issues' do
- expect(permissions(assignee, confidential_issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue)
+ expect(permissions(assignee, confidential_issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :award_emoji)
expect(permissions(assignee, confidential_issue)).to be_disallowed(:admin_issue, :set_issue_metadata, :set_confidentiality)
- expect(permissions(assignee, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
+ expect(permissions(assignee, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation, :award_emoji)
end
it 'does not allow issue assignees to read or update confidential issue moved to an private project' do
confidential_issue.project = create(:project, :private)
- expect(permissions(assignee, confidential_issue)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation)
+ expect(permissions(assignee, confidential_issue)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :set_issue_metadata, :set_confidentiality, :admin_issue_relation, :award_emoji)
end
end
end
diff --git a/spec/services/ci/pipeline_schedules/update_service_spec.rb b/spec/services/ci/pipeline_schedules/update_service_spec.rb
index 834bbcfcfeb..b84afacdcff 100644
--- a/spec/services/ci/pipeline_schedules/update_service_spec.rb
+++ b/spec/services/ci/pipeline_schedules/update_service_spec.rb
@@ -7,16 +7,16 @@ RSpec.describe Ci::PipelineSchedules::UpdateService, feature_category: :continuo
let_it_be_with_reload(:project) { create(:project, :public, :repository) }
let_it_be_with_reload(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: user) }
let_it_be(:reporter) { create(:user) }
+ let_it_be(:project_owner) { create(:user) }
let_it_be(:pipeline_schedule_variable) do
create(:ci_pipeline_schedule_variable,
key: 'foo', value: 'foovalue', pipeline_schedule: pipeline_schedule)
end
- subject(:service) { described_class.new(pipeline_schedule, user, params) }
-
before_all do
project.add_maintainer(user)
+ project.add_owner(project_owner)
project.add_reporter(reporter)
pipeline_schedule.reload
@@ -54,8 +54,10 @@ RSpec.describe Ci::PipelineSchedules::UpdateService, feature_category: :continuo
subject(:service) { described_class.new(pipeline_schedule, user, params) }
it 'updates database values with passed params' do
- expect { service.execute }
- .to change { pipeline_schedule.description }.from('pipeline schedule').to('updated_desc')
+ expect do
+ service.execute
+ pipeline_schedule.reload
+ end.to change { pipeline_schedule.description }.from('pipeline schedule').to('updated_desc')
.and change { pipeline_schedule.ref }.from('master').to('patch-x')
.and change { pipeline_schedule.active }.from(true).to(false)
.and change { pipeline_schedule.cron }.from('0 1 * * *').to('*/1 * * * *')
@@ -63,6 +65,48 @@ RSpec.describe Ci::PipelineSchedules::UpdateService, feature_category: :continuo
.and change { pipeline_schedule.variables.last.value }.from('foovalue').to('barvalue')
end
+ context 'when the new branch is protected', :request_store do
+ let(:maintainer_access) { :no_one_can_merge }
+
+ before do
+ create(:protected_branch, :no_one_can_push, maintainer_access, name: 'patch-x', project: project)
+ end
+
+ after do
+ ProtectedBranches::CacheService.new(project).refresh
+ end
+
+ context 'when called by someone other than the schedule owner who can update the ref' do
+ let(:maintainer_access) { :maintainers_can_merge }
+
+ subject(:service) { described_class.new(pipeline_schedule, project_owner, params) }
+
+ it 'does not update the schedule' do
+ expect do
+ service.execute
+ pipeline_schedule.reload
+ end.not_to change { pipeline_schedule.description }
+ end
+ end
+
+ context 'when called by the schedule owner' do
+ it 'does not update the schedule' do
+ expect do
+ service.execute
+ pipeline_schedule.reload
+ end.not_to change { pipeline_schedule.description }
+ end
+
+ context 'when the owner can update the ref' do
+ let(:maintainer_access) { :maintainers_can_merge }
+
+ it 'updates the schedule' do
+ expect { service.execute }.to change { pipeline_schedule.description }
+ end
+ end
+ end
+ end
+
context 'when creating a variable' do
let(:params) do
{
@@ -126,6 +170,8 @@ RSpec.describe Ci::PipelineSchedules::UpdateService, feature_category: :continuo
end
end
- it_behaves_like 'pipeline schedules checking variables permission'
+ it_behaves_like 'pipeline schedules checking variables permission' do
+ subject(:service) { described_class.new(pipeline_schedule, user, params) }
+ end
end
end
diff --git a/spec/services/projects/lfs_pointers/lfs_link_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_link_service_spec.rb
index d3f053aaedc..5862ed15c2a 100644
--- a/spec/services/projects/lfs_pointers/lfs_link_service_spec.rb
+++ b/spec/services/projects/lfs_pointers/lfs_link_service_spec.rb
@@ -91,5 +91,23 @@ RSpec.describe Projects::LfsPointers::LfsLinkService, feature_category: :source_
# 3. Insert the lfs_objects_projects for that batch
expect { subject.execute(new_oid_list.keys) }.not_to exceed_query_limit(3)
end
+
+ context 'when MAX_OIDS is 5' do
+ let(:max_oids) { 5 }
+ let(:oids) { Array.new(max_oids) { |i| "oid-#{i}" } }
+
+ before do
+ stub_const("#{described_class}::MAX_OIDS", max_oids)
+ end
+
+ it 'does not raise an error when trying to link exactly the OID limit' do
+ expect { subject.execute(oids) }.not_to raise_error
+ end
+
+ it 'raises an error when trying to link more than OID limit' do
+ oids << 'the straw'
+ expect { subject.execute(oids) }.to raise_error(described_class::TooManyOidsError)
+ end
+ end
end
end
diff --git a/spec/support/shared_examples/analytics/cycle_analytics/request_params_examples.rb b/spec/support/shared_examples/analytics/cycle_analytics/request_params_examples.rb
index cf539174587..ddf3b1d7d17 100644
--- a/spec/support/shared_examples/analytics/cycle_analytics/request_params_examples.rb
+++ b/spec/support/shared_examples/analytics/cycle_analytics/request_params_examples.rb
@@ -137,7 +137,9 @@ RSpec.shared_examples 'unlicensed cycle analytics request params' do
it 'disables all paid features' do
is_expected.to match(a_hash_including(enable_tasks_by_type_chart: 'false',
enable_customizable_stages: 'false',
- enable_projects_filter: 'false'))
+ enable_projects_filter: 'false',
+ enable_vsd_link: 'false'
+ ))
end
end
diff --git a/spec/workers/bulk_imports/pipeline_worker_spec.rb b/spec/workers/bulk_imports/pipeline_worker_spec.rb
index 39f4d6c4f6d..09f76169a12 100644
--- a/spec/workers/bulk_imports/pipeline_worker_spec.rb
+++ b/spec/workers/bulk_imports/pipeline_worker_spec.rb
@@ -344,7 +344,7 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
end
end
- it 'reenqueues the worker' do
+ it 're_enqueues the worker' do
expect_any_instance_of(BulkImports::Tracker) do |tracker|
expect(tracker).to receive(:retry).and_call_original
end
diff --git a/spec/workers/bulk_imports/stuck_import_worker_spec.rb b/spec/workers/bulk_imports/stuck_import_worker_spec.rb
index eadf3864190..4bcc72326c7 100644
--- a/spec/workers/bulk_imports/stuck_import_worker_spec.rb
+++ b/spec/workers/bulk_imports/stuck_import_worker_spec.rb
@@ -5,10 +5,21 @@ require 'spec_helper'
RSpec.describe BulkImports::StuckImportWorker, feature_category: :importers do
let_it_be(:created_bulk_import) { create(:bulk_import, :created) }
let_it_be(:started_bulk_import) { create(:bulk_import, :started) }
- let_it_be(:stale_created_bulk_import) { create(:bulk_import, :created, created_at: 3.days.ago) }
- let_it_be(:stale_started_bulk_import) { create(:bulk_import, :started, created_at: 3.days.ago) }
- let_it_be(:stale_created_bulk_import_entity) { create(:bulk_import_entity, :created, created_at: 3.days.ago) }
- let_it_be(:stale_started_bulk_import_entity) { create(:bulk_import_entity, :started, created_at: 3.days.ago) }
+ let_it_be(:stale_created_bulk_import) do
+ create(:bulk_import, :created, updated_at: 3.days.ago)
+ end
+
+ let_it_be(:stale_started_bulk_import) do
+ create(:bulk_import, :started, updated_at: 3.days.ago)
+ end
+
+ let_it_be(:stale_created_bulk_import_entity) do
+ create(:bulk_import_entity, :created, updated_at: 3.days.ago)
+ end
+
+ let_it_be(:stale_started_bulk_import_entity) do
+ create(:bulk_import_entity, :started, updated_at: 3.days.ago)
+ end
let_it_be(:started_bulk_import_tracker) do
create(:bulk_import_tracker, :started, entity: stale_started_bulk_import_entity)
@@ -61,5 +72,29 @@ RSpec.describe BulkImports::StuckImportWorker, feature_category: :importers do
expect { subject }.to not_change { created_bulk_import.reload.status }
.and not_change { started_bulk_import.reload.status }
end
+
+ context 'when bulk import has been updated recently', :clean_gitlab_redis_shared_state do
+ before do
+ stale_created_bulk_import.update!(updated_at: 2.minutes.ago)
+ stale_started_bulk_import.update!(updated_at: 2.minutes.ago)
+ end
+
+ it 'does not update the status of the import' do
+ expect { subject }.to not_change { stale_created_bulk_import.reload.status_name }
+ .and not_change { stale_started_bulk_import.reload.status_name }
+ end
+ end
+
+ context 'when bulk import entity has been updated recently', :clean_gitlab_redis_shared_state do
+ before do
+ stale_created_bulk_import_entity.update!(updated_at: 2.minutes.ago)
+ stale_started_bulk_import_entity.update!(updated_at: 2.minutes.ago)
+ end
+
+ it 'does not update the status of the entity' do
+ expect { subject }.to not_change { stale_created_bulk_import_entity.reload.status_name }
+ .and not_change { stale_started_bulk_import_entity.reload.status_name }
+ end
+ end
end
end
diff --git a/yarn.lock b/yarn.lock
index b6e575d4fb9..826d724b2aa 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9420,10 +9420,10 @@ merge2@^1.3.0, merge2@^1.4.1:
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
-mermaid@10.6.0:
- version "10.6.0"
- resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-10.6.0.tgz#151af64fb7c6cf1f8a5c403c53c6151832268b87"
- integrity sha512-Hcti+Q2NiWnb2ZCijSX89Bn2i7TCUwosBdIn/d+u63Sz7y40XU6EKMctT4UX4qZuZGfKGZpfOeim2/KTrdR7aQ==
+mermaid@10.6.1:
+ version "10.6.1"
+ resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-10.6.1.tgz#701f4160484137a417770ce757ce1887a98c00fc"
+ integrity sha512-Hky0/RpOw/1il9X8AvzOEChfJtVvmXm+y7JML5C//ePYMy0/9jCEmW1E1g86x9oDfW9+iVEdTV/i+M6KWRNs4A==
dependencies:
"@braintree/sanitize-url" "^6.0.1"
"@types/d3-scale" "^4.0.3"