Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									74d9798736
								
							
						
					
					
						commit
						e0277d5393
					
				|  | @ -453,6 +453,106 @@ | |||
| .feature-flag-development-config-patterns: &feature-flag-development-config-patterns | ||||
|   - "{,ee/}config/feature_flags/{development,ops}/*.yml" | ||||
| 
 | ||||
| ################## | ||||
| # Conditions set # | ||||
| ################## | ||||
| .strict-ee-only-rules: | ||||
|   rules: | ||||
|     - <<: *if-not-ee | ||||
|       when: never | ||||
|     - <<: *if-jh | ||||
|       when: never | ||||
| 
 | ||||
| .as-if-jh-default-exclusion-rules: | ||||
|   rules: | ||||
|     - <<: *if-security-merge-request | ||||
|       when: never | ||||
|     - <<: *if-merge-request-targeting-stable-branch | ||||
|       when: never | ||||
|     - <<: *if-stable-branch-refs | ||||
|       when: never | ||||
|     - <<: *if-merge-request-labels-as-if-jh | ||||
|       allow_failure: true | ||||
| 
 | ||||
| .rails:rules:minimal-default-rules: | ||||
|   rules: | ||||
|     - <<: *if-merge-request-approved | ||||
|       when: never | ||||
|     - <<: *if-automated-merge-request | ||||
|       when: never | ||||
|     - <<: *if-security-merge-request | ||||
|       when: never | ||||
| 
 | ||||
| 
 | ||||
| .rails:rules:ee-and-foss-default-rules: | ||||
|   rules: | ||||
|     - <<: *if-merge-request-labels-run-all-rspec | ||||
|     - <<: *if-merge-request | ||||
|       changes: *core-backend-patterns | ||||
|     - <<: *if-merge-request | ||||
|       changes: *ci-patterns | ||||
|     - <<: *if-automated-merge-request | ||||
|       changes: *backend-patterns | ||||
|     - <<: *if-security-merge-request | ||||
|       changes: *backend-patterns | ||||
|     - <<: *if-merge-request-not-approved | ||||
|       when: never | ||||
| 
 | ||||
| .rails:rules:as-if-foss-migration-unit-integration:minimal-default-rules: | ||||
|   rules: | ||||
|     - <<: *if-merge-request | ||||
|       changes: *core-backend-patterns | ||||
|       when: never | ||||
|     - <<: *if-merge-request | ||||
|       changes: *ci-patterns | ||||
|       when: never | ||||
| 
 | ||||
| .rails:rules:unit-integration:minimal-default-rules: | ||||
|   rules: | ||||
|     - <<: *if-merge-request-labels-run-all-rspec | ||||
|       when: never | ||||
|     - !reference [".rails:rules:as-if-foss-migration-unit-integration:minimal-default-rules", rules] | ||||
| 
 | ||||
| .rails:rules:system-default-rules: | ||||
|   rules: | ||||
|     - <<: *if-merge-request-labels-run-all-rspec | ||||
|     - <<: *if-merge-request | ||||
|       changes: *core-backend-patterns | ||||
|     - <<: *if-merge-request | ||||
|       changes: *workhorse-patterns | ||||
|     - <<: *if-merge-request | ||||
|       changes: *ci-patterns | ||||
|     - <<: *if-automated-merge-request | ||||
|       changes: *code-backstage-patterns | ||||
|     - <<: *if-security-merge-request | ||||
|       changes: *code-backstage-patterns | ||||
|     - <<: *if-merge-request-not-approved | ||||
|       when: never | ||||
| 
 | ||||
| .rails:rules:system:minimal-default-rules: | ||||
|   rules: | ||||
|     - <<: *if-merge-request-labels-run-all-rspec | ||||
|       when: never | ||||
|     - <<: *if-merge-request | ||||
|       changes: *core-backend-patterns | ||||
|       when: never | ||||
|     - <<: *if-merge-request | ||||
|       changes: *workhorse-patterns | ||||
|       when: never | ||||
|     - <<: *if-merge-request | ||||
|       changes: *ci-patterns | ||||
|       when: never | ||||
|     - <<: *if-merge-request | ||||
|       changes: *code-backstage-patterns | ||||
| 
 | ||||
| .rails:rules:previous-failed-tests-default-rules: | ||||
|   rules: | ||||
|     - <<: *if-security-merge-request | ||||
|       when: never | ||||
|     - <<: *if-merge-request-labels-run-all-rspec | ||||
|     - <<: *if-merge-request | ||||
|       changes: *code-backstage-patterns | ||||
| 
 | ||||
| ################ | ||||
| # Shared rules # | ||||
| ################ | ||||
|  | @ -516,7 +616,6 @@ | |||
|   rules: | ||||
|     - <<: *if-default-refs | ||||
|       changes: *code-backstage-patterns | ||||
|       when: on_success | ||||
| 
 | ||||
| .dev-fixtures:rules:ee-only: | ||||
|   rules: | ||||
|  | @ -524,7 +623,6 @@ | |||
|       when: never | ||||
|     - <<: *if-default-refs | ||||
|       changes: *code-backstage-patterns | ||||
|       when: on_success | ||||
| 
 | ||||
| ############## | ||||
| # Docs rules # | ||||
|  | @ -540,7 +638,6 @@ | |||
|   rules: | ||||
|     - <<: *if-default-refs | ||||
|       changes: *docs-patterns | ||||
|       when: on_success | ||||
| 
 | ||||
| .docs:rules:deprecations-and-removals: | ||||
|   rules: | ||||
|  | @ -550,14 +647,12 @@ | |||
| ################## | ||||
| # GraphQL rules  # | ||||
| ################## | ||||
| 
 | ||||
| .graphql:rules:graphql-verify: | ||||
|   rules: | ||||
|     - <<: *if-not-ee | ||||
|       when: never | ||||
|     - <<: *if-default-refs | ||||
|       changes: *code-backstage-qa-patterns | ||||
|       when: on_success | ||||
| 
 | ||||
| ################## | ||||
| # Frontend rules # | ||||
|  | @ -589,18 +684,8 @@ | |||
| 
 | ||||
| .frontend:rules:compile-test-assets-as-if-jh: | ||||
|   rules: | ||||
|     - <<: *if-not-ee | ||||
|       when: never | ||||
|     - <<: *if-jh | ||||
|       when: never | ||||
|     - <<: *if-security-merge-request | ||||
|       when: never | ||||
|     - <<: *if-merge-request-targeting-stable-branch | ||||
|       when: never | ||||
|     - <<: *if-stable-branch-refs | ||||
|       when: never | ||||
|     - <<: *if-merge-request-labels-as-if-jh | ||||
|       allow_failure: true | ||||
|     - !reference [".strict-ee-only-rules", rules] | ||||
|     - !reference [".as-if-jh-default-exclusion-rules", rules] | ||||
|     - <<: *if-merge-request-labels-run-all-rspec | ||||
|       allow_failure: true | ||||
|     - changes: *code-backstage-qa-patterns | ||||
|  | @ -616,10 +701,7 @@ | |||
| 
 | ||||
| .frontend:rules:default-frontend-jobs-as-if-foss: | ||||
|   rules: | ||||
|     - <<: *if-not-ee | ||||
|       when: never | ||||
|     - <<: *if-jh | ||||
|       when: never | ||||
|     - !reference [".strict-ee-only-rules", rules] | ||||
|     - <<: *if-security-merge-request | ||||
|       changes: *code-backstage-patterns | ||||
|     - <<: *if-merge-request-labels-as-if-foss | ||||
|  | @ -631,18 +713,8 @@ | |||
| 
 | ||||
| .frontend:rules:default-frontend-jobs-as-if-jh: | ||||
|   rules: | ||||
|     - <<: *if-not-ee | ||||
|       when: never | ||||
|     - <<: *if-jh | ||||
|       when: never | ||||
|     - <<: *if-security-merge-request | ||||
|       when: never | ||||
|     - <<: *if-merge-request-targeting-stable-branch | ||||
|       when: never | ||||
|     - <<: *if-stable-branch-refs | ||||
|       when: never | ||||
|     - <<: *if-merge-request-labels-as-if-jh | ||||
|       allow_failure: true | ||||
|     - !reference [".strict-ee-only-rules", rules] | ||||
|     - !reference [".as-if-jh-default-exclusion-rules", rules] | ||||
|     - <<: *if-merge-request-labels-run-all-rspec | ||||
|       allow_failure: true | ||||
|     - <<: *if-merge-request | ||||
|  | @ -670,12 +742,7 @@ | |||
| 
 | ||||
| .frontend:rules:jest:minimal: | ||||
|   rules: | ||||
|     - <<: *if-merge-request-approved | ||||
|       when: never | ||||
|     - <<: *if-automated-merge-request | ||||
|       when: never | ||||
|     - <<: *if-security-merge-request | ||||
|       when: never | ||||
|     - !reference [".rails:rules:minimal-default-rules", rules] | ||||
|     - <<: *if-merge-request-labels-run-all-jest | ||||
|       when: never | ||||
|     - <<: *if-default-refs | ||||
|  | @ -689,10 +756,7 @@ | |||
| 
 | ||||
| .frontend:rules:eslint-as-if-foss: | ||||
|   rules: | ||||
|     - <<: *if-not-ee | ||||
|       when: never | ||||
|     - <<: *if-jh | ||||
|       when: never | ||||
|     - !reference [".strict-ee-only-rules", rules] | ||||
|     # We already have `static-analysis as-if-foss` which already runs `lint:eslint:all` if the `pipeline:run-as-if-foss` label is set. | ||||
|     - <<: *if-merge-request-labels-as-if-foss | ||||
|       when: never | ||||
|  | @ -740,7 +804,6 @@ | |||
|   rules: | ||||
|     - <<: *if-default-refs | ||||
|       changes: *code-patterns | ||||
|       when: on_success | ||||
| 
 | ||||
| ############### | ||||
| # Pages rules # | ||||
|  | @ -756,14 +819,10 @@ | |||
|   rules: | ||||
|     - <<: *if-default-refs | ||||
|       changes: *code-qa-patterns | ||||
|       when: on_success | ||||
| 
 | ||||
| .qa:rules:as-if-foss: | ||||
|   rules: | ||||
|     - <<: *if-not-ee | ||||
|       when: never | ||||
|     - <<: *if-jh | ||||
|       when: never | ||||
|     - !reference [".strict-ee-only-rules", rules] | ||||
|     - <<: *if-security-merge-request | ||||
|       changes: *code-qa-patterns | ||||
|     - <<: *if-merge-request-labels-as-if-foss | ||||
|  | @ -835,20 +894,8 @@ | |||
| 
 | ||||
| .rails:rules:ee-and-foss-migration:minimal: | ||||
|   rules: | ||||
|     - <<: *if-merge-request-approved | ||||
|       when: never | ||||
|     - <<: *if-automated-merge-request | ||||
|       when: never | ||||
|     - <<: *if-security-merge-request | ||||
|       when: never | ||||
|     - <<: *if-merge-request-labels-run-all-rspec | ||||
|       when: never | ||||
|     - <<: *if-merge-request | ||||
|       changes: *core-backend-patterns | ||||
|       when: never | ||||
|     - <<: *if-merge-request | ||||
|       changes: *ci-patterns | ||||
|       when: never | ||||
|     - !reference [".rails:rules:minimal-default-rules", rules] | ||||
|     - !reference [".rails:rules:unit-integration:minimal-default-rules", rules] | ||||
|     - <<: *if-merge-request | ||||
|       changes: *db-patterns | ||||
|       when: never | ||||
|  | @ -869,110 +916,37 @@ | |||
| 
 | ||||
| .rails:rules:ee-and-foss-unit: | ||||
|   rules: | ||||
|     - <<: *if-merge-request-labels-run-all-rspec | ||||
|     - <<: *if-merge-request | ||||
|       changes: *core-backend-patterns | ||||
|     - <<: *if-merge-request | ||||
|       changes: *ci-patterns | ||||
|     - <<: *if-automated-merge-request | ||||
|       changes: *backend-patterns | ||||
|     - <<: *if-security-merge-request | ||||
|       changes: *backend-patterns | ||||
|     - <<: *if-merge-request-not-approved | ||||
|       when: never | ||||
|     - !reference [".rails:rules:ee-and-foss-default-rules", rules] | ||||
|     - changes: *backend-patterns | ||||
| 
 | ||||
| .rails:rules:ee-and-foss-unit:minimal: | ||||
|   rules: | ||||
|     - <<: *if-merge-request-approved | ||||
|       when: never | ||||
|     - <<: *if-automated-merge-request | ||||
|       when: never | ||||
|     - <<: *if-security-merge-request | ||||
|       when: never | ||||
|     - <<: *if-merge-request-labels-run-all-rspec | ||||
|       when: never | ||||
|     - <<: *if-merge-request | ||||
|       changes: *core-backend-patterns | ||||
|       when: never | ||||
|     - <<: *if-merge-request | ||||
|       changes: *ci-patterns | ||||
|       when: never | ||||
|     - !reference [".rails:rules:minimal-default-rules", rules] | ||||
|     - !reference [".rails:rules:unit-integration:minimal-default-rules", rules] | ||||
|     - <<: *if-merge-request | ||||
|       changes: *backend-patterns | ||||
| 
 | ||||
| .rails:rules:ee-and-foss-integration: | ||||
|   rules: | ||||
|     - <<: *if-merge-request-labels-run-all-rspec | ||||
|     - <<: *if-merge-request | ||||
|       changes: *core-backend-patterns | ||||
|     - <<: *if-merge-request | ||||
|       changes: *ci-patterns | ||||
|     - <<: *if-automated-merge-request | ||||
|       changes: *backend-patterns | ||||
|     - <<: *if-security-merge-request | ||||
|       changes: *backend-patterns | ||||
|     - <<: *if-merge-request-not-approved | ||||
|       when: never | ||||
|     - !reference [".rails:rules:ee-and-foss-default-rules", rules] | ||||
|     - changes: *backend-patterns | ||||
| 
 | ||||
| .rails:rules:ee-and-foss-integration:minimal: | ||||
|   rules: | ||||
|     - <<: *if-merge-request-approved | ||||
|       when: never | ||||
|     - <<: *if-automated-merge-request | ||||
|       when: never | ||||
|     - <<: *if-security-merge-request | ||||
|       when: never | ||||
|     - <<: *if-merge-request-labels-run-all-rspec | ||||
|       when: never | ||||
|     - <<: *if-merge-request | ||||
|       changes: *core-backend-patterns | ||||
|       when: never | ||||
|     - <<: *if-merge-request | ||||
|       changes: *ci-patterns | ||||
|       when: never | ||||
|     - !reference [".rails:rules:minimal-default-rules", rules] | ||||
|     - !reference [".rails:rules:unit-integration:minimal-default-rules", rules] | ||||
|     - <<: *if-merge-request | ||||
|       changes: *backend-patterns | ||||
| 
 | ||||
| .rails:rules:ee-and-foss-system: | ||||
|   rules: | ||||
|     - <<: *if-merge-request-labels-run-all-rspec | ||||
|     - <<: *if-merge-request | ||||
|       changes: *core-backend-patterns | ||||
|     - <<: *if-merge-request | ||||
|       changes: *workhorse-patterns | ||||
|     - <<: *if-merge-request | ||||
|       changes: *ci-patterns | ||||
|     - <<: *if-automated-merge-request | ||||
|       changes: *code-backstage-patterns | ||||
|     - <<: *if-security-merge-request | ||||
|       changes: *code-backstage-patterns | ||||
|     - <<: *if-merge-request-not-approved | ||||
|       when: never | ||||
|     - !reference [".rails:rules:system-default-rules", rules] | ||||
|     - changes: *code-backstage-patterns | ||||
| 
 | ||||
| .rails:rules:ee-and-foss-system:minimal: | ||||
|   rules: | ||||
|     - <<: *if-merge-request-approved | ||||
|       when: never | ||||
|     - <<: *if-automated-merge-request | ||||
|       when: never | ||||
|     - <<: *if-security-merge-request | ||||
|       when: never | ||||
|     - <<: *if-merge-request-labels-run-all-rspec | ||||
|       when: never | ||||
|     - <<: *if-merge-request | ||||
|       changes: *core-backend-patterns | ||||
|       when: never | ||||
|     - <<: *if-merge-request | ||||
|       changes: *workhorse-patterns | ||||
|       when: never | ||||
|     - <<: *if-merge-request | ||||
|       changes: *ci-patterns | ||||
|       when: never | ||||
|     - <<: *if-merge-request | ||||
|       changes: *code-backstage-patterns | ||||
|     - !reference [".rails:rules:minimal-default-rules", rules] | ||||
|     - !reference [".rails:rules:system:minimal-default-rules", rules] | ||||
| 
 | ||||
| .rails:rules:ee-and-foss-fast_spec_helper: | ||||
|   rules: | ||||
|  | @ -1011,20 +985,8 @@ | |||
|   rules: | ||||
|     - <<: *if-not-ee | ||||
|       when: never | ||||
|     - <<: *if-merge-request-approved | ||||
|       when: never | ||||
|     - <<: *if-automated-merge-request | ||||
|       when: never | ||||
|     - <<: *if-security-merge-request | ||||
|       when: never | ||||
|     - <<: *if-merge-request-labels-run-all-rspec | ||||
|       when: never | ||||
|     - <<: *if-merge-request | ||||
|       changes: *core-backend-patterns | ||||
|       when: never | ||||
|     - <<: *if-merge-request | ||||
|       changes: *ci-patterns | ||||
|       when: never | ||||
|     - !reference [".rails:rules:minimal-default-rules", rules] | ||||
|     - !reference [".rails:rules:unit-integration:minimal-default-rules", rules] | ||||
|     - <<: *if-merge-request | ||||
|       changes: *db-patterns | ||||
|       when: never | ||||
|  | @ -1033,37 +995,15 @@ | |||
|   rules: | ||||
|     - <<: *if-not-ee | ||||
|       when: never | ||||
|     - <<: *if-merge-request-labels-run-all-rspec | ||||
|     - <<: *if-merge-request | ||||
|       changes: *core-backend-patterns | ||||
|     - <<: *if-merge-request | ||||
|       changes: *ci-patterns | ||||
|     - <<: *if-automated-merge-request | ||||
|       changes: *backend-patterns | ||||
|     - <<: *if-security-merge-request | ||||
|       changes: *backend-patterns | ||||
|     - <<: *if-merge-request-not-approved | ||||
|       when: never | ||||
|     - !reference [".rails:rules:ee-and-foss-default-rules", rules] | ||||
|     - changes: *backend-patterns | ||||
| 
 | ||||
| .rails:rules:ee-only-unit:minimal: | ||||
|   rules: | ||||
|     - <<: *if-not-ee | ||||
|       when: never | ||||
|     - <<: *if-merge-request-approved | ||||
|       when: never | ||||
|     - <<: *if-automated-merge-request | ||||
|       when: never | ||||
|     - <<: *if-security-merge-request | ||||
|       when: never | ||||
|     - <<: *if-merge-request-labels-run-all-rspec | ||||
|       when: never | ||||
|     - <<: *if-merge-request | ||||
|       changes: *core-backend-patterns | ||||
|       when: never | ||||
|     - <<: *if-merge-request | ||||
|       changes: *ci-patterns | ||||
|       when: never | ||||
|     - !reference [".rails:rules:minimal-default-rules", rules] | ||||
|     - !reference [".rails:rules:unit-integration:minimal-default-rules", rules] | ||||
|     - <<: *if-merge-request | ||||
|       changes: *backend-patterns | ||||
| 
 | ||||
|  | @ -1071,37 +1011,15 @@ | |||
|   rules: | ||||
|     - <<: *if-not-ee | ||||
|       when: never | ||||
|     - <<: *if-merge-request-labels-run-all-rspec | ||||
|     - <<: *if-merge-request | ||||
|       changes: *core-backend-patterns | ||||
|     - <<: *if-merge-request | ||||
|       changes: *ci-patterns | ||||
|     - <<: *if-automated-merge-request | ||||
|       changes: *backend-patterns | ||||
|     - <<: *if-security-merge-request | ||||
|       changes: *backend-patterns | ||||
|     - <<: *if-merge-request-not-approved | ||||
|       when: never | ||||
|     - !reference [".rails:rules:ee-and-foss-default-rules", rules] | ||||
|     - changes: *backend-patterns | ||||
| 
 | ||||
| .rails:rules:ee-only-integration:minimal: | ||||
|   rules: | ||||
|     - <<: *if-not-ee | ||||
|       when: never | ||||
|     - <<: *if-merge-request-approved | ||||
|       when: never | ||||
|     - <<: *if-automated-merge-request | ||||
|       when: never | ||||
|     - <<: *if-security-merge-request | ||||
|       when: never | ||||
|     - <<: *if-merge-request-labels-run-all-rspec | ||||
|       when: never | ||||
|     - <<: *if-merge-request | ||||
|       changes: *core-backend-patterns | ||||
|       when: never | ||||
|     - <<: *if-merge-request | ||||
|       changes: *ci-patterns | ||||
|       when: never | ||||
|     - !reference [".rails:rules:minimal-default-rules", rules] | ||||
|     - !reference [".rails:rules:unit-integration:minimal-default-rules", rules] | ||||
|     - <<: *if-merge-request | ||||
|       changes: *backend-patterns | ||||
| 
 | ||||
|  | @ -1109,44 +1027,15 @@ | |||
|   rules: | ||||
|     - <<: *if-not-ee | ||||
|       when: never | ||||
|     - <<: *if-merge-request-labels-run-all-rspec | ||||
|     - <<: *if-merge-request | ||||
|       changes: *core-backend-patterns | ||||
|     - <<: *if-merge-request | ||||
|       changes: *workhorse-patterns | ||||
|     - <<: *if-merge-request | ||||
|       changes: *ci-patterns | ||||
|     - <<: *if-automated-merge-request | ||||
|       changes: *code-backstage-patterns | ||||
|     - <<: *if-security-merge-request | ||||
|       changes: *code-backstage-patterns | ||||
|     - <<: *if-merge-request-not-approved | ||||
|       when: never | ||||
|     - !reference [".rails:rules:system-default-rules", rules] | ||||
|     - changes: *code-backstage-patterns | ||||
| 
 | ||||
| .rails:rules:ee-only-system:minimal: | ||||
|   rules: | ||||
|     - <<: *if-not-ee | ||||
|       when: never | ||||
|     - <<: *if-merge-request-approved | ||||
|       when: never | ||||
|     - <<: *if-automated-merge-request | ||||
|       when: never | ||||
|     - <<: *if-security-merge-request | ||||
|       when: never | ||||
|     - <<: *if-merge-request-labels-run-all-rspec | ||||
|       when: never | ||||
|     - <<: *if-merge-request | ||||
|       changes: *core-backend-patterns | ||||
|       when: never | ||||
|     - <<: *if-merge-request | ||||
|       changes: *workhorse-patterns | ||||
|       when: never | ||||
|     - <<: *if-merge-request | ||||
|       changes: *ci-patterns | ||||
|       when: never | ||||
|     - <<: *if-merge-request | ||||
|       changes: *code-backstage-patterns | ||||
|     - !reference [".rails:rules:minimal-default-rules", rules] | ||||
|     - !reference [".rails:rules:system:minimal-default-rules", rules] | ||||
| 
 | ||||
| .rails:rules:as-if-foss-migration: | ||||
|   rules: | ||||
|  | @ -1172,18 +1061,8 @@ | |||
|   rules: | ||||
|     - <<: *if-not-ee | ||||
|       when: never | ||||
|     - <<: *if-merge-request-approved | ||||
|       when: never | ||||
|     - <<: *if-automated-merge-request | ||||
|       when: never | ||||
|     - <<: *if-security-merge-request | ||||
|       when: never | ||||
|     - <<: *if-merge-request | ||||
|       changes: *core-backend-patterns | ||||
|       when: never | ||||
|     - <<: *if-merge-request | ||||
|       changes: *ci-patterns | ||||
|       when: never | ||||
|     - !reference [".rails:rules:minimal-default-rules", rules] | ||||
|     - !reference [".rails:rules:as-if-foss-migration-unit-integration:minimal-default-rules", rules] | ||||
|     - <<: *if-merge-request-labels-as-if-foss | ||||
|       changes: *db-patterns | ||||
|       when: never | ||||
|  | @ -1192,17 +1071,7 @@ | |||
|   rules: | ||||
|     - <<: *if-not-ee | ||||
|       when: never | ||||
|     - <<: *if-merge-request-labels-run-all-rspec | ||||
|     - <<: *if-merge-request | ||||
|       changes: *core-backend-patterns | ||||
|     - <<: *if-merge-request | ||||
|       changes: *ci-patterns | ||||
|     - <<: *if-automated-merge-request | ||||
|       changes: *backend-patterns | ||||
|     - <<: *if-security-merge-request | ||||
|       changes: *backend-patterns | ||||
|     - <<: *if-merge-request-not-approved | ||||
|       when: never | ||||
|     - !reference [".rails:rules:ee-and-foss-default-rules", rules] | ||||
|     - <<: *if-merge-request-labels-as-if-foss | ||||
|       changes: *backend-patterns | ||||
| 
 | ||||
|  | @ -1210,18 +1079,8 @@ | |||
|   rules: | ||||
|     - <<: *if-not-ee | ||||
|       when: never | ||||
|     - <<: *if-merge-request-approved | ||||
|       when: never | ||||
|     - <<: *if-automated-merge-request | ||||
|       when: never | ||||
|     - <<: *if-security-merge-request | ||||
|       when: never | ||||
|     - <<: *if-merge-request | ||||
|       changes: *core-backend-patterns | ||||
|       when: never | ||||
|     - <<: *if-merge-request | ||||
|       changes: *ci-patterns | ||||
|       when: never | ||||
|     - !reference [".rails:rules:minimal-default-rules", rules] | ||||
|     - !reference [".rails:rules:as-if-foss-migration-unit-integration:minimal-default-rules", rules] | ||||
|     - <<: *if-merge-request-labels-as-if-foss | ||||
|       changes: *backend-patterns | ||||
| 
 | ||||
|  | @ -1229,17 +1088,7 @@ | |||
|   rules: | ||||
|     - <<: *if-not-ee | ||||
|       when: never | ||||
|     - <<: *if-merge-request-labels-run-all-rspec | ||||
|     - <<: *if-merge-request | ||||
|       changes: *core-backend-patterns | ||||
|     - <<: *if-merge-request | ||||
|       changes: *ci-patterns | ||||
|     - <<: *if-automated-merge-request | ||||
|       changes: *backend-patterns | ||||
|     - <<: *if-security-merge-request | ||||
|       changes: *backend-patterns | ||||
|     - <<: *if-merge-request-not-approved | ||||
|       when: never | ||||
|     - !reference [".rails:rules:ee-and-foss-default-rules", rules] | ||||
|     - <<: *if-merge-request-labels-as-if-foss | ||||
|       changes: *backend-patterns | ||||
| 
 | ||||
|  | @ -1247,18 +1096,8 @@ | |||
|   rules: | ||||
|     - <<: *if-not-ee | ||||
|       when: never | ||||
|     - <<: *if-merge-request-approved | ||||
|       when: never | ||||
|     - <<: *if-automated-merge-request | ||||
|       when: never | ||||
|     - <<: *if-security-merge-request | ||||
|       when: never | ||||
|     - <<: *if-merge-request | ||||
|       changes: *core-backend-patterns | ||||
|       when: never | ||||
|     - <<: *if-merge-request | ||||
|       changes: *ci-patterns | ||||
|       when: never | ||||
|     - !reference [".rails:rules:minimal-default-rules", rules] | ||||
|     - !reference [".rails:rules:as-if-foss-migration-unit-integration:minimal-default-rules", rules] | ||||
|     - <<: *if-merge-request-labels-as-if-foss | ||||
|       changes: *backend-patterns | ||||
| 
 | ||||
|  | @ -1266,19 +1105,7 @@ | |||
|   rules: | ||||
|     - <<: *if-not-ee | ||||
|       when: never | ||||
|     - <<: *if-merge-request-labels-run-all-rspec | ||||
|     - <<: *if-merge-request | ||||
|       changes: *core-backend-patterns | ||||
|     - <<: *if-merge-request | ||||
|       changes: *workhorse-patterns | ||||
|     - <<: *if-merge-request | ||||
|       changes: *ci-patterns | ||||
|     - <<: *if-automated-merge-request | ||||
|       changes: *code-backstage-patterns | ||||
|     - <<: *if-security-merge-request | ||||
|       changes: *code-backstage-patterns | ||||
|     - <<: *if-merge-request-not-approved | ||||
|       when: never | ||||
|     - !reference [".rails:rules:system-default-rules", rules] | ||||
|     - <<: *if-merge-request-labels-as-if-foss | ||||
|       changes: *code-backstage-patterns | ||||
| 
 | ||||
|  | @ -1286,12 +1113,7 @@ | |||
|   rules: | ||||
|     - <<: *if-not-ee | ||||
|       when: never | ||||
|     - <<: *if-merge-request-approved | ||||
|       when: never | ||||
|     - <<: *if-automated-merge-request | ||||
|       when: never | ||||
|     - <<: *if-security-merge-request | ||||
|       when: never | ||||
|     - !reference [".rails:rules:minimal-default-rules", rules] | ||||
|     - <<: *if-merge-request | ||||
|       changes: *core-backend-patterns | ||||
|       when: never | ||||
|  | @ -1306,18 +1128,8 @@ | |||
| 
 | ||||
| .rails:rules:as-if-jh-rspec: | ||||
|   rules: | ||||
|     - <<: *if-not-ee | ||||
|       when: never | ||||
|     - <<: *if-jh | ||||
|       when: never | ||||
|     - <<: *if-security-merge-request | ||||
|       when: never | ||||
|     - <<: *if-merge-request-targeting-stable-branch | ||||
|       when: never | ||||
|     - <<: *if-stable-branch-refs | ||||
|       when: never | ||||
|     - <<: *if-merge-request-labels-as-if-jh | ||||
|       allow_failure: true | ||||
|     - !reference [".strict-ee-only-rules", rules] | ||||
|     - !reference [".as-if-jh-default-exclusion-rules", rules] | ||||
|     - <<: *if-merge-request | ||||
|       changes: *ci-patterns | ||||
|       allow_failure: true | ||||
|  | @ -1345,19 +1157,11 @@ | |||
| 
 | ||||
| .rails:rules:detect-previous-failed-tests: | ||||
|   rules: | ||||
|     - <<: *if-security-merge-request | ||||
|       when: never | ||||
|     - <<: *if-merge-request-labels-run-all-rspec | ||||
|     - <<: *if-merge-request | ||||
|       changes: *code-backstage-patterns | ||||
|     - !reference [".rails:rules:previous-failed-tests-default-rules", rules] | ||||
| 
 | ||||
| .rails:rules:rerun-previous-failed-tests: | ||||
|   rules: | ||||
|     - <<: *if-security-merge-request | ||||
|       when: never | ||||
|     - <<: *if-merge-request-labels-run-all-rspec | ||||
|     - <<: *if-merge-request | ||||
|       changes: *code-backstage-patterns | ||||
|     - !reference [".rails:rules:previous-failed-tests-default-rules", rules] | ||||
| 
 | ||||
| .rails:rules:rspec-foss-impact: | ||||
|   rules: | ||||
|  | @ -1418,19 +1222,21 @@ | |||
|     - <<: *if-merge-request | ||||
|       changes: *backend-patterns | ||||
| 
 | ||||
| .rails:rules:default-branch-schedule-nightly--code-backstage: | ||||
| .rails:rules:default-branch-schedule-nightly--code-backstage-default-rules: | ||||
|   rules: | ||||
|     - <<: *if-default-branch-schedule-nightly | ||||
|     - <<: *if-merge-request | ||||
|       changes: [".gitlab/ci/rails.gitlab-ci.yml"] | ||||
| 
 | ||||
| .rails:rules:default-branch-schedule-nightly--code-backstage: | ||||
|   rules: | ||||
|     - !reference [".rails:rules:default-branch-schedule-nightly--code-backstage-default-rules", rules] | ||||
| 
 | ||||
| .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only: | ||||
|   rules: | ||||
|     - <<: *if-not-ee | ||||
|       when: never | ||||
|     - <<: *if-default-branch-schedule-nightly | ||||
|     - <<: *if-merge-request | ||||
|       changes: [".gitlab/ci/rails.gitlab-ci.yml"] | ||||
|     - !reference [".rails:rules:default-branch-schedule-nightly--code-backstage-default-rules", rules] | ||||
| 
 | ||||
| .rails:rules:rspec-feature-flags: | ||||
|   rules: | ||||
|  | @ -1737,7 +1543,6 @@ | |||
|       when: never | ||||
|     - <<: *if-default-branch-or-tag | ||||
|       changes: *code-backstage-qa-patterns | ||||
|       when: on_success | ||||
| 
 | ||||
| .setup:rules:dont-interrupt-me: | ||||
|   rules: | ||||
|  | @ -1752,7 +1557,6 @@ | |||
|   rules: | ||||
|     - <<: *if-default-refs | ||||
|       changes: *code-backstage-patterns | ||||
|       when: on_success | ||||
| 
 | ||||
| .setup:rules:no-ee-check: | ||||
|   rules: | ||||
|  | @ -1760,7 +1564,6 @@ | |||
|       when: never | ||||
|     - <<: *if-default-refs | ||||
|       changes: *code-backstage-patterns | ||||
|       when: on_success | ||||
| 
 | ||||
| .setup:rules:no-jh-check: | ||||
|   rules: | ||||
|  | @ -1768,7 +1571,6 @@ | |||
|       when: never | ||||
|     - <<: *if-default-refs | ||||
|       changes: *code-backstage-patterns | ||||
|       when: on_success | ||||
| 
 | ||||
| .setup:rules:verify-tests-yml: | ||||
|   rules: | ||||
|  | @ -1776,7 +1578,6 @@ | |||
|       when: never | ||||
|     - <<: *if-default-refs | ||||
|       changes: *code-backstage-patterns | ||||
|       when: on_success | ||||
| 
 | ||||
| .setup:rules:generate-frontend-fixtures-mapping: | ||||
|   rules: | ||||
|  | @ -1790,18 +1591,8 @@ | |||
| 
 | ||||
| .setup:rules:add-jh-folder: | ||||
|   rules: | ||||
|     - <<: *if-not-ee | ||||
|       when: never | ||||
|     - <<: *if-jh | ||||
|       when: never | ||||
|     - <<: *if-security-merge-request | ||||
|       when: never | ||||
|     - <<: *if-merge-request-targeting-stable-branch | ||||
|       when: never | ||||
|     - <<: *if-stable-branch-refs | ||||
|       when: never | ||||
|     - <<: *if-merge-request-labels-as-if-jh | ||||
|       allow_failure: true | ||||
|     - !reference [".strict-ee-only-rules", rules] | ||||
|     - !reference [".as-if-jh-default-exclusion-rules", rules] | ||||
|     - <<: *if-merge-request-labels-run-all-rspec | ||||
|       allow_failure: true | ||||
|     - changes: *code-backstage-qa-patterns | ||||
|  |  | |||
|  | @ -1 +1 @@ | |||
| 1.53.0 | ||||
| 1.54.0 | ||||
|  |  | |||
|  | @ -275,6 +275,8 @@ class GfmAutoComplete { | |||
|       UNASSIGN_REVIEWER: '/unassign_reviewer', | ||||
|       REASSIGN: '/reassign', | ||||
|       CC: '/cc', | ||||
|       ATTENTION: '/attention', | ||||
|       REMOVE_ATTENTION: '/remove_attention', | ||||
|     }; | ||||
|     let assignees = []; | ||||
|     let reviewers = []; | ||||
|  | @ -353,6 +355,23 @@ class GfmAutoComplete { | |||
|           } else if (command === MEMBER_COMMAND.UNASSIGN_REVIEWER) { | ||||
|             // Only include members which are not assigned as a reviewer to Issuable currently
 | ||||
|             return data.filter((member) => reviewers.includes(member.search)); | ||||
|           } else if ( | ||||
|             command === MEMBER_COMMAND.ATTENTION || | ||||
|             command === MEMBER_COMMAND.REMOVE_ATTENTION | ||||
|           ) { | ||||
|             const attentionUsers = [ | ||||
|               ...(SidebarMediator.singleton?.store?.assignees || []), | ||||
|               ...(SidebarMediator.singleton?.store?.reviewers || []), | ||||
|             ]; | ||||
|             const attentionRequested = command === MEMBER_COMMAND.REMOVE_ATTENTION; | ||||
| 
 | ||||
|             return data.filter((member) => | ||||
|               attentionUsers.find( | ||||
|                 (u) => | ||||
|                   createMemberSearchString(u).includes(member.search) && | ||||
|                   u.attention_requested === attentionRequested, | ||||
|               ), | ||||
|             ); | ||||
|           } | ||||
| 
 | ||||
|           return data; | ||||
|  |  | |||
|  | @ -123,7 +123,7 @@ export const COVERAGE_FUZZING_CONFIG_HELP_PATH = helpPagePath( | |||
| 
 | ||||
| export const CORPUS_MANAGEMENT_NAME = __('Corpus Management'); | ||||
| export const CORPUS_MANAGEMENT_DESCRIPTION = s__( | ||||
|   'SecurityConfiguration|Manage corpus files used as mutation sources in coverage fuzzing.', | ||||
|   'SecurityConfiguration|Manage corpus files used as seed inputs with coverage-guided fuzzing.', | ||||
| ); | ||||
| export const CORPUS_MANAGEMENT_CONFIG_TEXT = s__('SecurityConfiguration|Manage corpus'); | ||||
| 
 | ||||
|  |  | |||
|  | @ -501,6 +501,8 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo | |||
|       .can_push_to_branch?(@merge_request.source_branch) | ||||
| 
 | ||||
|     access_denied! unless access_check | ||||
| 
 | ||||
|     access_denied! unless merge_request.permits_force_push? | ||||
|   end | ||||
| 
 | ||||
|   def merge_access_check | ||||
|  |  | |||
|  | @ -350,7 +350,7 @@ module Ci | |||
|     # | ||||
|     # ref - The name (or names) of the branch(es)/tag(s) to limit the list of | ||||
|     #       pipelines to. | ||||
|     # sha - The commit SHA (or mutliple SHAs) to limit the list of pipelines to. | ||||
|     # sha - The commit SHA (or multiple SHAs) to limit the list of pipelines to. | ||||
|     # limit - This limits a backlog search, default to 100. | ||||
|     def self.newest_first(ref: nil, sha: nil, limit: 100) | ||||
|       relation = order(id: :desc) | ||||
|  |  | |||
|  | @ -471,6 +471,12 @@ class MergeRequest < ApplicationRecord | |||
|     rebase_jid.present? && Gitlab::SidekiqStatus.running?(rebase_jid) | ||||
|   end | ||||
| 
 | ||||
|   def permits_force_push? | ||||
|     return true unless ProtectedBranch.protected?(source_project, source_branch) | ||||
| 
 | ||||
|     ProtectedBranch.allow_force_push?(source_project, source_branch) | ||||
|   end | ||||
| 
 | ||||
|   # Use this method whenever you need to make sure the head_pipeline is synced with the | ||||
|   # branch head commit, for example checking if a merge request can be merged. | ||||
|   # For more information check: https://gitlab.com/gitlab-org/gitlab-foss/issues/40004 | ||||
|  |  | |||
|  | @ -2779,6 +2779,12 @@ class Project < ApplicationRecord | |||
|   end | ||||
| 
 | ||||
|   def save_topics | ||||
|     topic_ids_before = self.topic_ids | ||||
|     update_topics | ||||
|     Projects::Topic.update_non_private_projects_counter(topic_ids_before, self.topic_ids, visibility_level_previously_was, visibility_level) | ||||
|   end | ||||
| 
 | ||||
|   def update_topics | ||||
|     return if @topic_list.nil? | ||||
| 
 | ||||
|     @topic_list = @topic_list.split(',') if @topic_list.instance_of?(String) | ||||
|  |  | |||
|  | @ -25,6 +25,29 @@ module Projects | |||
|       def search(query) | ||||
|         fuzzy_search(query, [:name]) | ||||
|       end | ||||
| 
 | ||||
|       def update_non_private_projects_counter(ids_before, ids_after, project_visibility_level_before, project_visibility_level_after) | ||||
|         project_visibility_level_before ||= project_visibility_level_after | ||||
| 
 | ||||
|         topics_to_decrement = [] | ||||
|         topics_to_increment = [] | ||||
|         topic_ids_removed = ids_before - ids_after | ||||
|         topic_ids_retained = ids_before & ids_after | ||||
|         topic_ids_added = ids_after - ids_before | ||||
| 
 | ||||
|         if project_visibility_level_before > Gitlab::VisibilityLevel::PRIVATE | ||||
|           topics_to_decrement += topic_ids_removed | ||||
|           topics_to_decrement += topic_ids_retained if project_visibility_level_after == Gitlab::VisibilityLevel::PRIVATE | ||||
|         end | ||||
| 
 | ||||
|         if project_visibility_level_after > Gitlab::VisibilityLevel::PRIVATE | ||||
|           topics_to_increment += topic_ids_added | ||||
|           topics_to_increment += topic_ids_retained if project_visibility_level_before == Gitlab::VisibilityLevel::PRIVATE | ||||
|         end | ||||
| 
 | ||||
|         where(id: topics_to_increment).update_counters(non_private_projects_count: 1) unless topics_to_increment.empty? | ||||
|         where(id: topics_to_decrement).where('non_private_projects_count > 0').update_counters(non_private_projects_count: -1) unless topics_to_decrement.empty? | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -246,7 +246,9 @@ module MergeRequests | |||
|     def remove_all_attention_requests(merge_request) | ||||
|       return unless merge_request.attention_requested_enabled? | ||||
| 
 | ||||
|       ::MergeRequests::BulkRemoveAttentionRequestedService.new(project: merge_request.project, current_user: current_user, merge_request: merge_request).execute | ||||
|       users = merge_request.reviewers + merge_request.assignees | ||||
| 
 | ||||
|       ::MergeRequests::BulkRemoveAttentionRequestedService.new(project: merge_request.project, current_user: current_user, merge_request: merge_request, users: users.uniq).execute | ||||
|     end | ||||
| 
 | ||||
|     def remove_attention_requested(merge_request, user) | ||||
|  |  | |||
|  | @ -3,20 +3,24 @@ | |||
| module MergeRequests | ||||
|   class BulkRemoveAttentionRequestedService < MergeRequests::BaseService | ||||
|     attr_accessor :merge_request | ||||
|     attr_accessor :users | ||||
| 
 | ||||
|     def initialize(project:, current_user:, merge_request:) | ||||
|     def initialize(project:, current_user:, merge_request:, users:) | ||||
|       super(project: project, current_user: current_user) | ||||
| 
 | ||||
|       @merge_request = merge_request | ||||
|       @users = users | ||||
|     end | ||||
| 
 | ||||
|     # rubocop: disable CodeReuse/ActiveRecord | ||||
|     def execute | ||||
|       return error("Invalid permissions") unless can?(current_user, :update_merge_request, merge_request) | ||||
| 
 | ||||
|       merge_request.merge_request_assignees.update_all(state: :reviewed) | ||||
|       merge_request.merge_request_reviewers.update_all(state: :reviewed) | ||||
|       merge_request.merge_request_assignees.where(user_id: users).update_all(state: :reviewed) | ||||
|       merge_request.merge_request_reviewers.where(user_id: users).update_all(state: :reviewed) | ||||
| 
 | ||||
|       success | ||||
|     end | ||||
|     # rubocop: enable CodeReuse/ActiveRecord | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -16,7 +16,7 @@ | |||
|           = _("If you get a lot of false alarms from repository checks, you can clear all repository check information from the database.") | ||||
|           - clear_repository_checks_link = _('Clear all repository checks') | ||||
|           - clear_repository_checks_message = _('This clears repository check states for all projects in the database and cannot be undone. Are you sure?') | ||||
|         = link_to clear_repository_checks_link, clear_repository_check_states_admin_application_settings_path, data: { confirm: clear_repository_checks_message }, method: :put, class: "gl-button btn btn-sm btn-danger gl-mt-3" | ||||
|         = link_to clear_repository_checks_link, clear_repository_check_states_admin_application_settings_path, data: { confirm: clear_repository_checks_message, confirm_btn_variant: 'danger' }, aria: { label: _('Clear repository checks') }, method: :put, class: "gl-button btn btn-sm btn-danger gl-mt-3" | ||||
| 
 | ||||
|     .sub-section | ||||
|       %h4= _("Housekeeping") | ||||
|  |  | |||
|  | @ -282,6 +282,15 @@ | |||
|   :weight: 1 | ||||
|   :idempotent: true | ||||
|   :tags: [] | ||||
| - :name: cronjob:container_registry_migration_observer | ||||
|   :worker_name: ContainerRegistry::Migration::ObserverWorker | ||||
|   :feature_category: :container_registry | ||||
|   :has_external_dependencies: | ||||
|   :urgency: :low | ||||
|   :resource_boundary: :unknown | ||||
|   :weight: 1 | ||||
|   :idempotent: true | ||||
|   :tags: [] | ||||
| - :name: cronjob:database_batched_background_migration | ||||
|   :worker_name: Database::BatchedBackgroundMigrationWorker | ||||
|   :feature_category: :database | ||||
|  |  | |||
|  | @ -0,0 +1,40 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module ContainerRegistry | ||||
|   module Migration | ||||
|     class ObserverWorker | ||||
|       include ApplicationWorker | ||||
|       # This worker does not perform work scoped to a context | ||||
|       include CronjobQueue # rubocop:disable Scalability/CronWorkerContext | ||||
| 
 | ||||
|       COUNT_BATCH_SIZE = 50000 | ||||
| 
 | ||||
|       data_consistency :sticky | ||||
|       feature_category :container_registry | ||||
|       urgency :low | ||||
|       deduplicate :until_executed, including_scheduled: true | ||||
|       idempotent! | ||||
| 
 | ||||
|       def perform | ||||
|         return unless ::ContainerRegistry::Migration.enabled? | ||||
| 
 | ||||
|         use_replica_if_available do | ||||
|           ContainerRepository::MIGRATION_STATES.each do |state| | ||||
|             relation = ContainerRepository.with_migration_state(state) | ||||
|             count = ::Gitlab::Database::BatchCount.batch_count( | ||||
|               relation, batch_size: COUNT_BATCH_SIZE | ||||
|             ) | ||||
|             name = "#{state}_count".to_sym | ||||
|             log_extra_metadata_on_done(name, count) | ||||
|           end | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       private | ||||
| 
 | ||||
|       def use_replica_if_available(&block) | ||||
|         ::Gitlab::Database::LoadBalancing::Session.current.use_replicas_for_read_queries(&block) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -16,7 +16,15 @@ module Projects | |||
|     def before_gitaly_call(task, resource) | ||||
|       return unless gc?(task) | ||||
| 
 | ||||
|       ::Projects::GitDeduplicationService.new(resource).execute | ||||
|       # Don't block garbage collection if we can't fetch into an object pool | ||||
|       # due to some gRPC error because we don't want to accumulate cruft. | ||||
|       # See https://gitlab.com/gitlab-org/gitaly/-/issues/4022. | ||||
|       begin | ||||
|         ::Projects::GitDeduplicationService.new(resource).execute | ||||
|       rescue Gitlab::Git::CommandTimedOut, GRPC::Internal => e | ||||
|         Gitlab::ErrorTracking.track_exception(e) | ||||
|       end | ||||
| 
 | ||||
|       cleanup_orphan_lfs_file_references(resource) | ||||
|     end | ||||
| 
 | ||||
|  |  | |||
|  | @ -4,8 +4,8 @@ const path = require('path'); | |||
| const { History, HistoryWithTTL } = require('./history'); | ||||
| const log = require('./log'); | ||||
| 
 | ||||
| const onRequestEntryPoint = (app, callback) => { | ||||
|   app.use((req, res, next) => { | ||||
| const onRequestEntryPoint = (callback) => { | ||||
|   return (req, res, next) => { | ||||
|     const fileName = path.basename(req.url); | ||||
| 
 | ||||
|     /** | ||||
|  | @ -20,7 +20,7 @@ const onRequestEntryPoint = (app, callback) => { | |||
|     } | ||||
| 
 | ||||
|     next(); | ||||
|   }); | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  | @ -40,7 +40,9 @@ class NoopCompiler { | |||
|   logStatus() {} | ||||
| 
 | ||||
|   // eslint-disable-next-line class-methods-use-this
 | ||||
|   setupMiddleware() {} | ||||
|   createMiddleware() { | ||||
|     return null; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  | @ -55,8 +57,8 @@ class HistoryOnlyCompiler extends NoopCompiler { | |||
|     this.history = new History(historyFilePath); | ||||
|   } | ||||
| 
 | ||||
|   setupMiddleware(app) { | ||||
|     onRequestEntryPoint(app, (entryPoint) => { | ||||
|   createMiddleware() { | ||||
|     return onRequestEntryPoint((entryPoint) => { | ||||
|       this.history.onRequestEntryPoint(entryPoint); | ||||
|     }); | ||||
|   } | ||||
|  | @ -92,16 +94,16 @@ class IncrementalWebpackCompiler { | |||
|     log(`Currently compiling route entrypoints: ${this.history.size} of ${totalCount}`); | ||||
|   } | ||||
| 
 | ||||
|   setupMiddleware(app, server) { | ||||
|     onRequestEntryPoint(app, (entryPoint) => { | ||||
|   createMiddleware(devServer) { | ||||
|     return onRequestEntryPoint((entryPoint) => { | ||||
|       const wasVisitedRecently = this.history.onRequestEntryPoint(entryPoint); | ||||
|       if (!wasVisitedRecently) { | ||||
|         log(`Have not visited ${entryPoint} recently. Adding to compilation.`); | ||||
| 
 | ||||
|         setTimeout(() => { | ||||
|           server.middleware.invalidate(() => { | ||||
|             if (server.sockets) { | ||||
|               server.sockWrite(server.sockets, 'content-changed'); | ||||
|           devServer.invalidate(() => { | ||||
|             if (devServer.sockets) { | ||||
|               devServer.sendMessage(devServer.webSocketServer.clients, 'static-changed'); | ||||
|             } | ||||
|           }); | ||||
|         }, TIMEOUT); | ||||
|  |  | |||
|  | @ -542,6 +542,9 @@ Settings.cron_jobs['container_expiration_policy_worker']['job_class'] = 'Contain | |||
| Settings.cron_jobs['container_registry_migration_guard_worker'] ||= Settingslogic.new({}) | ||||
| Settings.cron_jobs['container_registry_migration_guard_worker']['cron'] ||= '*/10 * * * *' | ||||
| Settings.cron_jobs['container_registry_migration_guard_worker']['job_class'] = 'ContainerRegistry::Migration::GuardWorker' | ||||
| Settings.cron_jobs['container_registry_migration_observer_worker'] ||= Settingslogic.new({}) | ||||
| Settings.cron_jobs['container_registry_migration_observer_worker']['cron'] ||= '*/30 * * * *' | ||||
| Settings.cron_jobs['container_registry_migration_observer_worker']['job_class'] = 'ContainerRegistry::Migration::ObserverWorker' | ||||
| Settings.cron_jobs['image_ttl_group_policy_worker'] ||= Settingslogic.new({}) | ||||
| Settings.cron_jobs['image_ttl_group_policy_worker']['cron'] ||= '40 0 * * *' | ||||
| Settings.cron_jobs['image_ttl_group_policy_worker']['job_class'] = 'DependencyProxy::ImageTtlGroupPolicyWorker' | ||||
|  |  | |||
|  | @ -0,0 +1,25 @@ | |||
| --- | ||||
| key_path: redis_hll_counters.quickactions.i_quickactions_attention_monthly | ||||
| description: Count of MAU using the `/attention` quick action | ||||
| product_section: dev | ||||
| product_stage: create | ||||
| product_group: group::code review | ||||
| product_category: code_review | ||||
| value_type: number | ||||
| status: active | ||||
| milestone: '14.8' | ||||
| introduced_by_url: | ||||
| time_frame: 28d | ||||
| data_source: redis_hll | ||||
| data_category: optional | ||||
| instrumentation_class: RedisHLLMetric | ||||
| options: | ||||
|   events: | ||||
|     - i_quickactions_attention | ||||
| distribution: | ||||
|   - ce | ||||
|   - ee | ||||
| tier: | ||||
|   - free | ||||
|   - premium | ||||
|   - ultimate | ||||
|  | @ -0,0 +1,25 @@ | |||
| --- | ||||
| key_path: redis_hll_counters.quickactions.i_quickactions_remove_attention_monthly | ||||
| description: Count of MAU using the `/remove_attention` quick action | ||||
| product_section: dev | ||||
| product_stage: create | ||||
| product_group: group::code review | ||||
| product_category: code_review | ||||
| value_type: number | ||||
| status: active | ||||
| milestone: '14.8' | ||||
| introduced_by_url: | ||||
| time_frame: 28d | ||||
| data_source: redis_hll | ||||
| data_category: optional | ||||
| instrumentation_class: RedisHLLMetric | ||||
| options: | ||||
|   events: | ||||
|     - i_quickactions_remove_attention | ||||
| distribution: | ||||
|   - ce | ||||
|   - ee | ||||
| tier: | ||||
|   - free | ||||
|   - premium | ||||
|   - ultimate | ||||
|  | @ -0,0 +1,25 @@ | |||
| --- | ||||
| key_path: redis_hll_counters.quickactions.i_quickactions_attention_weekly | ||||
| description: Count of WAU using the `/attention` quick action | ||||
| product_section: dev | ||||
| product_stage: create | ||||
| product_group: group::code review | ||||
| product_category: code_review | ||||
| value_type: number | ||||
| status: active | ||||
| milestone: '14.8' | ||||
| introduced_by_url: | ||||
| time_frame: 7d | ||||
| data_source: redis_hll | ||||
| data_category: optional | ||||
| instrumentation_class: RedisHLLMetric | ||||
| options: | ||||
|   events: | ||||
|     - i_quickactions_attention | ||||
| distribution: | ||||
|   - ce | ||||
|   - ee | ||||
| tier: | ||||
|   - free | ||||
|   - premium | ||||
|   - ultimate | ||||
|  | @ -0,0 +1,25 @@ | |||
| --- | ||||
| key_path: redis_hll_counters.quickactions.i_quickactions_remove_attention_weekly | ||||
| description: Count of WAU using the `/remove_attention` quick action | ||||
| product_section: dev | ||||
| product_stage: create | ||||
| product_group: group::code review | ||||
| product_category: code_review | ||||
| value_type: number | ||||
| status: active | ||||
| milestone: '14.8' | ||||
| introduced_by_url: | ||||
| time_frame: 7d | ||||
| data_source: redis_hll | ||||
| data_category: optional | ||||
| instrumentation_class: RedisHLLMetric | ||||
| options: | ||||
|   events: | ||||
|     - i_quickactions_remove_attention | ||||
| distribution: | ||||
|   - ce | ||||
|   - ee | ||||
| tier: | ||||
|   - free | ||||
|   - premium | ||||
|   - ultimate | ||||
|  | @ -38,11 +38,10 @@ const SUPPORTED_BROWSERS_HASH = crypto | |||
| const VENDOR_DLL = process.env.WEBPACK_VENDOR_DLL && process.env.WEBPACK_VENDOR_DLL !== 'false'; | ||||
| const CACHE_PATH = process.env.WEBPACK_CACHE_PATH || path.join(ROOT_PATH, 'tmp/cache'); | ||||
| const IS_PRODUCTION = process.env.NODE_ENV === 'production'; | ||||
| const IS_DEV_SERVER = process.env.WEBPACK_DEV_SERVER === 'true'; | ||||
| const IS_DEV_SERVER = process.env.WEBPACK_SERVE === 'true'; | ||||
| 
 | ||||
| const DEV_SERVER_HOST = process.env.DEV_SERVER_HOST || 'localhost'; | ||||
| const DEV_SERVER_PORT = parseInt(process.env.DEV_SERVER_PORT, 10) || 3808; | ||||
| const { DEV_SERVER_PUBLIC_ADDR } = process.env; | ||||
| const { DEV_SERVER_HOST, DEV_SERVER_PUBLIC_ADDR } = process.env; | ||||
| const DEV_SERVER_PORT = parseInt(process.env.DEV_SERVER_PORT, 10); | ||||
| const DEV_SERVER_ALLOWED_HOSTS = | ||||
|   process.env.DEV_SERVER_ALLOWED_HOSTS && process.env.DEV_SERVER_ALLOWED_HOSTS.split(','); | ||||
| const DEV_SERVER_HTTPS = process.env.DEV_SERVER_HTTPS && process.env.DEV_SERVER_HTTPS !== 'false'; | ||||
|  | @ -654,9 +653,6 @@ module.exports = { | |||
|       }, | ||||
|     }, | ||||
| 
 | ||||
|     // enable HMR only in webpack-dev-server
 | ||||
|     DEV_SERVER_LIVERELOAD && new webpack.HotModuleReplacementPlugin(), | ||||
| 
 | ||||
|     // optionally generate webpack bundle analysis
 | ||||
|     WEBPACK_REPORT && | ||||
|       new BundleAnalyzerPlugin({ | ||||
|  | @ -689,19 +685,38 @@ module.exports = { | |||
|     */ | ||||
|     new webpack.IgnorePlugin(/moment/, /pikaday/), | ||||
|   ].filter(Boolean), | ||||
| 
 | ||||
|   devServer: { | ||||
|     before(app, server) { | ||||
|       incrementalCompiler.setupMiddleware(app, server); | ||||
|     setupMiddlewares: (middlewares, devServer) => { | ||||
|       if (!devServer) { | ||||
|         throw new Error('webpack-dev-server is not defined'); | ||||
|       } | ||||
| 
 | ||||
|       const incrementalCompilerMiddleware = incrementalCompiler.createMiddleware(devServer); | ||||
| 
 | ||||
|       if (incrementalCompilerMiddleware) { | ||||
|         middlewares.unshift(incrementalCompilerMiddleware); | ||||
|       } | ||||
| 
 | ||||
|       return middlewares; | ||||
|     }, | ||||
|     host: DEV_SERVER_HOST, | ||||
|     port: DEV_SERVER_PORT, | ||||
|     public: DEV_SERVER_PUBLIC_ADDR, | ||||
|     allowedHosts: DEV_SERVER_ALLOWED_HOSTS, | ||||
|     // Only print errors to CLI
 | ||||
|     devMiddleware: { | ||||
|       stats: 'errors-only', | ||||
|     }, | ||||
|     host: DEV_SERVER_HOST || 'localhost', | ||||
|     port: DEV_SERVER_PORT || 3808, | ||||
|     https: DEV_SERVER_HTTPS, | ||||
|     contentBase: false, | ||||
|     stats: 'errors-only', | ||||
|     hot: DEV_SERVER_LIVERELOAD, | ||||
|     inline: DEV_SERVER_LIVERELOAD, | ||||
|     // The following settings are mainly needed for HMR support in gitpod.
 | ||||
|     // Per default only local hosts are allowed, but here we could
 | ||||
|     // allow different hosts (e.g. ['.gitpod'], all of gitpod),
 | ||||
|     // as the webpack server will run on a different subdomain than
 | ||||
|     // the rails application
 | ||||
|     ...(DEV_SERVER_ALLOWED_HOSTS ? { allowedHosts: DEV_SERVER_ALLOWED_HOSTS } : {}), | ||||
|     client: { | ||||
|       ...(DEV_SERVER_PUBLIC_ADDR ? { webSocketURL: DEV_SERVER_PUBLIC_ADDR } : {}), | ||||
|     }, | ||||
|   }, | ||||
| 
 | ||||
|   devtool: NO_SOURCEMAPS ? false : devtool, | ||||
|  |  | |||
|  | @ -0,0 +1,11 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class AddTopicsNonPrivateProjectsCount < Gitlab::Database::Migration[1.0] | ||||
|   def up | ||||
|     add_column :topics, :non_private_projects_count, :bigint, null: false, default: 0 | ||||
|   end | ||||
| 
 | ||||
|   def down | ||||
|     remove_column :topics, :non_private_projects_count | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1,15 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class AddTopicsNonPrivateProjectsCountIndex < Gitlab::Database::Migration[1.0] | ||||
|   INDEX_NAME = 'index_topics_non_private_projects_count' | ||||
| 
 | ||||
|   disable_ddl_transaction! | ||||
| 
 | ||||
|   def up | ||||
|     add_concurrent_index :topics, [:non_private_projects_count, :id], order: { non_private_projects_count: :desc }, name: INDEX_NAME | ||||
|   end | ||||
| 
 | ||||
|   def down | ||||
|     remove_concurrent_index_by_name :topics, INDEX_NAME | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1,16 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class AddTmpIndexOnIdAndMigrationStateToContainerRepositories < Gitlab::Database::Migration[1.0] | ||||
|   INDEX_NAME = 'tmp_index_container_repositories_on_id_migration_state' | ||||
| 
 | ||||
|   disable_ddl_transaction! | ||||
| 
 | ||||
|   # Temporary index to be removed https://gitlab.com/gitlab-org/gitlab/-/issues/351783 | ||||
|   def up | ||||
|     add_concurrent_index :container_repositories, [:id, :migration_state], name: INDEX_NAME | ||||
|   end | ||||
| 
 | ||||
|   def down | ||||
|     remove_concurrent_index_by_name :container_repositories, INDEX_NAME | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1,23 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class SchedulePopulateTopicsNonPrivateProjectsCount < Gitlab::Database::Migration[1.0] | ||||
|   MIGRATION = 'PopulateTopicsNonPrivateProjectsCount' | ||||
|   BATCH_SIZE = 10_000 | ||||
|   DELAY_INTERVAL = 2.minutes | ||||
| 
 | ||||
|   disable_ddl_transaction! | ||||
| 
 | ||||
|   def up | ||||
|     queue_background_migration_jobs_by_range_at_intervals( | ||||
|       define_batchable_model('topics'), | ||||
|       MIGRATION, | ||||
|       DELAY_INTERVAL, | ||||
|       batch_size: BATCH_SIZE, | ||||
|       track_jobs: true | ||||
|     ) | ||||
|   end | ||||
| 
 | ||||
|   def down | ||||
|     # no-op | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1 @@ | |||
| 26600e01d8b31a4308d0e23564e4d4c52488ec87ad7990a410b7cc0c031f12e7 | ||||
|  | @ -0,0 +1 @@ | |||
| 51c7ab860b952281bd7f65d68e7a539a8eee57cac3bbdaf439ff5593f5b065ed | ||||
|  | @ -0,0 +1 @@ | |||
| 7740d1e71571576a709ae5bfd46f60ea3fb4be3f48cddec2cca53f148096cdd7 | ||||
|  | @ -0,0 +1 @@ | |||
| 0efe482aa626cf80912feaa1176837253b094fc434f273bee35b5fe3e8ce4243 | ||||
|  | @ -20272,6 +20272,7 @@ CREATE TABLE topics ( | |||
|     avatar text, | ||||
|     description text, | ||||
|     total_projects_count bigint DEFAULT 0 NOT NULL, | ||||
|     non_private_projects_count bigint DEFAULT 0 NOT NULL, | ||||
|     CONSTRAINT check_26753fb43a CHECK ((char_length(avatar) <= 255)), | ||||
|     CONSTRAINT check_5d1a07c8c8 CHECK ((char_length(description) <= 1024)), | ||||
|     CONSTRAINT check_7a90d4c757 CHECK ((char_length(name) <= 255)) | ||||
|  | @ -27935,6 +27936,8 @@ CREATE UNIQUE INDEX index_token_with_ivs_on_hashed_plaintext_token ON token_with | |||
| 
 | ||||
| CREATE UNIQUE INDEX index_token_with_ivs_on_hashed_token ON token_with_ivs USING btree (hashed_token); | ||||
| 
 | ||||
| CREATE INDEX index_topics_non_private_projects_count ON topics USING btree (non_private_projects_count DESC, id); | ||||
| 
 | ||||
| CREATE UNIQUE INDEX index_topics_on_name ON topics USING btree (name); | ||||
| 
 | ||||
| CREATE INDEX index_topics_on_name_trigram ON topics USING gin (name gin_trgm_ops); | ||||
|  | @ -28317,6 +28320,8 @@ CREATE INDEX tmp_idx_deduplicate_vulnerability_occurrences ON vulnerability_occu | |||
| 
 | ||||
| CREATE INDEX tmp_idx_vulnerability_occurrences_on_id_where_report_type_7_99 ON vulnerability_occurrences USING btree (id) WHERE (report_type = ANY (ARRAY[7, 99])); | ||||
| 
 | ||||
| CREATE INDEX tmp_index_container_repositories_on_id_migration_state ON container_repositories USING btree (id, migration_state); | ||||
| 
 | ||||
| CREATE INDEX tmp_index_for_namespace_id_migration_on_routes ON routes USING btree (id) WHERE ((namespace_id IS NULL) AND ((source_type)::text = 'Namespace'::text)); | ||||
| 
 | ||||
| CREATE INDEX tmp_index_members_on_state ON members USING btree (state) WHERE (state = 2); | ||||
|  |  | |||
|  | @ -990,7 +990,7 @@ POST /projects/:id/merge_requests/:merge_request_iid/approve | |||
| | `approval_password` | string  | no      | Current user's password. Required if [**Require user password to approve**](../user/project/merge_requests/approvals/settings.md#require-user-password-to-approve) is enabled in the project settings. | | ||||
| 
 | ||||
| The `sha` parameter works in the same way as | ||||
| when [accepting a merge request](merge_requests.md#accept-mr): if it is passed, then it must | ||||
| when [accepting a merge request](merge_requests.md#merge-a-merge-request): if it is passed, then it must | ||||
| match the current HEAD of the merge request for the approval to be added. If it | ||||
| does not match, the response code is `409`. | ||||
| 
 | ||||
|  |  | |||
|  | @ -1454,9 +1454,9 @@ DELETE /projects/:id/merge_requests/:merge_request_iid | |||
| curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/4/merge_requests/85" | ||||
| ``` | ||||
| 
 | ||||
| ## Accept MR | ||||
| ## Merge a merge request | ||||
| 
 | ||||
| Merge changes submitted with MR using this API. | ||||
| Accept and merge changes submitted with MR using this API. | ||||
| 
 | ||||
| If a merge request is unable to be accepted (such as Draft, Closed, Pipeline Pending Completion, or Failed while requiring Success) - you receive a `405` and the error message 'Method Not Allowed' | ||||
| 
 | ||||
|  |  | |||
|  | @ -18,21 +18,27 @@ to track custom events. | |||
| 
 | ||||
| For the recommended frontend tracking implementation, see [Usage recommendations](#usage-recommendations). | ||||
| 
 | ||||
| Tracking implementations must have an `action` and a `category`. You can provide additional | ||||
| categories from the [structured event taxonomy](index.md#structured-event-taxonomy) with an `extra` object | ||||
| that accepts key-value pairs. | ||||
| Structured events and page views include the [`gitlab_standard`](schemas.md#gitlab_standard) | ||||
| context, using the `window.gl.snowplowStandardContext` object which includes | ||||
| [default data](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/views/layouts/_snowplow.html.haml) | ||||
| as base. This object can be modified for any subsequent structured event fired, | ||||
| although it's not recommended. | ||||
| 
 | ||||
| | Field      | Type   | Default value              | Description                                                                                                                                                                                                    | | ||||
| Tracking implementations must have an `action` and a `category`. You can provide additional | ||||
| properties from the [structured event taxonomy](index.md#structured-event-taxonomy), in | ||||
| addition to an `extra` object that accepts key-value pairs. | ||||
| 
 | ||||
| | Property      | Type   | Default value              | Description                                                                                                                                                                                                    | | ||||
| |:-----------|:-------|:---------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | ||||
| | `category` | string | `document.body.dataset.page` | Page or subsection of a page in which events are captured.                                                                                                                                            | | ||||
| | `action`   | string | generic                  | Action the user is taking. Clicks must be `click` and activations must be `activate`. For example, focusing a form field is `activate_form_input`, and clicking a button is `click_button`. | | ||||
| | `data`     | object | `{}`                         | Additional data such as `label`, `property`, `value`, `context` as described in [Structured event taxonomy](index.md#structured-event-taxonomy), and `extra` (key-value pairs object). | | ||||
| | `action`   | string | `'generic'`                  | Action the user is taking. Clicks must be `click` and activations must be `activate`. For example, focusing a form field is `activate_form_input`, and clicking a button is `click_button`. | | ||||
| | `data`     | object | `{}`                         | Additional data such as `label`, `property`, `value` as described in [Structured event taxonomy](index.md#structured-event-taxonomy), `context` for custom contexts, and `extra` (key-value pairs object). | | ||||
| 
 | ||||
| ### Usage recommendations | ||||
| 
 | ||||
| - Use [data attributes](#implement-data-attribute-tracking) on HTML elements that emit `click`, `show.bs.dropdown`, or `hide.bs.dropdown` events. | ||||
| - Use the [Vue mixin](#implement-vue-component-tracking) for tracking custom events, or if the supported events for data attributes are not propagating. | ||||
| - Use the [tracking class](#implement-raw-javascript-tracking) when tracking raw JavaScript files. | ||||
| - Use the [Vue mixin](#implement-vue-component-tracking) for tracking custom events, or if the supported events for data attributes are not propagating. For example, clickable components that don't emit `click`. | ||||
| - Use the [tracking class](#implement-raw-javascript-tracking) when tracking in vanilla JavaScript files. | ||||
| 
 | ||||
| ### Implement data attribute tracking | ||||
| 
 | ||||
|  | @ -41,7 +47,10 @@ To implement tracking for HAML or Vue templates, add a [`data-track` attribute]( | |||
| The following example shows `data-track-*` attributes assigned to a button: | ||||
| 
 | ||||
| ```haml | ||||
| %button.btn{ data: { track_action: "click_button", track_label: "template_preview", track_property: "my-template" } } | ||||
| %button.btn{ data: { track: { action: "click_button", label: "template_preview", property: "my-template" } } } | ||||
| 
 | ||||
| // or | ||||
| // %button.btn{ data: { track_action: "click_button", track_label: "template_preview", track_property: "my-template" } } | ||||
| ``` | ||||
| 
 | ||||
| ```html | ||||
|  | @ -62,7 +71,7 @@ The following example shows `data-track-*` attributes assigned to a button: | |||
| | `data-track-property` | false    | Any additional property of the element, or object being acted on. | | ||||
| | `data-track-value`    | false    | Describes a numeric value (decimal) directly related to the event. This could be the value of an input. For example, `10` when clicking `internal` visibility. If omitted, this is the element's `value` property or `undefined`. For checkboxes, the default value is the element's checked attribute or `0` when unchecked. | | ||||
| | `data-track-extra` | false    | A key-value pair object passed as a valid JSON string. This attribute is added to the `extra` property in our [`gitlab_standard`](schemas.md#gitlab_standard) schema. | | ||||
| | `data-track-context`  | false    | The `context` as described in our [Structured event taxonomy](index.md#structured-event-taxonomy). | | ||||
| | `data-track-context`  | false    | To append a custom context object, passed as a valid JSON string. | | ||||
| 
 | ||||
| #### Event listeners | ||||
| 
 | ||||
|  | @ -74,12 +83,19 @@ If click events stop propagating, you must implement listeners and [Vue componen | |||
| 
 | ||||
| #### Helper methods | ||||
| 
 | ||||
| Use the following Ruby helper: | ||||
| You can use the following Ruby helper: | ||||
| 
 | ||||
| ```ruby | ||||
| tracking_attrs(label, action, property) # { data: { track_label... } } | ||||
| ``` | ||||
| 
 | ||||
| You can also use it on HAML templates: | ||||
| 
 | ||||
| ```haml | ||||
| %button{ **tracking_attrs('main_navigation', 'click_button', 'navigation') } | ||||
| 
 | ||||
| // When adding additional data | ||||
| // %button{ data: { platform: "...", **tracking_attrs('main_navigation', 'click_button', 'navigation') } } | ||||
| ``` | ||||
| 
 | ||||
| If you use the GitLab helper method [`nav_link`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/helpers/tab_helper.rb#L76), you must wrap `html_options` under the `html_options` keyword argument. If you | ||||
|  | @ -91,7 +107,7 @@ use the `ActionView` helper method [`link_to`](https://api.rubyonrails.org/v5.2. | |||
| track_action: "click_button" }) | ||||
| 
 | ||||
| # Good | ||||
| = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { data: { track_label:  | ||||
| = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { data: { track_label: | ||||
| "explore_groups", track_action: "click_button" } }) | ||||
| 
 | ||||
| # Good (other helpers) | ||||
|  | @ -101,63 +117,64 @@ track_action: "click_button" }) | |||
| 
 | ||||
| ### Implement Vue component tracking | ||||
| 
 | ||||
| For custom event tracking, use a Vue `mixin` in components. Vue `mixin` exposes the `Tracking.event` | ||||
| static method and the `track` method. You can specify tracking options in `data` or `computed`. | ||||
| These options override any defaults and allow the values to be dynamic from props or based on state. | ||||
| For custom event tracking, use the [Vue mixin](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/javascripts/tracking/tracking.js#L207). It exposes `Tracking.event` as the `track` method. | ||||
| You can specify tracking options by creating a `tracking` data object or | ||||
| computed property, and as a second parameter: `this.track('click_button', opts)`. | ||||
| These options override any defaults and allow the values to be dynamic from props or based on state: | ||||
| 
 | ||||
| Several default options are passed when an event is tracked from the component: | ||||
| 
 | ||||
| - `category`: If you don't specify, by default `document.body.dataset.page` is used. | ||||
| - `label` | ||||
| - `property` | ||||
| - `value` | ||||
| | Property | Type | Default | Example | | ||||
| | -- | -- | -- | -- | | ||||
| | `category` | string | `document.body.dataset.page` | `'code_quality_walkthrough'` | | ||||
| | `label` | string | `''` | `'process_start_button'` | | ||||
| | `property` | string | `''` | `'asc'` or `'desc'` | | ||||
| | `value` | integer | `undefined` | `0`, `1`, `500` | | ||||
| | `extra` | object | `{}` | `{ selectedVariant: this.variant }` | | ||||
| 
 | ||||
| To implement Vue component tracking: | ||||
| 
 | ||||
| 1. Import the `Tracking` library and request a `mixin`: | ||||
| 1. Import the `Tracking` library and call the `mixin` method: | ||||
| 
 | ||||
|     ```javascript | ||||
|     import Tracking from '~/tracking'; | ||||
|     const trackingMixin = Tracking.mixin; | ||||
| 
 | ||||
|     const trackingMixin = Tracking.mixin(); | ||||
| 
 | ||||
|     // Optionally provide default properties | ||||
|     // const trackingMixin = Tracking.mixin({ label: 'right_sidebar' }); | ||||
|     ``` | ||||
| 
 | ||||
| 1. Provide categories to track the event from the component. For example, to track all events in a | ||||
| component with a label, use the `label` category: | ||||
| 
 | ||||
|     ```javascript | ||||
|     import Tracking from '~/tracking'; | ||||
|     const trackingMixin = Tracking.mixin({ label: 'right_sidebar' }); | ||||
|     ``` | ||||
| 
 | ||||
| 1. In the component, declare the Vue `mixin`: | ||||
| 1. Use the mixin in the component: | ||||
| 
 | ||||
|     ```javascript | ||||
|     export default { | ||||
|       mixins: [trackingMixin], | ||||
|       // ...[component implementation]... | ||||
|       // Or | ||||
|       // mixins: [Tracking.mixin()], | ||||
|       // mixins: [Tracking.mixin({ label: 'right_sidebar' })], | ||||
| 
 | ||||
|       data() { | ||||
|         return { | ||||
|           expanded: false, | ||||
|           tracking: { | ||||
|             label: 'left_sidebar', | ||||
|           }, | ||||
|         }; | ||||
|       }, | ||||
|     }; | ||||
|     ``` | ||||
| 
 | ||||
| 1. To receive event data as a tracking object or computed property: | ||||
|    - Declare it in the `data` function. Use a `tracking` object when default event properties are dynamic or provided at runtime: | ||||
| 1. You can specify tracking options in by creating a `tracking` data object | ||||
| or computed property: | ||||
| 
 | ||||
|       ```javascript | ||||
|       export default { | ||||
|         name: 'RightSidebar', | ||||
| 
 | ||||
|         mixins: [Tracking.mixin()], | ||||
| 
 | ||||
|         data() { | ||||
|           return { | ||||
|             expanded: false, | ||||
|             variant: '', | ||||
|             tracking: { | ||||
|               label: 'right_sidebar', | ||||
|               // category: '', | ||||
|               // property: '', | ||||
|               // value: '', | ||||
|               // experiment: '', | ||||
|  | @ -165,18 +182,28 @@ component with a label, use the `label` category: | |||
|             }, | ||||
|           }; | ||||
|         }, | ||||
| 
 | ||||
|         // Or | ||||
|         // computed: { | ||||
|         //   tracking() { | ||||
|         //     return { | ||||
|         //       property: this.variant, | ||||
|         //       extra: { expanded: this.expanded }, | ||||
|         //     }; | ||||
|         //   }, | ||||
|         // }, | ||||
|       }; | ||||
|       ``` | ||||
| 
 | ||||
|    - Declare it in the event data in the `track` function. This object merges with any previously provided options: | ||||
| 1. Call the `track` method. Tracking options can be passed as the second parameter: | ||||
| 
 | ||||
|       ```javascript | ||||
|       this.track('click_button', { | ||||
|         label: 'right_sidebar', | ||||
|       }); | ||||
|       ``` | ||||
|     ```javascript | ||||
|     this.track('click_button', { | ||||
|       label: 'right_sidebar', | ||||
|     }); | ||||
|     ``` | ||||
| 
 | ||||
| 1. Optional. Use the `track` method in a template: | ||||
|     Or use the `track` method in the template: | ||||
| 
 | ||||
|     ```html | ||||
|     <template> | ||||
|  | @ -185,55 +212,67 @@ component with a label, use the `label` category: | |||
| 
 | ||||
|         <div v-if="expanded"> | ||||
|           <p>Hello world!</p> | ||||
|           <button @click="track('click_action')">Track another event</button> | ||||
|           <button @click="track('click_button')">Track another event</button> | ||||
|         </div> | ||||
|       </div> | ||||
|     </template> | ||||
|     ``` | ||||
| 
 | ||||
| The following example shows an implementation of Vue component tracking: | ||||
| #### Testing example | ||||
| 
 | ||||
| ```javascript | ||||
| export default { | ||||
|   name: 'RightSidebar', | ||||
|   mixins: [Tracking.mixin({ label: 'right_sidebar' })], | ||||
|   name: 'CountDropdown', | ||||
| 
 | ||||
|   mixins: [Tracking.mixin({ label: 'count_dropdown' })], | ||||
| 
 | ||||
|   data() { | ||||
|     return { | ||||
|       expanded: false, | ||||
|       variant: 'counter', | ||||
|       count: 0, | ||||
|     }; | ||||
|   }, | ||||
| 
 | ||||
|   methods: { | ||||
|     toggle() { | ||||
|       this.expanded = !this.expanded; | ||||
|       // Additional data will be merged, like `value` below | ||||
|       this.track('click_toggle', { value: Number(this.expanded) }); | ||||
|     } | ||||
|   } | ||||
|     handleChange({ target }) { | ||||
|       const { variant } = this; | ||||
| 
 | ||||
|       this.count = Number(target.value); | ||||
| 
 | ||||
|       this.track('change_value', { | ||||
|         value: this.count, | ||||
|         extra: { variant } | ||||
|       }); | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| ``` | ||||
| 
 | ||||
| #### Testing example | ||||
| 
 | ||||
| ```javascript | ||||
| import { mockTracking } from 'helpers/tracking_helper'; | ||||
| // mockTracking(category, documentOverride, spyMethod) | ||||
| 
 | ||||
| describe('RightSidebar.vue', () => { | ||||
| describe('CountDropdown.vue', () => { | ||||
|   let trackingSpy; | ||||
|   let wrapper; | ||||
| 
 | ||||
|   ... | ||||
| 
 | ||||
|   beforeEach(() => { | ||||
|     trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); | ||||
|   }); | ||||
| 
 | ||||
|   const findToggle = () => wrapper.find('[data-testid="toggle"]'); | ||||
|   const findDropdown = () => wrapper.find('[data-testid="dropdown"]'); | ||||
| 
 | ||||
|   it('tracks turning off toggle', () => { | ||||
|     findToggle().trigger('click'); | ||||
|   it('tracks change event', () => { | ||||
|     const dropdown = findDropdown(); | ||||
|     dropdown.element.value = 30; | ||||
|     dropdown.trigger('change'); | ||||
| 
 | ||||
|     expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_toggle', { | ||||
|       label: 'right_sidebar', | ||||
|       value: 0, | ||||
|     expect(trackingSpy).toHaveBeenCalledWith(undefined, 'change_value', { | ||||
|       value: 30, | ||||
|       label: 'count_dropdown', | ||||
|       extra: { variant: 'counter' }, | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  | @ -241,7 +280,8 @@ describe('RightSidebar.vue', () => { | |||
| 
 | ||||
| ### Implement raw JavaScript tracking | ||||
| 
 | ||||
| To call custom event tracking and instrumentation directly from the JavaScript file, call the `Tracking.event` static function. | ||||
| To track from a vanilla JavaScript file, use the `Tracking.event` static function | ||||
| (calls [`dispatchSnowplowEvent`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/javascripts/tracking/dispatch_snowplow_event.js)). | ||||
| 
 | ||||
| The following example demonstrates tracking a click on a button by manually calling `Tracking.event`. | ||||
| 
 | ||||
|  | @ -251,7 +291,7 @@ import Tracking from '~/tracking'; | |||
| const button = document.getElementById('create_from_template_button'); | ||||
| 
 | ||||
| button.addEventListener('click', () => { | ||||
|   Tracking.event('dashboard:projects:index', 'click_button', { | ||||
|   Tracking.event(undefined, 'click_button', { | ||||
|     label: 'create_from_template', | ||||
|     property: 'template_preview', | ||||
|     extra: { | ||||
|  |  | |||
|  | @ -72,6 +72,7 @@ sequenceDiagram | |||
|       GitLab.com Snowplow Collector ->> GitLab.com Snowplow Collector: Write to disk | ||||
|     end | ||||
|     GitLab.com Snowplow Collector ->> S3 Bucket: Kinesis Firehose | ||||
|     Note over GitLab.com Snowplow Collector, S3 Bucket: Pseudonymization | ||||
|     S3 Bucket->>Snowflake DW: Import data | ||||
|     Snowflake DW->>Snowflake DW: Transform data using dbt | ||||
|     Snowflake DW->>Sisense Dashboards: Data available for querying | ||||
|  |  | |||
|  | @ -12,19 +12,23 @@ This page provides Snowplow schema reference for GitLab events. | |||
| 
 | ||||
| We are including the [`gitlab_standard` schema](https://gitlab.com/gitlab-org/iglu/-/blob/master/public/schemas/com.gitlab/gitlab_standard/jsonschema/) with every event. See [Standardize Snowplow Schema](https://gitlab.com/groups/gitlab-org/-/epics/5218) for details. | ||||
| 
 | ||||
| The [`StandardContext`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/tracking/standard_context.rb) class represents this schema in the application. | ||||
| The [`StandardContext`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/tracking/standard_context.rb) | ||||
| class represents this schema in the application. Some properties are automatically populated for [frontend](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/views/layouts/_snowplow.html.haml) | ||||
| events. | ||||
| 
 | ||||
| | Field Name     | Required            | Type                  | Description                                                                                 | | ||||
| |----------------|---------------------|-----------------------|---------------------------------------------------------------------------------------------| | ||||
| | `project_id`   | **{dotted-circle}** | integer               |                                                                 | | ||||
| | `namespace_id` | **{dotted-circle}** | integer               |                                                               | | ||||
| | `user_id`      | **{dotted-circle}** | integer               | User database record ID attribute. This file undergoes a pseudonymization process at the collector level. | | ||||
| | `context_generated_at` | **{dotted-circle}** | string (date time format) | Timestamp indicating when context was generated.                                                                | | ||||
| | `environment`  | **{check-circle}**  | string (max 32 chars) | Name of the source environment, such as `production` or `staging`             | | ||||
| | `source`       | **{check-circle}**  | string (max 32 chars) | Name of the source application, such as  `gitlab-rails` or `gitlab-javascript` | | ||||
| | `plan`         | **{dotted-circle}**  | string (max 32 chars) | Name of the plan for the namespace, such as `free`, `premium`, or `ultimate`. Automatically picked from the `namespace`. | | ||||
| | `google_analytics_id` | **{dotted-circle}**  | string (max 32 chars) | Google Analytics ID, present when set from our marketing sites. | | ||||
| | `extra`        | **{dotted-circle}**  | JSON                  | Any additional data associated with the event, in the form of key-value pairs | | ||||
| | Field Name     | Required            | Default value | Type                  | Description                                                                                 | | ||||
| |----------------|:-------------------:|-----------------------|--|---------------------------------------------------------------------------------------------| | ||||
| | `project_id`   | **{dotted-circle}** | Current project ID * | integer               |                                                                 | | ||||
| | `namespace_id` | **{dotted-circle}** | Current group/namespace ID * | integer               |                                                               | | ||||
| | `user_id`      | **{dotted-circle}** | Current user ID * | integer               | User database record ID attribute. This file undergoes a pseudonymization process at the collector level. | | ||||
| | `context_generated_at` | **{dotted-circle}** | Current timestamp | string (date time format) | Timestamp indicating when context was generated.                                                                | | ||||
| | `environment`  | **{check-circle}**  | Current environment | string (max 32 chars) | Name of the source environment, such as `production` or `staging`             | | ||||
| | `source`       | **{check-circle}**  | Event source | string (max 32 chars) | Name of the source application, such as  `gitlab-rails` or `gitlab-javascript` | | ||||
| | `plan`         | **{dotted-circle}**  | Current namespace plan * | string (max 32 chars) | Name of the plan for the namespace, such as `free`, `premium`, or `ultimate`. Automatically picked from the `namespace`. | | ||||
| | `google_analytics_id` | **{dotted-circle}**  | GA ID value * | string (max 32 chars) | Google Analytics ID, present when set from our marketing sites. | | ||||
| | `extra`        | **{dotted-circle}**  |  | JSON                  | Any additional data associated with the event, in the form of key-value pairs | | ||||
| 
 | ||||
| _\* Default value present for frontend events only_ | ||||
| 
 | ||||
| ## Default Schema | ||||
| 
 | ||||
|  |  | |||
|  | @ -116,87 +116,106 @@ your website could enable the covert redirect attack. | |||
| 
 | ||||
| 1. Select the icon. Sign in to GitHub and authorize the GitLab application. | ||||
| 
 | ||||
| ## GitHub Enterprise with self-signed Certificate | ||||
| 
 | ||||
| If you are attempting to import projects from GitHub Enterprise with a self-signed | ||||
| certificate and the imports are failing, you must disable SSL verification. | ||||
| It should be disabled by adding `verify_ssl` to `false` in the provider configuration | ||||
| and changing the global Git `sslVerify` option to `false` in the GitLab server. | ||||
| 
 | ||||
| For Omnibus package: | ||||
| 
 | ||||
| ```ruby | ||||
| gitlab_rails['omniauth_providers'] = [ | ||||
|   { | ||||
|     name: "github", | ||||
|     # label: "Provider name", # optional label for login button, defaults to "GitHub" | ||||
|     app_id: "YOUR_APP_ID", | ||||
|     app_secret: "YOUR_APP_SECRET", | ||||
|     url: "https://github.example.com/", | ||||
|     verify_ssl: false, | ||||
|     args: { scope: "user:email" } | ||||
|   } | ||||
| ] | ||||
| ``` | ||||
| 
 | ||||
| You must also disable Git SSL verification on the server hosting GitLab. | ||||
| 
 | ||||
| ```ruby | ||||
| omnibus_gitconfig['system'] = { "http" => ["sslVerify = false"] } | ||||
| ``` | ||||
| 
 | ||||
| For installation from source: | ||||
| 
 | ||||
| ```yaml | ||||
| - { name: 'github', | ||||
|     # label: 'Provider name', # optional label for login button, defaults to "GitHub" | ||||
|     app_id: 'YOUR_APP_ID', | ||||
|     app_secret: 'YOUR_APP_SECRET', | ||||
|     url: "https://github.example.com/", | ||||
|     verify_ssl: false, | ||||
|     args: { scope: 'user:email' } } | ||||
| ``` | ||||
| 
 | ||||
| You must also disable Git SSL verification on the server hosting GitLab. | ||||
| 
 | ||||
| ```shell | ||||
| git config --global http.sslVerify false | ||||
| ``` | ||||
| 
 | ||||
| For the changes to take effect, [reconfigure GitLab](../administration/restart_gitlab.md#omnibus-gitlab-reconfigure) if you installed | ||||
| via Omnibus, or [restart GitLab](../administration/restart_gitlab.md#installations-from-source) if you installed from source. | ||||
| 
 | ||||
| ## Troubleshooting | ||||
| 
 | ||||
| ### Error 500 when trying to sign in to GitLab via GitHub Enterprise | ||||
| ### Imports from GitHub Enterprise with a self-signed certificate fail | ||||
| 
 | ||||
| Check the [`production.log`](../administration/logs.md#productionlog) | ||||
| on your GitLab server to obtain further details. If you are getting the error like | ||||
| `Faraday::ConnectionFailed (execution expired)` in the log, there may be a connectivity issue | ||||
| between your GitLab instance and GitHub Enterprise. To verify it, [start the rails console](../administration/operations/rails_console.md#starting-a-rails-console-session) | ||||
| and run the commands below replacing `<github_url>` with the URL of your GitHub Enterprise instance: | ||||
| When you import projects from GitHub Enterprise using a self-signed | ||||
| certificate, the imports fail. | ||||
| 
 | ||||
| ```ruby | ||||
| uri = URI.parse("https://<github_url>") # replace `GitHub-URL` with the real one here | ||||
| http = Net::HTTP.new(uri.host, uri.port) | ||||
| http.use_ssl = true | ||||
| http.verify_mode = 1 | ||||
| response = http.request(Net::HTTP::Get.new(uri.request_uri)) | ||||
| ``` | ||||
| To fix this issue, you must disable SSL verification: | ||||
| 
 | ||||
| If you are getting a similar `execution expired` error, it confirms the theory about the | ||||
| network connectivity. In that case, make sure that the GitLab server is able to reach your | ||||
| GitHub enterprise instance. | ||||
| 1. Set `verify_ssl` to `false` in the configuration file. | ||||
| 
 | ||||
|    - **For Omnibus installations** | ||||
| 
 | ||||
|      ```ruby | ||||
|      gitlab_rails['omniauth_providers'] = [ | ||||
|        { | ||||
|          name: "github", | ||||
|          # label: "Provider name", # optional label for login button, defaults to "GitHub" | ||||
|          app_id: "YOUR_APP_ID", | ||||
|          app_secret: "YOUR_APP_SECRET", | ||||
|          url: "https://github.example.com/", | ||||
|          verify_ssl: false, | ||||
|          args: { scope: "user:email" } | ||||
|        } | ||||
|      ] | ||||
|      ``` | ||||
| 
 | ||||
|    - **For installations from source** | ||||
| 
 | ||||
|      ```yaml | ||||
|      - { name: 'github', | ||||
|          # label: 'Provider name', # optional label for login button, defaults to "GitHub" | ||||
|          app_id: 'YOUR_APP_ID', | ||||
|          app_secret: 'YOUR_APP_SECRET', | ||||
|          url: "https://github.example.com/", | ||||
|          verify_ssl: false, | ||||
|          args: { scope: 'user:email' } } | ||||
|      ``` | ||||
| 
 | ||||
| 1. Change the global Git `sslVerify` option to `false` on the GitLab server. | ||||
| 
 | ||||
|    - **For Omnibus installations** | ||||
| 
 | ||||
|      ```ruby | ||||
|      omnibus_gitconfig['system'] = { "http" => ["sslVerify = false"] } | ||||
|      ``` | ||||
| 
 | ||||
|    - **For installations from source** | ||||
| 
 | ||||
|      ```shell | ||||
|      git config --global http.sslVerify false | ||||
|      ``` | ||||
| 
 | ||||
| 1. [Reconfigure GitLab](../administration/restart_gitlab.md#omnibus-gitlab-reconfigure) | ||||
|    if you installed using Omnibus, or [restart GitLab](../administration/restart_gitlab.md#installations-from-source) | ||||
|    if you installed from source. | ||||
| 
 | ||||
| ### Signing in using GitHub Enterprise returns a 500 error | ||||
| 
 | ||||
| This error can occur because of a network connectivity issue between your | ||||
| GitLab instance and GitHub Enterprise. | ||||
| 
 | ||||
| To check for a connectivity issue: | ||||
| 
 | ||||
| 1. Go to the [`production.log`](../administration/logs.md#productionlog) | ||||
|    on your GitLab server and look for the following error: | ||||
| 
 | ||||
|    ``` plaintext | ||||
|    Faraday::ConnectionFailed (execution expired) | ||||
|    ``` | ||||
| 
 | ||||
| 1. [Start the rails console](../administration/operations/rails_console.md#starting-a-rails-console-session) | ||||
|    and run the following commands. Replace `<github_url>` with the URL of your | ||||
|    GitHub Enterprise instance: | ||||
| 
 | ||||
|    ```ruby | ||||
|    uri = URI.parse("https://<github_url>") # replace `GitHub-URL` with the real one here | ||||
|    http = Net::HTTP.new(uri.host, uri.port) | ||||
|    http.use_ssl = true | ||||
|    http.verify_mode = 1 | ||||
|    response = http.request(Net::HTTP::Get.new(uri.request_uri)) | ||||
|    ``` | ||||
| 
 | ||||
| 1. If a similar `execution expired` error is returned, this confirms the error is | ||||
|    caused by a connectivity issue. Make sure the GitLab server can reach | ||||
|    your GitHub Enterprise instance. | ||||
| 
 | ||||
| ### Signing in using your GitHub account without a pre-existing GitLab account is not allowed | ||||
| 
 | ||||
| If you're getting the message `Signing in using your GitHub account without a pre-existing | ||||
| GitLab account is not allowed. Create a GitLab account first, and then connect it to your | ||||
| GitHub account` when signing in, in GitLab: | ||||
| When you sign in to GitLab, you get the following error: | ||||
| 
 | ||||
| 1. In the top-right corner, select your avatar. | ||||
| ```plaintext | ||||
| Signing in using your GitHub account without a pre-existing | ||||
| GitLab account is not allowed. Create a GitLab account first, | ||||
| and then connect it to your GitHub account | ||||
| ``` | ||||
| 
 | ||||
| To fix this issue, you must activate GitHub sign-in in GitLab: | ||||
| 
 | ||||
| 1. On the top bar, in the top right corner, select your avatar. | ||||
| 1. Select **Edit profile**. | ||||
| 1. On the left sidebar, select **Account**. | ||||
| 1. In the **Social sign-in** section, select **Connect to GitHub**. | ||||
| 
 | ||||
| After that, you should be able to sign in via GitHub successfully. | ||||
|  |  | |||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 12 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 20 KiB | 
|  | @ -1,119 +1,66 @@ | |||
| --- | ||||
| description: "Merge Request Analytics help you understand the efficiency of your code review process, and the productivity of your team." # Up to ~200 chars long. They will be displayed in Google Search snippets. It may help to write the page intro first, and then reuse it here. | ||||
| description: "Merge request analytics help you understand the efficiency of your code review process, and the productivity of your team." # Up to ~200 chars long. They will be displayed in Google Search snippets. It may help to write the page intro first, and then reuse it here. | ||||
| stage: Manage | ||||
| group: Optimize | ||||
| info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments | ||||
| --- | ||||
| 
 | ||||
| # Merge Request Analytics **(PREMIUM)** | ||||
| # Merge request analytics **(PREMIUM)** | ||||
| 
 | ||||
| > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/229045) in GitLab 13.3. | ||||
| > - Moved to GitLab Premium in 13.9. | ||||
| 
 | ||||
| Merge Request Analytics helps you understand the efficiency of your code review process, and the productivity of your team. | ||||
| Use merge request analytics to view: | ||||
| 
 | ||||
| ## Overview | ||||
| - The number of merge requests your organization merged per month. | ||||
| - The average time between merge request creation and merge. | ||||
| - Information about each merged merge request. | ||||
| 
 | ||||
| Merge Request Analytics displays information that will help you evaluate the efficiency and productivity of your merge request process. | ||||
| You can use merge request analytics to identify: | ||||
| 
 | ||||
| The Throughput chart shows the number of merge requests merged, by month. Merge request throughput is | ||||
| a common measure of productivity in software engineering. Although imperfect, the average throughput can | ||||
| be a meaningful benchmark of your team's overall productivity. | ||||
| - Low or high productivity months. | ||||
| - Efficiency and productivity of your merge request process. | ||||
| - Efficiency of your code review process. | ||||
| 
 | ||||
| To access Merge Request Analytics: | ||||
| ## View merge request analytics | ||||
| 
 | ||||
| You must have at least the Reporter role to view merge request analytics. | ||||
| 
 | ||||
| To view merge request analytics: | ||||
| 
 | ||||
| 1. On the top bar, select **Menu > Projects** and find your project. | ||||
| 1. On the left sidebar, select **Analytics > Merge request**. | ||||
| 
 | ||||
| ## Use cases | ||||
| ## View merge requests merged per month | ||||
| 
 | ||||
| This feature is designed for [development team leaders](https://about.gitlab.com/handbook/marketing/strategic-marketing/roles-personas/#delaney-development-team-lead) | ||||
| and others who want to understand broad patterns in code review and productivity. | ||||
| > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/232651) in GitLab 13.3. | ||||
| > - Filtering [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/229266) in GitLab 13.4 | ||||
| 
 | ||||
| You can use Merge Request Analytics to expose when your team is most and least productive, and | ||||
| identify improvements that might substantially accelerate your development cycle. | ||||
| To view the number of merge requests merged per month: | ||||
| 
 | ||||
| Merge Request Analytics could be used when: | ||||
| 1. On the top bar, select **Menu > Projects** and find your project. | ||||
| 1. On the left sidebar, select **Analytics > Merge request**. | ||||
| 1. Optional. Filter results:    | ||||
|    1. Select the filter bar. | ||||
|    1. Select a parameter. | ||||
|    1. Select a value or enter text to refine the results. | ||||
|    1. To adjust the date range: | ||||
|        - In the **From** field, select a start date. | ||||
|        - In the **To** field, select an end date. | ||||
| 
 | ||||
| - You want to know if you were more productive this month than last month, or 12 months ago. | ||||
| - You want to drill into low- or high-productivity months to understand the work that took place. | ||||
| The **Throughput** chart shows the number of merge requests merged per month. | ||||
| 
 | ||||
| ## Visualizations and data | ||||
| The table shows up to 20 merge requests per page, and includes | ||||
| information about each merge request. | ||||
| 
 | ||||
| The following visualizations and data are available, representing all merge requests that were merged in the given date range. | ||||
| 
 | ||||
| ### Mean time to merge | ||||
| ## View average time between merge request creation and merge | ||||
| 
 | ||||
| > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/229389) in GitLab 13.9. | ||||
| 
 | ||||
| The mean time to merge (MTTM) metric shows the average time between when a merge request is created, | ||||
| and when it is merged. To view how the MTTM changes over time, compare MTTM across different date ranges. | ||||
| Use the number in **Mean time to merge** to view the average time between when a merge request is | ||||
| created and when it's merged. | ||||
| 
 | ||||
|  | ||||
| To view **Mean time to merge**: | ||||
| 
 | ||||
| ### Throughput chart | ||||
| 
 | ||||
| The throughput chart shows the number of merge requests merged per month. | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| ### Throughput table | ||||
| 
 | ||||
| > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/232651) in GitLab 13.3. | ||||
| 
 | ||||
| The Throughput table displays the most recent merge requests merged in the date range. The | ||||
| table displays up to 20 merge requests at a time. If there are more than 20 merge requests, | ||||
| you can paginate to them. For each merge request, you can review the following data: | ||||
| 
 | ||||
| - Title (as a link to the merge request itself) | ||||
| - ID | ||||
| - Pipeline status | ||||
| - Label count | ||||
| - Comment count | ||||
| - Approval count (if approved) | ||||
| - Date merged | ||||
| - Time to merge | ||||
| - Milestone | ||||
| - Commit count | ||||
| - Pipeline count | ||||
| - Line change counts | ||||
| - Assignees | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| ## Filter the data | ||||
| 
 | ||||
| > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/229266) in GitLab 13.4 | ||||
| 
 | ||||
| You can filter the data that is presented on the page based on the following parameters: | ||||
| 
 | ||||
| - Author | ||||
| - Assignee | ||||
| - Label | ||||
| - Milestone | ||||
| - Source branch | ||||
| - Target branch | ||||
| 
 | ||||
| To filter results: | ||||
| 
 | ||||
| 1. Select the filter bar. | ||||
| 1. Select a parameter to filter by. | ||||
| 1. Select a value from the autocompleted results, or enter search text to refine the results. | ||||
| 1. Press Enter. | ||||
| 
 | ||||
| ## Date range | ||||
| 
 | ||||
| The date range is set to the past 12 months by default. You can modify the date range by changing the "From" and/or "To" values that appear alongside the filter bar. After changing either value, the data displayed on the page will update automatically. | ||||
| 
 | ||||
| ## Tip: Bookmark preferred settings | ||||
| 
 | ||||
| You can bookmark preferred filters and date ranges. After you have applied a change to the | ||||
| filter bar or the date range, you'll see that information in the URL. You can create a | ||||
| bookmark for those preferred settings in your browser. | ||||
| 
 | ||||
| ## Permissions | ||||
| 
 | ||||
| The **Merge Request Analytics** feature can be accessed only: | ||||
| 
 | ||||
| - On [GitLab Premium](https://about.gitlab.com/pricing/) and above. | ||||
| - By users with at least the Reporter role. | ||||
| 1. On the top bar, select **Menu > Projects** and find your project. | ||||
| 1. On the left sidebar, select **Analytics > Merge request**. | ||||
|  |  | |||
|  | @ -5,107 +5,30 @@ group: Import | |||
| info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments | ||||
| --- | ||||
| 
 | ||||
| # Import your project from Bitbucket Server to GitLab **(FREE)** | ||||
| # Import your project from Bitbucket Server **(FREE)** | ||||
| 
 | ||||
| > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/20164) in GitLab 11.2. | ||||
| 
 | ||||
| NOTE: | ||||
| The Bitbucket Server importer does not work with [Bitbucket Cloud](https://bitbucket.org). | ||||
| Use the [Bitbucket Cloud importer](bitbucket.md) for that. | ||||
| This process is different than [importing from Bitbucket Cloud](bitbucket.md). | ||||
| 
 | ||||
| Import your projects from Bitbucket Server to GitLab with minimal effort. | ||||
| From Bitbucket Server, you can import: | ||||
| 
 | ||||
| The Bitbucket importer can import: | ||||
| 
 | ||||
| - Repository description (GitLab 11.2+) | ||||
| - Git repository data (GitLab 11.2+) | ||||
| - Pull requests (GitLab 11.2+) | ||||
| - Pull request comments (GitLab 11.2+) | ||||
| - Repository description | ||||
| - Git repository data | ||||
| - Pull requests | ||||
| - Pull request comments | ||||
| 
 | ||||
| When importing, repository public access is retained. If a repository is private in Bitbucket, it's | ||||
| created as private in GitLab as well. | ||||
| 
 | ||||
| ## Limitations | ||||
| 
 | ||||
| - GitLab doesn't allow comments on arbitrary lines of code, so any Bitbucket comments out of bounds | ||||
|   are inserted as comments in the merge request. | ||||
| - Bitbucket Server allows multiple levels of threading. GitLab import collapses this into one thread | ||||
|   and quote part of the original comment. | ||||
| - Declined pull requests have unreachable commits, which prevents the GitLab importer from | ||||
|   generating a proper diff. These pull requests show up as empty changes. | ||||
| - Pull request approvals are not imported. | ||||
| - Attachments in Markdown are not imported. | ||||
| - Task lists are not imported. | ||||
| - Emoji reactions are not imported. | ||||
| - Project filtering does not support fuzzy search (only `starts with` or `full match strings` are | ||||
|   supported). | ||||
| 
 | ||||
| ## How it works | ||||
| 
 | ||||
| The Bitbucket Server importer works as follows: | ||||
| 
 | ||||
| 1. The user is prompted to enter the URL, username, and password (or personal access token) to log in to Bitbucket. | ||||
|    These credentials are preserved only as long as the importer is running. | ||||
| 1. The importer attempts to list all the current repositories on the Bitbucket Server. | ||||
| 1. Upon selection, the importer clones the repository and import pull requests and comments. | ||||
| 
 | ||||
| ### User assignment | ||||
| 
 | ||||
| When issues/pull requests are being imported, the Bitbucket importer tries to | ||||
| find the author's email address with a confirmed email address in the GitLab | ||||
| user database. If no such user is available, the project creator is set as | ||||
| the author. The importer appends a note in the comment to mark the original | ||||
| creator. | ||||
| 
 | ||||
| The importer creates any new namespaces (groups) if they don't exist or in | ||||
| the case the namespace is taken, the repository is imported under the user's | ||||
| namespace that started the import process. | ||||
| 
 | ||||
| #### User assignment by username | ||||
| 
 | ||||
| > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/218609) in GitLab 13.4. | ||||
| > - It's [deployed behind a feature flag](../../feature_flags.md), disabled by default. | ||||
| > - It's disabled on GitLab.com. | ||||
| > - It's not recommended for production use. | ||||
| > - To use it in GitLab self-managed instances, ask a GitLab administrator to enable it. | ||||
| 
 | ||||
| WARNING: | ||||
| This feature might not be available to you. Check the **version history** note above for details. | ||||
| 
 | ||||
| If you've enabled this feature, the importer tries to find a user in the GitLab user database with | ||||
| the author's: | ||||
| 
 | ||||
| - `username` | ||||
| - `slug` | ||||
| - `displayName` | ||||
| 
 | ||||
| If the user is not found by any of these properties, the project creator is set as the author. | ||||
| 
 | ||||
| ##### Enable or disable User assignment by username | ||||
| 
 | ||||
| User assignment by username is under development and not ready for production use. It is | ||||
| deployed behind a feature flag that is **disabled by default**. | ||||
| [GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md) | ||||
| can enable it. | ||||
| 
 | ||||
| To enable it: | ||||
| 
 | ||||
| ```ruby | ||||
| Feature.enable(:bitbucket_server_user_mapping_by_username) | ||||
| ``` | ||||
| 
 | ||||
| To disable it: | ||||
| 
 | ||||
| ```ruby | ||||
| Feature.disable(:bitbucket_server_user_mapping_by_username) | ||||
| ``` | ||||
| 
 | ||||
| ## Import your Bitbucket repositories | ||||
| 
 | ||||
| Prerequisite: | ||||
| Prerequisites: | ||||
| 
 | ||||
| - An administrator must have enabled the importer in | ||||
|   **Admin > Application Settings > Visibility and access controls > Import sources**. | ||||
| - An administrator must have enabled the **Bitbucket Server** in | ||||
|   **Admin > Settings > General > Visibility and access controls > Import sources**. | ||||
| - Review the importer's [limitations](#limitations). | ||||
| 
 | ||||
| To import your Bitbucket repositories: | ||||
| 
 | ||||
|  | @ -115,28 +38,77 @@ To import your Bitbucket repositories: | |||
| 1. Select **Import project**. | ||||
| 1. Select **Bitbucket Server**. | ||||
| 1. Log in to Bitbucket and grant GitLab access to your Bitbucket account. | ||||
| 1. Select the projects that you'd like to import or import all projects. | ||||
|    You can filter projects by name and select the namespace | ||||
|    each project will be imported for. | ||||
| 1. Select the projects to import, or import all projects. You can filter projects by name and select | ||||
|    the namespace for which to import each project. | ||||
| 
 | ||||
| ## Automate group and project import **(PREMIUM)** | ||||
| ## Limitations | ||||
| 
 | ||||
| For information on automating user, group, and project import API calls, see | ||||
| [Automate group and project import](index.md#automate-group-and-project-import). | ||||
| - GitLab doesn't allow comments on arbitrary lines of code. Any out-of-bounds Bitbucket comments are | ||||
|   inserted as comments in the merge request. | ||||
| - Bitbucket Server allows multiple threading levels. The importer collapses this into one thread and | ||||
|   quotes part of the original comment. | ||||
| - Declined pull requests have unreachable commits. This prevents the importer from generating a | ||||
|   proper diff. These pull requests show up as empty changes. | ||||
| - Project filtering doesn't support fuzzy search. Only starts with or full match strings are | ||||
|   supported. | ||||
| 
 | ||||
| The following aren't imported: | ||||
| 
 | ||||
| - Pull request approvals | ||||
| - Attachments in Markdown | ||||
| - Task lists | ||||
| - Emoji reactions | ||||
| 
 | ||||
| ## User assignment | ||||
| 
 | ||||
| When issues and pull requests are importing, the importer tries to find the author's email address | ||||
| with a confirmed email address in the GitLab user database. If no such user is available, the | ||||
| project creator is set as the author. The importer appends a note in the comment to mark the | ||||
| original creator. | ||||
| 
 | ||||
| The importer creates any new namespaces (groups) if they don't exist. If the namespace is taken, the | ||||
| repository imports under the namespace of the user who started the import process. | ||||
| 
 | ||||
| ### User assignment by username | ||||
| 
 | ||||
| > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/218609) in GitLab 13.4 [with a flag](../../../administration/feature_flags.md) named `bitbucket_server_user_mapping_by_username`. Disabled by default. | ||||
| > - Not recommended for production use. | ||||
| 
 | ||||
| FLAG: | ||||
| On self-managed GitLab and GitLab.com, by default this feature is not available. To make it | ||||
| available, ask an administrator to [enable the feature flag](../../../administration/feature_flags.md) | ||||
| named `bitbucket_server_user_mapping_by_username`. This feature is not ready for production use. | ||||
| 
 | ||||
| With this feature enabled, the importer tries to find a user in the GitLab user database with the | ||||
| author's: | ||||
| 
 | ||||
| - `username` | ||||
| - `slug` | ||||
| - `displayName` | ||||
| 
 | ||||
| If no user matches these properties, the project creator is set as the author. | ||||
| 
 | ||||
| ## Troubleshooting | ||||
| 
 | ||||
| ### General | ||||
| 
 | ||||
| If the GUI-based import tool does not work, you can try to: | ||||
| 
 | ||||
| - Use the [GitLab Import API](../../../api/import.md#import-repository-from-bitbucket-server) | ||||
|   Bitbucket Server endpoint. | ||||
| - Set up [repository mirroring](../repository/mirror/index.md). | ||||
|   It provides verbose error output. | ||||
| 
 | ||||
| See the [troubleshooting section](bitbucket.md#troubleshooting) | ||||
| for Bitbucket Cloud. | ||||
| 
 | ||||
| ### LFS objects not imported | ||||
| 
 | ||||
| If the project import completes but LFS objects can't be downloaded or cloned, you may be using a | ||||
| password or personal access token containing special characters. For more information, see | ||||
| [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/337769). | ||||
| 
 | ||||
| ### General | ||||
| ## Related topics | ||||
| 
 | ||||
| If the GUI-based import tool does not work, you can try to: | ||||
| 
 | ||||
| - Use the [GitLab Import API](../../../api/import.md#import-repository-from-bitbucket-server) Bitbucket server endpoint. | ||||
| - Set up [Repository Mirroring](../repository/mirror/index.md), which provides verbose error output. | ||||
| 
 | ||||
| See the [troubleshooting](bitbucket.md#troubleshooting) section for [Bitbucket](bitbucket.md). | ||||
| For information on automating user, group, and project import API calls, see | ||||
| [Automate group and project import](index.md#automate-group-and-project-import). | ||||
|  |  | |||
|  | @ -117,6 +117,9 @@ module API | |||
| 
 | ||||
|         forbidden!('Cannot push to source branch') unless | ||||
|           user_access.can_push_to_branch?(merge_request.source_branch) | ||||
| 
 | ||||
|         forbidden!('Source branch is protected from force push') unless | ||||
|           merge_request.permits_force_push? | ||||
|       end | ||||
| 
 | ||||
|       params :merge_requests_params do | ||||
|  |  | |||
|  | @ -0,0 +1,36 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module Gitlab | ||||
|   module BackgroundMigration | ||||
|     # The class to populates the non private projects counter of topics | ||||
|     class PopulateTopicsNonPrivateProjectsCount | ||||
|       SUB_BATCH_SIZE = 100 | ||||
| 
 | ||||
|       # Temporary AR model for topics | ||||
|       class Topic < ActiveRecord::Base | ||||
|         include EachBatch | ||||
| 
 | ||||
|         self.table_name = 'topics' | ||||
|       end | ||||
| 
 | ||||
|       def perform(start_id, stop_id) | ||||
|         Topic.where(id: start_id..stop_id).each_batch(of: SUB_BATCH_SIZE) do |batch| | ||||
|           ActiveRecord::Base.connection.execute(<<~SQL) | ||||
|             WITH batched_relation AS #{Gitlab::Database::AsWithMaterialized.materialized_if_supported} (#{batch.select(:id).limit(SUB_BATCH_SIZE).to_sql}) | ||||
|             UPDATE topics | ||||
|             SET non_private_projects_count = ( | ||||
|               SELECT COUNT(*)  | ||||
|               FROM project_topics  | ||||
|               INNER JOIN projects  | ||||
|               ON project_topics.project_id = projects.id  | ||||
|               WHERE project_topics.topic_id = batched_relation.id  | ||||
|               AND projects.visibility_level > 0 | ||||
|             ) | ||||
|             FROM batched_relation | ||||
|             WHERE topics.id = batched_relation.id | ||||
|           SQL | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -221,6 +221,7 @@ security-code-scan-sast: | |||
|   image: | ||||
|     name: "$SAST_ANALYZER_IMAGE" | ||||
|   variables: | ||||
|     SAST_ANALYZER_IMAGE_TAG: '2' | ||||
|     SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/security-code-scan:$SAST_ANALYZER_IMAGE_TAG" | ||||
|   rules: | ||||
|     - if: $SAST_DISABLED | ||||
|  | @ -230,8 +231,6 @@ security-code-scan-sast: | |||
|     # This rule shim will be removed in %15.0, | ||||
|     # See https://gitlab.com/gitlab-org/gitlab/-/issues/350935 | ||||
|     - if: $CI_COMMIT_BRANCH && $CI_SERVER_VERSION_MAJOR == '14' | ||||
|       variables: | ||||
|         SAST_ANALYZER_IMAGE_TAG: '2' | ||||
|       exists: | ||||
|         - '**/*.csproj' | ||||
|         - '**/*.vbproj' | ||||
|  |  | |||
|  | @ -40,7 +40,7 @@ module Gitlab | |||
|           # updating the timestamp. | ||||
|           project.touch(:last_repository_updated_at) # rubocop: disable Rails/SkipsModelValidations | ||||
| 
 | ||||
|           project.repository.fetch_remote(project.import_url, refmap: Gitlab::GithubImport.refmap, forced: false) | ||||
|           project.repository.fetch_remote(project.import_url, refmap: Gitlab::GithubImport.refmap, forced: true) | ||||
| 
 | ||||
|           pname = project.path_with_namespace | ||||
| 
 | ||||
|  |  | |||
|  | @ -57,6 +57,11 @@ module Gitlab | |||
|           access_check.can_push_to_branch?(merge_request.source_branch) | ||||
|         end | ||||
|         command :rebase do | ||||
|           unless quick_action_target.permits_force_push? | ||||
|             @execution_message[:rebase] = _('This merge request branch is protected from force push.') | ||||
|             next | ||||
|           end | ||||
| 
 | ||||
|           if quick_action_target.cannot_be_merged? | ||||
|             @execution_message[:rebase] = _('This merge request cannot be rebased while there are conflicts.') | ||||
|             next | ||||
|  | @ -249,6 +254,76 @@ module Gitlab | |||
|             @updates[:reviewer_ids] = [] | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         desc do | ||||
|           if quick_action_target.allows_multiple_reviewers? | ||||
|             _('Request attention from assignee(s) or reviewer(s)') | ||||
|           else | ||||
|             _('Request attention from assignee or reviewer') | ||||
|           end | ||||
|         end | ||||
|         explanation do |users| | ||||
|           _('Request attention from %{users_sentence}.') % { users_sentence: reviewer_users_sentence(users) } | ||||
|         end | ||||
|         execution_message do |users = nil| | ||||
|           if users.blank? | ||||
|             _("Failed to request attention because no user was found.") | ||||
|           else | ||||
|             _('Requested attention from %{users_sentence}.') % { users_sentence: reviewer_users_sentence(users) } | ||||
|           end | ||||
|         end | ||||
|         params do | ||||
|           quick_action_target.allows_multiple_reviewers? ? '@user1 @user2' : '@user' | ||||
|         end | ||||
|         types MergeRequest | ||||
|         condition do | ||||
|           Feature.enabled?(:mr_attention_requests, project, default_enabled: :yaml) && | ||||
|             current_user.can?(:"admin_#{quick_action_target.to_ability_name}", project) | ||||
|         end | ||||
|         parse_params do |attention_param| | ||||
|           extract_users(attention_param) | ||||
|         end | ||||
|         command :attention do |users| | ||||
|           next if users.empty? | ||||
| 
 | ||||
|           users.each do |user| | ||||
|             ::MergeRequests::ToggleAttentionRequestedService.new(project: quick_action_target.project, merge_request: quick_action_target, current_user: current_user, user: user).execute | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         desc do | ||||
|           if quick_action_target.allows_multiple_reviewers? | ||||
|             _('Remove attention request(s)') | ||||
|           else | ||||
|             _('Remove attention request') | ||||
|           end | ||||
|         end | ||||
|         explanation do |users| | ||||
|           _('Removes attention from %{users_sentence}.') % { users_sentence: reviewer_users_sentence(users) } | ||||
|         end | ||||
|         execution_message do |users = nil| | ||||
|           if users.blank? | ||||
|             _("Failed to remove attention because no user was found.") | ||||
|           else | ||||
|             _('Removed attention from %{users_sentence}.') % { users_sentence: reviewer_users_sentence(users) } | ||||
|           end | ||||
|         end | ||||
|         params do | ||||
|           quick_action_target.allows_multiple_reviewers? ? '@user1 @user2' : '@user' | ||||
|         end | ||||
|         types MergeRequest | ||||
|         condition do | ||||
|           Feature.enabled?(:mr_attention_requests, project, default_enabled: :yaml) && | ||||
|             current_user.can?(:"admin_#{quick_action_target.to_ability_name}", project) | ||||
|         end | ||||
|         parse_params do |attention_param| | ||||
|           extract_users(attention_param) | ||||
|         end | ||||
|         command :remove_attention do |users| | ||||
|           next if users.empty? | ||||
| 
 | ||||
|           ::MergeRequests::BulkRemoveAttentionRequestedService.new(project: quick_action_target.project, merge_request: quick_action_target, current_user: current_user, users: users).execute | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       def reviewer_users_sentence(users) | ||||
|  |  | |||
|  | @ -295,3 +295,11 @@ | |||
|   category: quickactions | ||||
|   redis_slot: quickactions | ||||
|   aggregation: weekly | ||||
| - name: i_quickactions_attention | ||||
|   category: quickactions | ||||
|   redis_slot: quickactions | ||||
|   aggregation: weekly | ||||
| - name: i_quickactions_remove_attention | ||||
|   category: quickactions | ||||
|   redis_slot: quickactions | ||||
|   aggregation: weekly | ||||
|  |  | |||
|  | @ -7405,6 +7405,9 @@ msgstr "" | |||
| msgid "Clear recent searches" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Clear repository checks" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Clear search" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -9896,10 +9899,10 @@ msgstr "" | |||
| msgid "CorpusManagement|Actions" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "CorpusManagement|Corpus are used in fuzz testing as mutation source to Improve future testing." | ||||
| msgid "CorpusManagement|Corpus file" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "CorpusManagement|Corpus file" | ||||
| msgid "CorpusManagement|Corpus files are used in coverage-guided fuzz testing as seed inputs to improve testing." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "CorpusManagement|Corpus files must be in *.zip format. Maximum 5 GB" | ||||
|  | @ -15023,6 +15026,9 @@ msgstr "" | |||
| msgid "Failed to remove a to-do item for the design." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Failed to remove attention because no user was found." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Failed to remove mirror." | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -15035,6 +15041,9 @@ msgstr "" | |||
| msgid "Failed to remove user key." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Failed to request attention because no user was found." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Failed to reset key. Please try again." | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -30016,6 +30025,9 @@ msgstr "" | |||
| msgid "Remove attention request" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Remove attention request(s)" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Remove avatar" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -30154,6 +30166,9 @@ msgstr "" | |||
| msgid "Removed an issue from an epic." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Removed attention from %{users_sentence}." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Removed attention request from @%{username}" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -30205,6 +30220,9 @@ msgstr "" | |||
| msgid "Removes an issue from an epic." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Removes attention from %{users_sentence}." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Removes parent epic %{epic_ref}." | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -30605,6 +30623,15 @@ msgstr "" | |||
| msgid "Request attention" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Request attention from %{users_sentence}." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Request attention from assignee or reviewer" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Request attention from assignee(s) or reviewer(s)" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Request attention to review" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -30629,6 +30656,9 @@ msgstr "" | |||
| msgid "Requested %{time_ago}" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Requested attention from %{users_sentence}." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Requested attention from @%{username}" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -31973,7 +32003,7 @@ msgstr "" | |||
| msgid "SecurityConfiguration|Manage corpus" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "SecurityConfiguration|Manage corpus files used as mutation sources in coverage fuzzing." | ||||
| msgid "SecurityConfiguration|Manage corpus files used as seed inputs with coverage-guided fuzzing." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "SecurityConfiguration|Manage profiles for use by DAST scans." | ||||
|  | @ -37134,6 +37164,9 @@ msgstr "" | |||
| msgid "This means you can not push code until you create an empty repository or import existing one." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "This merge request branch is protected from force push." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "This merge request cannot be rebased while there are conflicts." | ||||
| msgstr "" | ||||
| 
 | ||||
|  |  | |||
|  | @ -198,7 +198,7 @@ | |||
|     "web-vitals": "^0.2.4", | ||||
|     "webpack": "^4.46.0", | ||||
|     "webpack-bundle-analyzer": "^4.4.2", | ||||
|     "webpack-cli": "^3.3.12", | ||||
|     "webpack-cli": "^4.9.2", | ||||
|     "webpack-stats-plugin": "^0.3.1", | ||||
|     "worker-loader": "^2.0.0", | ||||
|     "xterm": "3.14.5", | ||||
|  | @ -257,7 +257,7 @@ | |||
|     "stylelint": "^14.3.0", | ||||
|     "timezone-mock": "^1.0.8", | ||||
|     "vue-jest": "4.0.1", | ||||
|     "webpack-dev-server": "^3.11.3", | ||||
|     "webpack-dev-server": "4.7.4", | ||||
|     "xhr-mock": "^2.5.1", | ||||
|     "yarn-check-webpack-plugin": "^1.2.0", | ||||
|     "yarn-deduplicate": "^3.1.0" | ||||
|  |  | |||
|  | @ -2078,6 +2078,20 @@ RSpec.describe Projects::MergeRequestsController do | |||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when source branch is protected from force push' do | ||||
|       before do | ||||
|         create(:protected_branch, project: project, name: merge_request.source_branch, allow_force_push: false) | ||||
|       end | ||||
| 
 | ||||
|       it 'returns 404' do | ||||
|         expect_rebase_worker_for(user).never | ||||
| 
 | ||||
|         post_rebase | ||||
| 
 | ||||
|         expect(response).to have_gitlab_http_status(:not_found) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'with a forked project' do | ||||
|       let(:forked_project) { fork_project(project, fork_owner, repository: true) } | ||||
|       let(:fork_owner) { create(:user) } | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| import MockAdapter from 'axios-mock-adapter'; | ||||
| import $ from 'jquery'; | ||||
| import waitForPromises from 'helpers/wait_for_promises'; | ||||
| import '~/lib/utils/common_utils'; | ||||
| import axios from '~/lib/utils/axios_utils'; | ||||
| import { addDelimiter } from '~/lib/utils/text_utility'; | ||||
|  | @ -71,7 +72,7 @@ describe('Todos', () => { | |||
|     describe('on done todo click', () => { | ||||
|       let onToggleSpy; | ||||
| 
 | ||||
|       beforeEach((done) => { | ||||
|       beforeEach(() => { | ||||
|         const el = document.querySelector('.js-done-todo'); | ||||
|         const path = el.dataset.href; | ||||
| 
 | ||||
|  | @ -86,7 +87,7 @@ describe('Todos', () => { | |||
|         el.click(); | ||||
| 
 | ||||
|         // Wait for axios and HTML to udpate
 | ||||
|         setImmediate(done); | ||||
|         return waitForPromises(); | ||||
|       }); | ||||
| 
 | ||||
|       it('dispatches todo:toggle', () => { | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| import MockAdapter from 'axios-mock-adapter'; | ||||
| import $ from 'jquery'; | ||||
| import { TEST_HOST } from 'helpers/test_constants'; | ||||
| import waitForPromises from 'helpers/wait_for_promises'; | ||||
| import { sanitize } from '~/lib/dompurify'; | ||||
| import axios from '~/lib/utils/axios_utils'; | ||||
| import ProjectFindFile from '~/projects/project_find_file'; | ||||
|  | @ -53,7 +54,7 @@ describe('ProjectFindFile', () => { | |||
|     { path: 'folde?rC/fil#F.txt', escaped: 'folde%3FrC/fil%23F.txt' }, | ||||
|   ]; | ||||
| 
 | ||||
|   beforeEach((done) => { | ||||
|   beforeEach(() => { | ||||
|     // Create a mock adapter for stubbing axios API requests
 | ||||
|     mock = new MockAdapter(axios); | ||||
| 
 | ||||
|  | @ -64,7 +65,7 @@ describe('ProjectFindFile', () => { | |||
|     ); | ||||
|     getProjectFindFileInstance(); // This triggers a load / axios call + subsequent render in the constructor
 | ||||
| 
 | ||||
|     setImmediate(done); | ||||
|     return waitForPromises(); | ||||
|   }); | ||||
| 
 | ||||
|   afterEach(() => { | ||||
|  |  | |||
|  | @ -1,4 +1,5 @@ | |||
| import MockAdapter from 'axios-mock-adapter'; | ||||
| import waitForPromises from 'helpers/wait_for_promises'; | ||||
| import axios from '~/lib/utils/axios_utils'; | ||||
| import PANEL_STATE from '~/prometheus_metrics/constants'; | ||||
| import PrometheusMetrics from '~/prometheus_metrics/prometheus_metrics'; | ||||
|  | @ -132,7 +133,7 @@ describe('PrometheusMetrics', () => { | |||
|       mock.restore(); | ||||
|     }); | ||||
| 
 | ||||
|     it('should show loader animation while response is being loaded and hide it when request is complete', (done) => { | ||||
|     it('should show loader animation while response is being loaded and hide it when request is complete', async () => { | ||||
|       mockSuccess(); | ||||
| 
 | ||||
|       prometheusMetrics.loadActiveMetrics(); | ||||
|  | @ -140,34 +141,31 @@ describe('PrometheusMetrics', () => { | |||
|       expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeFalsy(); | ||||
|       expect(axios.get).toHaveBeenCalledWith(prometheusMetrics.activeMetricsEndpoint); | ||||
| 
 | ||||
|       setImmediate(() => { | ||||
|         expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy(); | ||||
|         done(); | ||||
|       }); | ||||
|       await waitForPromises(); | ||||
| 
 | ||||
|       expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy(); | ||||
|     }); | ||||
| 
 | ||||
|     it('should show empty state if response failed to load', (done) => { | ||||
|     it('should show empty state if response failed to load', async () => { | ||||
|       mockError(); | ||||
| 
 | ||||
|       prometheusMetrics.loadActiveMetrics(); | ||||
| 
 | ||||
|       setImmediate(() => { | ||||
|         expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy(); | ||||
|         expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeFalsy(); | ||||
|         done(); | ||||
|       }); | ||||
|       await waitForPromises(); | ||||
| 
 | ||||
|       expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy(); | ||||
|       expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeFalsy(); | ||||
|     }); | ||||
| 
 | ||||
|     it('should populate metrics list once response is loaded', (done) => { | ||||
|     it('should populate metrics list once response is loaded', async () => { | ||||
|       jest.spyOn(prometheusMetrics, 'populateActiveMetrics').mockImplementation(); | ||||
|       mockSuccess(); | ||||
| 
 | ||||
|       prometheusMetrics.loadActiveMetrics(); | ||||
| 
 | ||||
|       setImmediate(() => { | ||||
|         expect(prometheusMetrics.populateActiveMetrics).toHaveBeenCalledWith(metrics); | ||||
|         done(); | ||||
|       }); | ||||
|       await waitForPromises(); | ||||
| 
 | ||||
|       expect(prometheusMetrics.populateActiveMetrics).toHaveBeenCalledWith(metrics); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  |  | |||
|  | @ -97,14 +97,14 @@ describe('SidebarSeverity', () => { | |||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     it('shows error alert when severity update fails ', () => { | ||||
|     it('shows error alert when severity update fails ', async () => { | ||||
|       const errorMsg = 'Something went wrong'; | ||||
|       jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValueOnce(errorMsg); | ||||
|       findCriticalSeverityDropdownItem().vm.$emit('click'); | ||||
| 
 | ||||
|       setImmediate(() => { | ||||
|         expect(createFlash).toHaveBeenCalled(); | ||||
|       }); | ||||
|       await waitForPromises(); | ||||
| 
 | ||||
|       expect(createFlash).toHaveBeenCalled(); | ||||
|     }); | ||||
| 
 | ||||
|     it('shows loading icon while updating', async () => { | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| import MockAdapter from 'axios-mock-adapter'; | ||||
| import $ from 'jquery'; | ||||
| import waitForPromises from 'helpers/wait_for_promises'; | ||||
| import createFlash from '~/flash'; | ||||
| import axios from '~/lib/utils/axios_utils'; | ||||
| import SidebarMoveIssue from '~/sidebar/lib/sidebar_move_issue'; | ||||
|  | @ -77,15 +78,14 @@ describe('SidebarMoveIssue', () => { | |||
|       expect(test.sidebarMoveIssue.$dropdownToggle.data('deprecatedJQueryDropdown')).toBeTruthy(); | ||||
|     }); | ||||
| 
 | ||||
|     it('escapes html from project name', (done) => { | ||||
|     it('escapes html from project name', async () => { | ||||
|       test.$toggleButton.dropdown('toggle'); | ||||
| 
 | ||||
|       setImmediate(() => { | ||||
|         expect(test.$content.find('.js-move-issue-dropdown-item')[1].innerHTML.trim()).toEqual( | ||||
|           '<img src=x onerror=alert(document.domain)> foo / bar', | ||||
|         ); | ||||
|         done(); | ||||
|       }); | ||||
|       await waitForPromises(); | ||||
| 
 | ||||
|       expect(test.$content.find('.js-move-issue-dropdown-item')[1].innerHTML.trim()).toEqual( | ||||
|         '<img src=x onerror=alert(document.domain)> foo / bar', | ||||
|       ); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|  | @ -101,20 +101,20 @@ describe('SidebarMoveIssue', () => { | |||
|       expect(test.$confirmButton.hasClass('is-loading')).toBe(true); | ||||
|     }); | ||||
| 
 | ||||
|     it('should remove loading state from confirm button on failure', (done) => { | ||||
|     it('should remove loading state from confirm button on failure', async () => { | ||||
|       jest.spyOn(test.mediator, 'moveIssue').mockReturnValue(Promise.reject()); | ||||
|       test.mediator.setMoveToProjectId(7); | ||||
| 
 | ||||
|       test.sidebarMoveIssue.onConfirmClicked(); | ||||
| 
 | ||||
|       expect(test.mediator.moveIssue).toHaveBeenCalled(); | ||||
| 
 | ||||
|       // Wait for the move issue request to fail
 | ||||
|       setImmediate(() => { | ||||
|         expect(createFlash).toHaveBeenCalled(); | ||||
|         expect(test.$confirmButton.prop('disabled')).toBeFalsy(); | ||||
|         expect(test.$confirmButton.hasClass('is-loading')).toBe(false); | ||||
|         done(); | ||||
|       }); | ||||
|       await waitForPromises(); | ||||
| 
 | ||||
|       expect(createFlash).toHaveBeenCalled(); | ||||
|       expect(test.$confirmButton.prop('disabled')).toBeFalsy(); | ||||
|       expect(test.$confirmButton.hasClass('is-loading')).toBe(false); | ||||
|     }); | ||||
| 
 | ||||
|     it('should not move the issue with id=0', () => { | ||||
|  | @ -127,35 +127,33 @@ describe('SidebarMoveIssue', () => { | |||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   it('should set moveToProjectId on dropdown item "No project" click', (done) => { | ||||
|   it('should set moveToProjectId on dropdown item "No project" click', async () => { | ||||
|     jest.spyOn(test.mediator, 'setMoveToProjectId').mockImplementation(() => {}); | ||||
| 
 | ||||
|     // Open the dropdown
 | ||||
|     test.$toggleButton.dropdown('toggle'); | ||||
| 
 | ||||
|     // Wait for the autocomplete request to finish
 | ||||
|     setImmediate(() => { | ||||
|       test.$content.find('.js-move-issue-dropdown-item').eq(0).trigger('click'); | ||||
|     await waitForPromises(); | ||||
| 
 | ||||
|       expect(test.mediator.setMoveToProjectId).toHaveBeenCalledWith(0); | ||||
|       expect(test.$confirmButton.prop('disabled')).toBeTruthy(); | ||||
|       done(); | ||||
|     }); | ||||
|     test.$content.find('.js-move-issue-dropdown-item').eq(0).trigger('click'); | ||||
| 
 | ||||
|     expect(test.mediator.setMoveToProjectId).toHaveBeenCalledWith(0); | ||||
|     expect(test.$confirmButton.prop('disabled')).toBeTruthy(); | ||||
|   }); | ||||
| 
 | ||||
|   it('should set moveToProjectId on dropdown item click', (done) => { | ||||
|   it('should set moveToProjectId on dropdown item click', async () => { | ||||
|     jest.spyOn(test.mediator, 'setMoveToProjectId').mockImplementation(() => {}); | ||||
| 
 | ||||
|     // Open the dropdown
 | ||||
|     test.$toggleButton.dropdown('toggle'); | ||||
| 
 | ||||
|     // Wait for the autocomplete request to finish
 | ||||
|     setImmediate(() => { | ||||
|       test.$content.find('.js-move-issue-dropdown-item').eq(1).trigger('click'); | ||||
|     await waitForPromises(); | ||||
| 
 | ||||
|       expect(test.mediator.setMoveToProjectId).toHaveBeenCalledWith(20); | ||||
|       expect(test.$confirmButton.attr('disabled')).toBe(undefined); | ||||
|       done(); | ||||
|     }); | ||||
|     test.$content.find('.js-move-issue-dropdown-item').eq(1).trigger('click'); | ||||
| 
 | ||||
|     expect(test.mediator.setMoveToProjectId).toHaveBeenCalledWith(20); | ||||
|     expect(test.$confirmButton.attr('disabled')).toBe(undefined); | ||||
|   }); | ||||
| }); | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| import axios from 'axios'; | ||||
| import MockAdapter from 'axios-mock-adapter'; | ||||
| import Vue, { nextTick } from 'vue'; | ||||
| import waitForPromises from 'helpers/wait_for_promises'; | ||||
| import MemoryUsage from '~/vue_merge_request_widget/components/deployment/memory_usage.vue'; | ||||
| import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service'; | ||||
| 
 | ||||
|  | @ -152,23 +153,18 @@ describe('MemoryUsage', () => { | |||
|     }); | ||||
| 
 | ||||
|     describe('loadMetrics', () => { | ||||
|       const returnServicePromise = () => | ||||
|         new Promise((resolve) => { | ||||
|           resolve({ | ||||
|             data: metricsMockData, | ||||
|           }); | ||||
|       it('should load metrics data using MRWidgetService', async () => { | ||||
|         jest.spyOn(MRWidgetService, 'fetchMetrics').mockResolvedValue({ | ||||
|           data: metricsMockData, | ||||
|         }); | ||||
| 
 | ||||
|       it('should load metrics data using MRWidgetService', (done) => { | ||||
|         jest.spyOn(MRWidgetService, 'fetchMetrics').mockReturnValue(returnServicePromise(true)); | ||||
|         jest.spyOn(vm, 'computeGraphData').mockImplementation(() => {}); | ||||
| 
 | ||||
|         vm.loadMetrics(); | ||||
|         setImmediate(() => { | ||||
|           expect(MRWidgetService.fetchMetrics).toHaveBeenCalledWith(url); | ||||
|           expect(vm.computeGraphData).toHaveBeenCalledWith(metrics, deployment_time); | ||||
|           done(); | ||||
|         }); | ||||
| 
 | ||||
|         await waitForPromises(); | ||||
| 
 | ||||
|         expect(MRWidgetService.fetchMetrics).toHaveBeenCalledWith(url); | ||||
|         expect(vm.computeGraphData).toHaveBeenCalledWith(metrics, deployment_time); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ import { shallowMount } from '@vue/test-utils'; | |||
| import { nextTick } from 'vue'; | ||||
| import { trimText } from 'helpers/text_helper'; | ||||
| import { extendedWrapper } from 'helpers/vue_test_utils_helper'; | ||||
| import waitForPromises from 'helpers/wait_for_promises'; | ||||
| import autoMergeEnabledComponent from '~/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue'; | ||||
| import { MWPS_MERGE_STRATEGY } from '~/vue_merge_request_widget/constants'; | ||||
| import eventHub from '~/vue_merge_request_widget/event_hub'; | ||||
|  | @ -185,7 +186,7 @@ describe('MRWidgetAutoMergeEnabled', () => { | |||
| 
 | ||||
|       describe('methods', () => { | ||||
|         describe('cancelAutomaticMerge', () => { | ||||
|           it('should set flag and call service then tell main component to update the widget with data', (done) => { | ||||
|           it('should set flag and call service then tell main component to update the widget with data', async () => { | ||||
|             factory({ | ||||
|               ...defaultMrProps(), | ||||
|             }); | ||||
|  | @ -201,20 +202,20 @@ describe('MRWidgetAutoMergeEnabled', () => { | |||
|             ); | ||||
| 
 | ||||
|             wrapper.vm.cancelAutomaticMerge(); | ||||
|             setImmediate(() => { | ||||
|               expect(wrapper.vm.isCancellingAutoMerge).toBeTruthy(); | ||||
|               if (mergeRequestWidgetGraphql) { | ||||
|                 expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested'); | ||||
|               } else { | ||||
|                 expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj); | ||||
|               } | ||||
|               done(); | ||||
|             }); | ||||
| 
 | ||||
|             await waitForPromises(); | ||||
| 
 | ||||
|             expect(wrapper.vm.isCancellingAutoMerge).toBeTruthy(); | ||||
|             if (mergeRequestWidgetGraphql) { | ||||
|               expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested'); | ||||
|             } else { | ||||
|               expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj); | ||||
|             } | ||||
|           }); | ||||
|         }); | ||||
| 
 | ||||
|         describe('removeSourceBranch', () => { | ||||
|           it('should set flag and call service then request main component to update the widget', (done) => { | ||||
|           it('should set flag and call service then request main component to update the widget', async () => { | ||||
|             factory({ | ||||
|               ...defaultMrProps(), | ||||
|             }); | ||||
|  | @ -227,14 +228,14 @@ describe('MRWidgetAutoMergeEnabled', () => { | |||
|             ); | ||||
| 
 | ||||
|             wrapper.vm.removeSourceBranch(); | ||||
|             setImmediate(() => { | ||||
|               expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested'); | ||||
|               expect(wrapper.vm.service.merge).toHaveBeenCalledWith({ | ||||
|                 sha, | ||||
|                 auto_merge_strategy: MWPS_MERGE_STRATEGY, | ||||
|                 should_remove_source_branch: true, | ||||
|               }); | ||||
|               done(); | ||||
| 
 | ||||
|             await waitForPromises(); | ||||
| 
 | ||||
|             expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested'); | ||||
|             expect(wrapper.vm.service.merge).toHaveBeenCalledWith({ | ||||
|               sha, | ||||
|               auto_merge_strategy: MWPS_MERGE_STRATEGY, | ||||
|               should_remove_source_branch: true, | ||||
|             }); | ||||
|           }); | ||||
|         }); | ||||
|  |  | |||
|  | @ -0,0 +1,50 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe Gitlab::BackgroundMigration::PopulateTopicsNonPrivateProjectsCount, schema: 20220125122640 do | ||||
|   it 'correctly populates the non private projects counters' do | ||||
|     namespaces = table(:namespaces) | ||||
|     projects = table(:projects) | ||||
|     topics = table(:topics) | ||||
|     project_topics = table(:project_topics) | ||||
| 
 | ||||
|     group = namespaces.create!(name: 'group', path: 'group') | ||||
|     project_public = projects.create!(namespace_id: group.id, visibility_level: Gitlab::VisibilityLevel::PUBLIC) | ||||
|     project_internal = projects.create!(namespace_id: group.id, visibility_level: Gitlab::VisibilityLevel::INTERNAL) | ||||
|     project_private = projects.create!(namespace_id: group.id, visibility_level: Gitlab::VisibilityLevel::PRIVATE) | ||||
|     topic_1 = topics.create!(name: 'Topic1') | ||||
|     topic_2 = topics.create!(name: 'Topic2') | ||||
|     topic_3 = topics.create!(name: 'Topic3') | ||||
|     topic_4 = topics.create!(name: 'Topic4') | ||||
|     topic_5 = topics.create!(name: 'Topic5') | ||||
|     topic_6 = topics.create!(name: 'Topic6') | ||||
|     topic_7 = topics.create!(name: 'Topic7') | ||||
|     topic_8 = topics.create!(name: 'Topic8') | ||||
| 
 | ||||
|     project_topics.create!(topic_id: topic_1.id, project_id: project_public.id) | ||||
|     project_topics.create!(topic_id: topic_2.id, project_id: project_internal.id) | ||||
|     project_topics.create!(topic_id: topic_3.id, project_id: project_private.id) | ||||
|     project_topics.create!(topic_id: topic_4.id, project_id: project_public.id) | ||||
|     project_topics.create!(topic_id: topic_4.id, project_id: project_internal.id) | ||||
|     project_topics.create!(topic_id: topic_5.id, project_id: project_public.id) | ||||
|     project_topics.create!(topic_id: topic_5.id, project_id: project_private.id) | ||||
|     project_topics.create!(topic_id: topic_6.id, project_id: project_internal.id) | ||||
|     project_topics.create!(topic_id: topic_6.id, project_id: project_private.id) | ||||
|     project_topics.create!(topic_id: topic_7.id, project_id: project_public.id) | ||||
|     project_topics.create!(topic_id: topic_7.id, project_id: project_internal.id) | ||||
|     project_topics.create!(topic_id: topic_7.id, project_id: project_private.id) | ||||
|     project_topics.create!(topic_id: topic_8.id, project_id: project_public.id) | ||||
| 
 | ||||
|     subject.perform(topic_1.id, topic_7.id) | ||||
| 
 | ||||
|     expect(topic_1.reload.non_private_projects_count).to eq(1) | ||||
|     expect(topic_2.reload.non_private_projects_count).to eq(1) | ||||
|     expect(topic_3.reload.non_private_projects_count).to eq(0) | ||||
|     expect(topic_4.reload.non_private_projects_count).to eq(2) | ||||
|     expect(topic_5.reload.non_private_projects_count).to eq(1) | ||||
|     expect(topic_6.reload.non_private_projects_count).to eq(1) | ||||
|     expect(topic_7.reload.non_private_projects_count).to eq(2) | ||||
|     expect(topic_8.reload.non_private_projects_count).to eq(0) | ||||
|   end | ||||
| end | ||||
|  | @ -164,7 +164,7 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestsImporter do | |||
| 
 | ||||
|       expect(project.repository) | ||||
|         .to receive(:fetch_remote) | ||||
|         .with(url, forced: false, refmap: Gitlab::GithubImport.refmap) | ||||
|         .with(url, forced: true, refmap: Gitlab::GithubImport.refmap) | ||||
| 
 | ||||
|       freeze_time do | ||||
|         importer.update_repository | ||||
|  |  | |||
|  | @ -1538,6 +1538,42 @@ RSpec.describe MergeRequest, factory_default: :keep do | |||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#permits_force_push?' do | ||||
|     let_it_be(:merge_request) { build_stubbed(:merge_request) } | ||||
| 
 | ||||
|     subject { merge_request.permits_force_push? } | ||||
| 
 | ||||
|     context 'when source branch is not protected' do | ||||
|       before do | ||||
|         allow(ProtectedBranch).to receive(:protected?).and_return(false) | ||||
|       end | ||||
| 
 | ||||
|       it { is_expected.to be_truthy } | ||||
|     end | ||||
| 
 | ||||
|     context 'when source branch is protected' do | ||||
|       before do | ||||
|         allow(ProtectedBranch).to receive(:protected?).and_return(true) | ||||
|       end | ||||
| 
 | ||||
|       context 'when force push is not allowed' do | ||||
|         before do | ||||
|           allow(ProtectedBranch).to receive(:allow_force_push?) { false } | ||||
|         end | ||||
| 
 | ||||
|         it { is_expected.to be_falsey } | ||||
|       end | ||||
| 
 | ||||
|       context 'when force push is allowed' do | ||||
|         before do | ||||
|           allow(ProtectedBranch).to receive(:allow_force_push?) { true } | ||||
|         end | ||||
| 
 | ||||
|         it { is_expected.to be_truthy } | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#can_remove_source_branch?' do | ||||
|     let_it_be(:user) { create(:user) } | ||||
|     let_it_be(:merge_request, reload: true) { create(:merge_request, :simple) } | ||||
|  |  | |||
|  | @ -7418,6 +7418,67 @@ RSpec.describe Project, factory_default: :keep do | |||
|         expect(project.reload.topics.map(&:name)).to eq(%w[topic1 topic2 topic3]) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'public topics counter' do | ||||
|       let_it_be(:topic_1) { create(:topic, name: 't1') } | ||||
|       let_it_be(:topic_2) { create(:topic, name: 't2') } | ||||
|       let_it_be(:topic_3) { create(:topic, name: 't3') } | ||||
| 
 | ||||
|       let(:private) { Gitlab::VisibilityLevel::PRIVATE } | ||||
|       let(:internal) { Gitlab::VisibilityLevel::INTERNAL } | ||||
|       let(:public) { Gitlab::VisibilityLevel::PUBLIC } | ||||
| 
 | ||||
|       subject do | ||||
|         project_updates = { | ||||
|           visibility_level: new_visibility, | ||||
|           topic_list: new_topic_list | ||||
|         }.compact | ||||
| 
 | ||||
|         project.update!(project_updates) | ||||
|       end | ||||
| 
 | ||||
|       using RSpec::Parameterized::TableSyntax | ||||
| 
 | ||||
|       # rubocop:disable Lint/BinaryOperatorWithIdenticalOperands | ||||
|       where(:initial_visibility, :new_visibility, :new_topic_list, :expected_count_changes) do | ||||
|         ref(:private)  | nil            | 't2, t3' | [0, 0, 0] | ||||
|         ref(:internal) | nil            | 't2, t3' | [-1, 0, 1] | ||||
|         ref(:public)   | nil            | 't2, t3' | [-1, 0, 1] | ||||
|         ref(:private)  | ref(:public)   | nil      | [1, 1, 0] | ||||
|         ref(:private)  | ref(:internal) | nil      | [1, 1, 0] | ||||
|         ref(:private)  | ref(:private)  | nil      | [0, 0, 0] | ||||
|         ref(:internal) | ref(:public)   | nil      | [0, 0, 0] | ||||
|         ref(:internal) | ref(:internal) | nil      | [0, 0, 0] | ||||
|         ref(:internal) | ref(:private)  | nil      | [-1, -1, 0] | ||||
|         ref(:public)   | ref(:public)   | nil      | [0, 0, 0] | ||||
|         ref(:public)   | ref(:internal) | nil      | [0, 0, 0] | ||||
|         ref(:public)   | ref(:private)  | nil      | [-1, -1, 0] | ||||
|         ref(:private)  | ref(:public)   | 't2, t3' | [0, 1, 1] | ||||
|         ref(:private)  | ref(:internal) | 't2, t3' | [0, 1, 1] | ||||
|         ref(:private)  | ref(:private)  | 't2, t3' | [0, 0, 0] | ||||
|         ref(:internal) | ref(:public)   | 't2, t3' | [-1, 0, 1] | ||||
|         ref(:internal) | ref(:internal) | 't2, t3' | [-1, 0, 1] | ||||
|         ref(:internal) | ref(:private)  | 't2, t3' | [-1, -1, 0] | ||||
|         ref(:public)   | ref(:public)   | 't2, t3' | [-1, 0, 1] | ||||
|         ref(:public)   | ref(:internal) | 't2, t3' | [-1, 0, 1] | ||||
|         ref(:public)   | ref(:private)  | 't2, t3' | [-1, -1, 0] | ||||
|       end | ||||
|       # rubocop:enable Lint/BinaryOperatorWithIdenticalOperands | ||||
| 
 | ||||
|       with_them do | ||||
|         it 'increments or decrements counters of topics' do | ||||
|           project.reload.update!( | ||||
|             visibility_level: initial_visibility, | ||||
|             topic_list: [topic_1.name, topic_2.name] | ||||
|           ) | ||||
| 
 | ||||
|           expect { subject } | ||||
|             .to change { topic_1.reload.non_private_projects_count }.by(expected_count_changes[0]) | ||||
|             .and change { topic_2.reload.non_private_projects_count }.by(expected_count_changes[1]) | ||||
|             .and change { topic_3.reload.non_private_projects_count }.by(expected_count_changes[2]) | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   shared_examples 'all_runners' do | ||||
|  |  | |||
|  | @ -3344,6 +3344,18 @@ RSpec.describe API::MergeRequests do | |||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when merge request branch does not allow force push' do | ||||
|       before do | ||||
|         create(:protected_branch, project: project, name: merge_request.source_branch, allow_force_push: false) | ||||
|       end | ||||
| 
 | ||||
|       it 'returns 403' do | ||||
|         put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/rebase", user) | ||||
| 
 | ||||
|         expect(response).to have_gitlab_http_status(:forbidden) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     it 'returns 403 if the user cannot push to the branch' do | ||||
|       guest = create(:user) | ||||
|       project.add_guest(guest) | ||||
|  |  | |||
|  | @ -10,7 +10,7 @@ RSpec.describe MergeRequests::BulkRemoveAttentionRequestedService do | |||
|   let(:reviewer) { merge_request.find_reviewer(user) } | ||||
|   let(:assignee) { merge_request.find_assignee(assignee_user) } | ||||
|   let(:project) { merge_request.project } | ||||
|   let(:service) { described_class.new(project: project, current_user: current_user, merge_request: merge_request) } | ||||
|   let(:service) { described_class.new(project: project, current_user: current_user, merge_request: merge_request, users: [user, assignee_user]) } | ||||
|   let(:result) { service.execute } | ||||
| 
 | ||||
|   before do | ||||
|  | @ -20,7 +20,7 @@ RSpec.describe MergeRequests::BulkRemoveAttentionRequestedService do | |||
| 
 | ||||
|   describe '#execute' do | ||||
|     context 'invalid permissions' do | ||||
|       let(:service) { described_class.new(project: project, current_user: create(:user), merge_request: merge_request) } | ||||
|       let(:service) { described_class.new(project: project, current_user: create(:user), merge_request: merge_request, users: [user]) } | ||||
| 
 | ||||
|       it 'returns an error' do | ||||
|         expect(result[:status]).to eq :error | ||||
|  |  | |||
|  | @ -701,6 +701,27 @@ RSpec.describe QuickActions::InterpretService do | |||
|       end | ||||
|     end | ||||
| 
 | ||||
|     shared_examples 'attention command' do | ||||
|       it 'updates reviewers attention status' do | ||||
|         _, _, message = service.execute(content, issuable) | ||||
| 
 | ||||
|         expect(message).to eq("Requested attention from #{developer.to_reference}.") | ||||
| 
 | ||||
|         reviewer.reload | ||||
| 
 | ||||
|         expect(reviewer).to be_attention_requested | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     shared_examples 'remove attention command' do | ||||
|       it 'updates reviewers attention status' do | ||||
|         _, _, message = service.execute(content, issuable) | ||||
| 
 | ||||
|         expect(message).to eq("Removed attention from #{developer.to_reference}.") | ||||
|         expect(reviewer).not_to be_attention_requested | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     it_behaves_like 'reopen command' do | ||||
|       let(:content) { '/reopen' } | ||||
|       let(:issuable) { issue } | ||||
|  | @ -2283,6 +2304,82 @@ RSpec.describe QuickActions::InterpretService do | |||
|         expect(message).to eq('One or more contacts were successfully removed.') | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     describe 'attention command' do | ||||
|       let(:issuable) { create(:merge_request, reviewers: [developer], source_project: project) } | ||||
|       let(:reviewer) { issuable.merge_request_reviewers.find_by(user_id: developer.id) } | ||||
|       let(:content) { "/attention @#{developer.username}" } | ||||
| 
 | ||||
|       context 'with one user' do | ||||
|         before do | ||||
|           reviewer.update!(state: :reviewed) | ||||
|         end | ||||
| 
 | ||||
|         it_behaves_like 'attention command' | ||||
|       end | ||||
| 
 | ||||
|       context 'with no user' do | ||||
|         let(:content) { "/attention" } | ||||
| 
 | ||||
|         it_behaves_like 'failed command', 'Failed to request attention because no user was found.' | ||||
|       end | ||||
| 
 | ||||
|       context 'with incorrect permissions' do | ||||
|         let(:service) { described_class.new(project, create(:user)) } | ||||
| 
 | ||||
|         it_behaves_like 'failed command', 'Could not apply attention command.' | ||||
|       end | ||||
| 
 | ||||
|       context 'with feature flag disabled' do | ||||
|         before do | ||||
|           stub_feature_flags(mr_attention_requests: false) | ||||
|         end | ||||
| 
 | ||||
|         it_behaves_like 'failed command', 'Could not apply attention command.' | ||||
|       end | ||||
| 
 | ||||
|       context 'with an issue instead of a merge request' do | ||||
|         let(:issuable) { issue } | ||||
| 
 | ||||
|         it_behaves_like 'failed command', 'Could not apply attention command.' | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     describe 'remove attention command' do | ||||
|       let(:issuable) { create(:merge_request, reviewers: [developer], source_project: project) } | ||||
|       let(:reviewer) { issuable.merge_request_reviewers.find_by(user_id: developer.id) } | ||||
|       let(:content) { "/remove_attention @#{developer.username}" } | ||||
| 
 | ||||
|       context 'with one user' do | ||||
|         it_behaves_like 'remove attention command' | ||||
|       end | ||||
| 
 | ||||
|       context 'with no user' do | ||||
|         let(:content) { "/remove_attention" } | ||||
| 
 | ||||
|         it_behaves_like 'failed command', 'Failed to remove attention because no user was found.' | ||||
|       end | ||||
| 
 | ||||
|       context 'with incorrect permissions' do | ||||
|         let(:service) { described_class.new(project, create(:user)) } | ||||
| 
 | ||||
|         it_behaves_like 'failed command', 'Could not apply remove_attention command.' | ||||
|       end | ||||
| 
 | ||||
|       context 'with feature flag disabled' do | ||||
|         before do | ||||
|           stub_feature_flags(mr_attention_requests: false) | ||||
|         end | ||||
| 
 | ||||
|         it_behaves_like 'failed command', 'Could not apply remove_attention command.' | ||||
|       end | ||||
| 
 | ||||
|       context 'with an issue instead of a merge request' do | ||||
|         let(:issuable) { issue } | ||||
| 
 | ||||
|         it_behaves_like 'failed command', 'Could not apply remove_attention command.' | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#explain' do | ||||
|  |  | |||
|  | @ -73,6 +73,16 @@ RSpec.shared_examples 'rebase quick action' do | |||
|           expect(page).to have_content 'This merge request cannot be rebased while there are conflicts.' | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'when the merge request branch is protected from force push' do | ||||
|         let!(:protected_branch) { create(:protected_branch, project: project, name: merge_request.source_branch, allow_force_push: false) } | ||||
| 
 | ||||
|         it 'does not rebase the MR' do | ||||
|           add_note("/rebase") | ||||
| 
 | ||||
|           expect(page).to have_content 'This merge request branch is protected from force push.' | ||||
|         end | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when the current user cannot rebase the MR' do | ||||
|  |  | |||
|  | @ -0,0 +1,57 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe ContainerRegistry::Migration::ObserverWorker, :aggregate_failures do | ||||
|   let(:worker) { described_class.new } | ||||
| 
 | ||||
|   describe '#perform' do | ||||
|     subject { worker.perform } | ||||
| 
 | ||||
|     context 'when the migration feature flag is disabled' do | ||||
|       before do | ||||
|         stub_feature_flags(container_registry_migration_phase2_enabled: false) | ||||
|       end | ||||
| 
 | ||||
|       it 'does nothing' do | ||||
|         expect(worker).not_to receive(:log_extra_metadata_on_done) | ||||
| 
 | ||||
|         subject | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when the migration is enabled' do | ||||
|       before do | ||||
|         create_list(:container_repository, 3) | ||||
|         create(:container_repository, :pre_importing) | ||||
|         create(:container_repository, :pre_import_done) | ||||
|         create_list(:container_repository, 2, :importing) | ||||
|         create(:container_repository, :import_aborted) | ||||
|         # batch_count is not allowed within a transaction but | ||||
|         # all rspec tests run inside of a transaction. | ||||
|         # This mocks the false positive. | ||||
|         allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false) # rubocop:disable Database/MultipleDatabases | ||||
|       end | ||||
| 
 | ||||
|       it 'logs all the counts' do | ||||
|         expect(worker).to receive(:log_extra_metadata_on_done).with(:default_count, 3) | ||||
|         expect(worker).to receive(:log_extra_metadata_on_done).with(:pre_importing_count, 1) | ||||
|         expect(worker).to receive(:log_extra_metadata_on_done).with(:pre_import_done_count, 1) | ||||
|         expect(worker).to receive(:log_extra_metadata_on_done).with(:importing_count, 2) | ||||
|         expect(worker).to receive(:log_extra_metadata_on_done).with(:import_done_count, 0) | ||||
|         expect(worker).to receive(:log_extra_metadata_on_done).with(:import_aborted_count, 1) | ||||
|         expect(worker).to receive(:log_extra_metadata_on_done).with(:import_skipped_count, 0) | ||||
| 
 | ||||
|         subject | ||||
|       end | ||||
| 
 | ||||
|       context 'with load balancing enabled', :db_load_balancing do | ||||
|         it 'uses the replica' do | ||||
|           expect(Gitlab::Database::LoadBalancing::Session.current).to receive(:use_replicas_for_read_queries).and_call_original | ||||
| 
 | ||||
|           subject | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -32,6 +32,21 @@ RSpec.describe Projects::GitGarbageCollectWorker do | |||
| 
 | ||||
|         subject.perform(*params) | ||||
|       end | ||||
| 
 | ||||
|       context 'when deduplication service runs into a GRPC internal error' do | ||||
|         before do | ||||
|           allow_next_instance_of(::Projects::GitDeduplicationService) do |instance| | ||||
|             expect(instance).to receive(:execute).and_raise(GRPC::Internal) | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         it_behaves_like 'can collect git garbage' do | ||||
|           let(:resource) { project } | ||||
|           let(:statistics_service_klass) { Projects::UpdateStatisticsService } | ||||
|           let(:statistics_keys) { [:repository_size, :lfs_objects_size] } | ||||
|           let(:expected_default_lease) { "projects:#{resource.id}" } | ||||
|         end | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'LFS object garbage collection' do | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue