Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									621bb6eed5
								
							
						
					
					
						commit
						731c36f7b6
					
				|  | @ -651,6 +651,11 @@ rspec:merge-auto-explain-logs: | ||||||
|   needs: !reference ["rspec:coverage", "needs"] |   needs: !reference ["rspec:coverage", "needs"] | ||||||
|   script: |   script: | ||||||
|     - scripts/merge-auto-explain-logs |     - scripts/merge-auto-explain-logs | ||||||
|  |     - | | ||||||
|  |       if [[ -f "$RSPEC_AUTO_EXPLAIN_LOG_PATH" && "$CI_COMMIT_REF_NAME" == "$CI_DEFAULT_BRANCH" ]]; then | ||||||
|  |         source scripts/gitlab_component_helpers.sh | ||||||
|  |         create_and_upload_auto_explain_package | ||||||
|  |       fi | ||||||
|   artifacts: |   artifacts: | ||||||
|     name: auto-explain-logs |     name: auto-explain-logs | ||||||
|     expire_in: 31d |     expire_in: 31d | ||||||
|  | @ -929,6 +934,31 @@ rspec-ee system pg16 single-db-sec-connection: | ||||||
|     - .rspec-ee-system-parallel |     - .rspec-ee-system-parallel | ||||||
|     - .rails:rules:single-db-sec-connection-ee |     - .rails:rules:single-db-sec-connection-ee | ||||||
| 
 | 
 | ||||||
|  | # Integration tests with Elasticsearch and PG production versions | ||||||
|  | # These run on merge requests which meet certain conditions and the nightly pipeline | ||||||
|  | rspec-ee unit pg16 es8: | ||||||
|  |   extends: | ||||||
|  |     - .rspec-ee-base-pg16-es8 | ||||||
|  |     - .rspec-ee-unit-parallel | ||||||
|  |   rules: | ||||||
|  |     - !reference [".rails:rules:run-search-tests", rules] | ||||||
|  |     - !reference [".rails:rules:default-branch-schedule-nightly--code-backstage-ee-only", rules] | ||||||
|  | rspec-ee integration pg16 es8: | ||||||
|  |   extends: | ||||||
|  |     - .rspec-ee-base-pg16-es8 | ||||||
|  |     - .rspec-ee-integration-parallel | ||||||
|  |   rules: | ||||||
|  |     - !reference [".rails:rules:run-search-tests", rules] | ||||||
|  |     - !reference [".rails:rules:default-branch-schedule-nightly--code-backstage-ee-only", rules] | ||||||
|  | 
 | ||||||
|  | rspec-ee system pg16 es8: | ||||||
|  |   extends: | ||||||
|  |     - .rspec-ee-base-pg16-es8 | ||||||
|  |     - .rspec-ee-system-parallel | ||||||
|  |   rules: | ||||||
|  |     - !reference [".rails:rules:run-search-tests", rules] | ||||||
|  |     - !reference [".rails:rules:default-branch-schedule-nightly--code-backstage-ee-only", rules] | ||||||
|  | 
 | ||||||
| # EE: default refs (MRs, default branch, schedules) jobs # | # EE: default refs (MRs, default branch, schedules) jobs # | ||||||
| ################################################## | ################################################## | ||||||
| 
 | 
 | ||||||
|  | @ -1108,8 +1138,8 @@ rspec-ee system pg15: | ||||||
|     - .rspec-ee-system-parallel |     - .rspec-ee-system-parallel | ||||||
| 
 | 
 | ||||||
| # PG16 | # PG16 | ||||||
| # Integration tests with Elastic Search and the actual PG production version (PG16) | # Integration tests with supported Elasticsearch and OpenSearch versions | ||||||
| # https://gitlab.com/gitlab-org/quality/engineering-productivity/team/-/issues/534 | # and the actual PG production version (PG16) | ||||||
| rspec-ee unit pg16 opensearch1: | rspec-ee unit pg16 opensearch1: | ||||||
|   extends: |   extends: | ||||||
|     - .rspec-ee-base-pg16-opensearch1 |     - .rspec-ee-base-pg16-opensearch1 | ||||||
|  | @ -1146,23 +1176,7 @@ rspec-ee system pg16 opensearch2: | ||||||
|     - .rspec-ee-system-parallel |     - .rspec-ee-system-parallel | ||||||
|     - .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only |     - .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only | ||||||
| 
 | 
 | ||||||
| rspec-ee unit pg16 es8: | # Production version tests for ES and PG in nightly pipeline are defined in the previous section | ||||||
|   extends: |  | ||||||
|     - .rspec-ee-base-pg16-es8 |  | ||||||
|     - .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only |  | ||||||
|     - .rspec-ee-unit-parallel |  | ||||||
| 
 |  | ||||||
| rspec-ee integration pg16 es8: |  | ||||||
|   extends: |  | ||||||
|     - .rspec-ee-base-pg16-es8 |  | ||||||
|     - .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only |  | ||||||
|     - .rspec-ee-integration-parallel |  | ||||||
| 
 |  | ||||||
| rspec-ee system pg16 es8: |  | ||||||
|   extends: |  | ||||||
|     - .rspec-ee-base-pg16-es8 |  | ||||||
|     - .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only |  | ||||||
|     - .rspec-ee-system-parallel |  | ||||||
| 
 | 
 | ||||||
| # PG17 | # PG17 | ||||||
| # Note: PG17 brought us close to the limit of nightly jobs, with 1946 out of 2000. | # Note: PG17 brought us close to the limit of nightly jobs, with 1946 out of 2000. | ||||||
|  |  | ||||||
|  | @ -108,6 +108,9 @@ | ||||||
| .if-merge-request-labels-run-all-e2e: &if-merge-request-labels-run-all-e2e | .if-merge-request-labels-run-all-e2e: &if-merge-request-labels-run-all-e2e | ||||||
|   if: '($CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_EVENT_TYPE != "merge_train") && $CI_MERGE_REQUEST_LABELS =~ /pipeline:run-all-e2e/' |   if: '($CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_EVENT_TYPE != "merge_train") && $CI_MERGE_REQUEST_LABELS =~ /pipeline:run-all-e2e/' | ||||||
| 
 | 
 | ||||||
|  | .if-merge-request-labels-run-search-tests: &if-merge-request-labels-run-search-tests | ||||||
|  |   if: '($CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_EVENT_TYPE != "merge_train") && $CI_MERGE_REQUEST_LABELS =~ /pipeline:run-search-tests/' | ||||||
|  | 
 | ||||||
| .if-merge-request-labels-run-observability-e2e-tests-main-branch: &if-merge-request-labels-run-observability-e2e-tests-main-branch | .if-merge-request-labels-run-observability-e2e-tests-main-branch: &if-merge-request-labels-run-observability-e2e-tests-main-branch | ||||||
|   if: '($CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_EVENT_TYPE != "merge_train") && $CI_MERGE_REQUEST_LABELS =~ /pipeline:run-observability-e2e-tests-main-branch/' |   if: '($CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_EVENT_TYPE != "merge_train") && $CI_MERGE_REQUEST_LABELS =~ /pipeline:run-observability-e2e-tests-main-branch/' | ||||||
| 
 | 
 | ||||||
|  | @ -797,11 +800,14 @@ | ||||||
| 
 | 
 | ||||||
| .rails:rules:run-search-tests: | .rails:rules:run-search-tests: | ||||||
|   rules: |   rules: | ||||||
|     - !reference [".rails:rules:default-branch-schedule-nightly--code-backstage-ee-only", rules] |     - <<: *if-merge-request-labels-run-search-tests | ||||||
|     - <<: *if-merge-request-labels-group-global-search |       changes: *ci-patterns | ||||||
|       changes: *search-backend-patterns |  | ||||||
|     - <<: *if-merge-request-labels-group-global-search |     - <<: *if-merge-request-labels-group-global-search | ||||||
|       changes: *ci-patterns |       changes: *ci-patterns | ||||||
|  |     - <<: *if-merge-request-labels-run-search-tests | ||||||
|  |       changes: *search-backend-patterns | ||||||
|  |     - <<: *if-merge-request-labels-group-global-search | ||||||
|  |       changes: *search-backend-patterns | ||||||
| 
 | 
 | ||||||
| .rails:rules:ee-and-foss-default-rules: | .rails:rules:ee-and-foss-default-rules: | ||||||
|   rules: |   rules: | ||||||
|  |  | ||||||
|  | @ -1 +1 @@ | ||||||
| a9cdf8a33308b7734ea08810cb9be586b696c3fb | 29c3fd80cb7a73cb76ac1aaedb61e01289148e47 | ||||||
|  |  | ||||||
|  | @ -127,6 +127,9 @@ export default { | ||||||
|     removeButtonCategory() { |     removeButtonCategory() { | ||||||
|       return this.isMobile ? 'secondary' : 'tertiary'; |       return this.isMobile ? 'secondary' : 'tertiary'; | ||||||
|     }, |     }, | ||||||
|  |     removeButtonSize() { | ||||||
|  |       return this.isMobile ? 'medium' : 'small'; | ||||||
|  |     }, | ||||||
|     variables() { |     variables() { | ||||||
|       return this.form[this.refParam]?.variables ?? []; |       return this.form[this.refParam]?.variables ?? []; | ||||||
|     }, |     }, | ||||||
|  | @ -340,9 +343,9 @@ export default { | ||||||
|             <template v-if="variables.length > 1"> |             <template v-if="variables.length > 1"> | ||||||
|               <gl-button |               <gl-button | ||||||
|                 v-if="canRemove(index)" |                 v-if="canRemove(index)" | ||||||
|                 size="small" |  | ||||||
|                 class="gl-shrink-0" |                 class="gl-shrink-0" | ||||||
|                 data-testid="remove-ci-variable-row" |                 data-testid="remove-ci-variable-row" | ||||||
|  |                 :size="removeButtonSize" | ||||||
|                 :category="removeButtonCategory" |                 :category="removeButtonCategory" | ||||||
|                 :aria-label="s__('CiVariables|Remove variable')" |                 :aria-label="s__('CiVariables|Remove variable')" | ||||||
|                 @click="removeVariable(index)" |                 @click="removeVariable(index)" | ||||||
|  |  | ||||||
|  | @ -5,7 +5,9 @@ import { | ||||||
|   GlFormGroup, |   GlFormGroup, | ||||||
|   GlFormInput, |   GlFormInput, | ||||||
|   GlFormTextarea, |   GlFormTextarea, | ||||||
|  |   GlIcon, | ||||||
| } from '@gitlab/ui'; | } from '@gitlab/ui'; | ||||||
|  | import { GlBreakpointInstance } from '@gitlab/ui/dist/utils'; | ||||||
| import { __ } from '~/locale'; | import { __ } from '~/locale'; | ||||||
| import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; | import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; | ||||||
| import InputsAdoptionBanner from '~/ci/common/pipeline_inputs/inputs_adoption_banner.vue'; | import InputsAdoptionBanner from '~/ci/common/pipeline_inputs/inputs_adoption_banner.vue'; | ||||||
|  | @ -21,6 +23,7 @@ export default { | ||||||
|     GlFormInput, |     GlFormInput, | ||||||
|     GlFormTextarea, |     GlFormTextarea, | ||||||
|     InputsAdoptionBanner, |     InputsAdoptionBanner, | ||||||
|  |     GlIcon, | ||||||
|   }, |   }, | ||||||
|   mixins: [glFeatureFlagsMixin()], |   mixins: [glFeatureFlagsMixin()], | ||||||
|   props: { |   props: { | ||||||
|  | @ -41,7 +44,7 @@ export default { | ||||||
|       showVarValues: false, |       showVarValues: false, | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
|   formElementClasses: 'md:gl-mr-3 gl-mb-3 gl-basis-1/4 gl-shrink-0 gl-flex-grow-0', |   formElementClasses: '!gl-block gl-basis-1/4 gl-shrink-0 gl-flex-grow-0', | ||||||
|   // it's used to prevent the overwrite if 'gl-h-7' or '!gl-h-7' were used |   // it's used to prevent the overwrite if 'gl-h-7' or '!gl-h-7' were used | ||||||
|   textAreaStyle: { height: '32px' }, |   textAreaStyle: { height: '32px' }, | ||||||
|   typeOptions: [ |   typeOptions: [ | ||||||
|  | @ -67,6 +70,15 @@ export default { | ||||||
|     showVarSecurityBtn() { |     showVarSecurityBtn() { | ||||||
|       return this.editing && this.hasExistingScheduleVariables; |       return this.editing && this.hasExistingScheduleVariables; | ||||||
|     }, |     }, | ||||||
|  |     isMobile() { | ||||||
|  |       return ['sm', 'xs'].includes(GlBreakpointInstance.getBreakpointSize()); | ||||||
|  |     }, | ||||||
|  |     removeButtonCategory() { | ||||||
|  |       return this.isMobile ? 'secondary' : 'tertiary'; | ||||||
|  |     }, | ||||||
|  |     removeButtonSize() { | ||||||
|  |       return this.isMobile ? 'medium' : 'small'; | ||||||
|  |     }, | ||||||
|   }, |   }, | ||||||
|   watch: { |   watch: { | ||||||
|     variables: { |     variables: { | ||||||
|  | @ -133,10 +145,10 @@ export default { | ||||||
|         class="gl-mt-0" |         class="gl-mt-0" | ||||||
|         :feature-name="$options.userCalloutsFeatureName" |         :feature-name="$options.userCalloutsFeatureName" | ||||||
|       /> |       /> | ||||||
|       <div v-for="(variable, index) in variables" :key="`var-${index}`"> |       <div v-for="(variable, index) in variables" :key="`var-${index}`" class="gl-mb-4"> | ||||||
|         <div |         <div | ||||||
|           v-if="!variable.destroy" |           v-if="!variable.destroy" | ||||||
|           class="gl-mb-3 gl-flex gl-flex-col gl-items-stretch gl-pb-2 md:gl-flex-row md:gl-items-start" |           class="gl-flex gl-flex-col gl-items-stretch gl-gap-4 md:gl-flex-row" | ||||||
|           data-testid="ci-variable-row" |           data-testid="ci-variable-row" | ||||||
|         > |         > | ||||||
|           <gl-collapsible-listbox |           <gl-collapsible-listbox | ||||||
|  | @ -163,7 +175,7 @@ export default { | ||||||
|             v-if="displayHiddenChars(variable)" |             v-if="displayHiddenChars(variable)" | ||||||
|             value="*****************" |             value="*****************" | ||||||
|             disabled |             disabled | ||||||
|             class="gl-mb-3 !gl-h-7" |             class="!gl-h-7" | ||||||
|             data-testid="pipeline-form-ci-variable-hidden-value" |             data-testid="pipeline-form-ci-variable-hidden-value" | ||||||
|           /> |           /> | ||||||
| 
 | 
 | ||||||
|  | @ -172,7 +184,7 @@ export default { | ||||||
|             v-model="variable.value" |             v-model="variable.value" | ||||||
|             :placeholder="s__('CiVariables|Input variable value')" |             :placeholder="s__('CiVariables|Input variable value')" | ||||||
|             :aria-label="s__('CiVariables|Input variable value')" |             :aria-label="s__('CiVariables|Input variable value')" | ||||||
|             class="gl-mb-3 gl-min-h-7" |             class="gl-min-h-7" | ||||||
|             :style="$options.textAreaStyle" |             :style="$options.textAreaStyle" | ||||||
|             :no-resize="false" |             :no-resize="false" | ||||||
|             data-testid="pipeline-form-ci-variable-value" |             data-testid="pipeline-form-ci-variable-value" | ||||||
|  | @ -182,17 +194,19 @@ export default { | ||||||
|           <template v-if="variables.length > 1"> |           <template v-if="variables.length > 1"> | ||||||
|             <gl-button |             <gl-button | ||||||
|               v-if="canRemove(index)" |               v-if="canRemove(index)" | ||||||
|               class="gl-mb-3 md:gl-ml-3" |               class="gl-shrink-0" | ||||||
|               data-testid="remove-ci-variable-row" |               data-testid="remove-ci-variable-row" | ||||||
|               variant="danger" |               :size="removeButtonSize" | ||||||
|               category="secondary" |               :category="removeButtonCategory" | ||||||
|               icon="clear" |  | ||||||
|               :aria-label="s__('CiVariables|Remove variable')" |               :aria-label="s__('CiVariables|Remove variable')" | ||||||
|               @click="removeVariable(index)" |               @click="removeVariable(index)" | ||||||
|             /> |             > | ||||||
|  |               <gl-icon class="!gl-mr-0" name="remove" /> | ||||||
|  |               <span class="md:gl-hidden">{{ s__('CiVariables|Remove variable') }}</span> | ||||||
|  |             </gl-button> | ||||||
|             <gl-button |             <gl-button | ||||||
|               v-else |               v-else | ||||||
|               class="gl-invisible gl-mb-3 gl-hidden md:gl-ml-3 md:gl-block" |               class="gl-invisible gl-hidden md:gl-block" | ||||||
|               icon="clear" |               icon="clear" | ||||||
|               :aria-label="s__('CiVariables|Remove variable')" |               :aria-label="s__('CiVariables|Remove variable')" | ||||||
|             /> |             /> | ||||||
|  |  | ||||||
|  | @ -1,57 +0,0 @@ | ||||||
| import Sortable from 'sortablejs'; |  | ||||||
| import { createAlert } from '~/alert'; |  | ||||||
| import axios from '~/lib/utils/axios_utils'; |  | ||||||
| import { s__ } from '~/locale'; |  | ||||||
| import { getSortableDefaultOptions, sortableStart } from '~/sortable/utils'; |  | ||||||
| 
 |  | ||||||
| const updateIssue = (url, { move_before_id, move_after_id }) => |  | ||||||
|   axios |  | ||||||
|     .put(`${url}/reorder`, { |  | ||||||
|       move_before_id, |  | ||||||
|       move_after_id, |  | ||||||
|     }) |  | ||||||
|     .catch(() => { |  | ||||||
|       createAlert({ |  | ||||||
|         message: s__("ManualOrdering|Couldn't save the order of the issues"), |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
| const initManualOrdering = () => { |  | ||||||
|   const issueList = document.querySelector('.manual-ordering'); |  | ||||||
| 
 |  | ||||||
|   if (!issueList || !(gon.current_user_id > 0)) { |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   Sortable.create( |  | ||||||
|     issueList, |  | ||||||
|     getSortableDefaultOptions({ |  | ||||||
|       scroll: true, |  | ||||||
|       fallbackTolerance: 1, |  | ||||||
|       dataIdAttr: 'data-id', |  | ||||||
|       fallbackOnBody: false, |  | ||||||
|       group: { |  | ||||||
|         name: 'issues', |  | ||||||
|       }, |  | ||||||
|       draggable: 'li.issue', |  | ||||||
|       onStart: () => { |  | ||||||
|         sortableStart(); |  | ||||||
|       }, |  | ||||||
|       onUpdate: (event) => { |  | ||||||
|         const el = event.item; |  | ||||||
| 
 |  | ||||||
|         const url = el.getAttribute('url'); |  | ||||||
| 
 |  | ||||||
|         const prev = el.previousElementSibling; |  | ||||||
|         const next = el.nextElementSibling; |  | ||||||
| 
 |  | ||||||
|         const beforeId = prev && parseInt(prev.dataset.id, 10); |  | ||||||
|         const afterId = next && parseInt(next.dataset.id, 10); |  | ||||||
| 
 |  | ||||||
|         updateIssue(url, { move_after_id: afterId, move_before_id: beforeId }); |  | ||||||
|       }, |  | ||||||
|     }), |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| export default initManualOrdering; |  | ||||||
|  | @ -1,21 +1,5 @@ | ||||||
| import { createFilteredSearchTokenKeys } from '~/filtered_search/issuable_filtered_search_token_keys'; |  | ||||||
| import { mountIssuesDashboardApp } from '~/issues/dashboard'; | import { mountIssuesDashboardApp } from '~/issues/dashboard'; | ||||||
| import initManualOrdering from '~/issues/manual_ordering'; |  | ||||||
| import { FILTERED_SEARCH } from '~/filtered_search/constants'; |  | ||||||
| import initFilteredSearch from '~/pages/search/init_filtered_search'; |  | ||||||
| import { initNewResourceDropdown } from '~/vue_shared/components/new_resource_dropdown/init_new_resource_dropdown'; | import { initNewResourceDropdown } from '~/vue_shared/components/new_resource_dropdown/init_new_resource_dropdown'; | ||||||
| 
 | 
 | ||||||
| const IssuableFilteredSearchTokenKeys = createFilteredSearchTokenKeys({ |  | ||||||
|   disableReleaseFilter: true, |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| initFilteredSearch({ |  | ||||||
|   page: FILTERED_SEARCH.ISSUES, |  | ||||||
|   filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys, |  | ||||||
|   useDefaultState: true, |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| initNewResourceDropdown(); | initNewResourceDropdown(); | ||||||
| initManualOrdering(); |  | ||||||
| 
 |  | ||||||
| mountIssuesDashboardApp(); | mountIssuesDashboardApp(); | ||||||
|  |  | ||||||
|  | @ -70,6 +70,14 @@ export default { | ||||||
|     isLoading() { |     isLoading() { | ||||||
|       return this.$apollo.queries.wikiPage.loading; |       return this.$apollo.queries.wikiPage.loading; | ||||||
|     }, |     }, | ||||||
|  |     queryData() { | ||||||
|  |       const { defaultClient: cache } = this.$apollo.provider.clients; | ||||||
|  | 
 | ||||||
|  |       return cache.readQuery({ | ||||||
|  |         query: wikiPageQuery, | ||||||
|  |         variables: this.queryVariables, | ||||||
|  |       }); | ||||||
|  |     }, | ||||||
|   }, |   }, | ||||||
|   mounted() { |   mounted() { | ||||||
|     eventHub.$on(EVENT_EDIT_WIKI_START, () => { |     eventHub.$on(EVENT_EDIT_WIKI_START, () => { | ||||||
|  | @ -87,35 +95,9 @@ export default { | ||||||
|     removePlaceholder() { |     removePlaceholder() { | ||||||
|       this.placeholderNote = {}; |       this.placeholderNote = {}; | ||||||
|     }, |     }, | ||||||
|     async updateDiscussions(discussion) { |  | ||||||
|       // apollo does not update cache when a discussion is added so we have to do it manually |  | ||||||
|       if (!this.$apollo.provider) return; |  | ||||||
|       const { defaultClient: cache } = this.$apollo.provider.clients; |  | ||||||
|       const queryData = cache.readQuery({ |  | ||||||
|         query: wikiPageQuery, |  | ||||||
|         variables: this.queryVariables, |  | ||||||
|       }); |  | ||||||
|       const data = produce(queryData, (draft) => { |  | ||||||
|         draft.wikiPage.discussions.nodes.push({ |  | ||||||
|           ...discussion, |  | ||||||
|           replyId: null, |  | ||||||
|           resolvable: false, |  | ||||||
|           resolved: false, |  | ||||||
|           resolvedAt: null, |  | ||||||
|           resolvedBy: null, |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
| 
 |  | ||||||
|       cache.writeQuery({ |  | ||||||
|         query: wikiPageQuery, |  | ||||||
|         variables: this.queryVariables, |  | ||||||
|         data, |  | ||||||
|       }); |  | ||||||
|     }, |  | ||||||
|     getDiscussionKey(key, stringModifier) { |     getDiscussionKey(key, stringModifier) { | ||||||
|       return [key, stringModifier].join('-'); |       return [key, stringModifier].join('-'); | ||||||
|     }, |     }, | ||||||
| 
 |  | ||||||
|     handleDeleteNote(noteId, discussionId) { |     handleDeleteNote(noteId, discussionId) { | ||||||
|       const discussionIndex = this.discussions.findIndex( |       const discussionIndex = this.discussions.findIndex( | ||||||
|         (discussion) => discussion.id === discussionId, |         (discussion) => discussion.id === discussionId, | ||||||
|  | @ -123,15 +105,74 @@ export default { | ||||||
| 
 | 
 | ||||||
|       if (discussionIndex === -1) return; |       if (discussionIndex === -1) return; | ||||||
| 
 | 
 | ||||||
|       if (this.discussions[discussionIndex].notes.nodes.length === 1) { |       const discussion = this.discussions[discussionIndex]; | ||||||
|  |       const isLastNote = discussion.notes.nodes.length === 1; | ||||||
|  | 
 | ||||||
|  |       // Update local state | ||||||
|  |       if (isLastNote) { | ||||||
|  |         // Remove entire discussion if it's the last note | ||||||
|         this.discussions = this.discussions.filter(({ id }) => id !== discussionId); |         this.discussions = this.discussions.filter(({ id }) => id !== discussionId); | ||||||
|       } else { |       } else { | ||||||
|         const updatedNotes = this.discussions[discussionIndex].notes.nodes.filter( |         // Remove only the specific note | ||||||
|  |         this.discussions[discussionIndex].notes.nodes = discussion.notes.nodes.filter( | ||||||
|           ({ id }) => id !== noteId, |           ({ id }) => id !== noteId, | ||||||
|         ); |         ); | ||||||
| 
 |  | ||||||
|         this.discussions[discussionIndex].notes.nodes = updatedNotes; |  | ||||||
|       } |       } | ||||||
|  | 
 | ||||||
|  |       this.updateCache({ discussionId, noteId, isLastNote }); | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     updateCache({ discussion, discussionId, noteId, isLastNote }) { | ||||||
|  |       if (!this.$apollo.provider) return; | ||||||
|  |       const { defaultClient: cache } = this.$apollo.provider.clients; | ||||||
|  | 
 | ||||||
|  |       const queryData = cache.readQuery({ | ||||||
|  |         query: wikiPageQuery, | ||||||
|  |         variables: this.queryVariables, | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       if (!queryData) return; | ||||||
|  | 
 | ||||||
|  |       let data; | ||||||
|  |       if (discussion) { | ||||||
|  |         data = produce(queryData, (draft) => { | ||||||
|  |           draft.wikiPage.discussions.nodes.push({ | ||||||
|  |             ...discussion, | ||||||
|  |             replyId: null, | ||||||
|  |             resolvable: false, | ||||||
|  |             resolved: false, | ||||||
|  |             resolvedAt: null, | ||||||
|  |             resolvedBy: null, | ||||||
|  |           }); | ||||||
|  |         }); | ||||||
|  |       } else { | ||||||
|  |         data = produce(queryData, (draft) => { | ||||||
|  |           const cachedDiscussionIndex = draft.wikiPage.discussions.nodes.findIndex( | ||||||
|  |             (d) => d.id === discussionId, | ||||||
|  |           ); | ||||||
|  | 
 | ||||||
|  |           if (cachedDiscussionIndex === -1) return; | ||||||
|  | 
 | ||||||
|  |           if (isLastNote) { | ||||||
|  |             // Remove entire discussion if it's the last note | ||||||
|  |             draft.wikiPage.discussions.nodes = draft.wikiPage.discussions.nodes.filter( | ||||||
|  |               (d) => d.id !== discussionId, | ||||||
|  |             ); | ||||||
|  |           } else { | ||||||
|  |             // Remove only the specific note | ||||||
|  |             draft.wikiPage.discussions.nodes[cachedDiscussionIndex].notes.nodes = | ||||||
|  |               draft.wikiPage.discussions.nodes[cachedDiscussionIndex].notes.nodes.filter( | ||||||
|  |                 (note) => note.id !== noteId, | ||||||
|  |               ); | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       cache.writeQuery({ | ||||||
|  |         query: wikiPageQuery, | ||||||
|  |         variables: this.queryVariables, | ||||||
|  |         data, | ||||||
|  |       }); | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
|  | @ -147,7 +188,7 @@ export default { | ||||||
|           :note-id="noteableId" |           :note-id="noteableId" | ||||||
|           @creating-note:start="setPlaceHolderNote" |           @creating-note:start="setPlaceHolderNote" | ||||||
|           @creating-note:done="removePlaceholder" |           @creating-note:done="removePlaceholder" | ||||||
|           @creating-note:success="updateDiscussions" |           @creating-note:success="(discussion) => updateCache({ discussion })" | ||||||
|         /> |         /> | ||||||
|       </template> |       </template> | ||||||
|       <template v-if="placeholderNote.body" #place-holder-note> |       <template v-if="placeholderNote.body" #place-holder-note> | ||||||
|  |  | ||||||
|  | @ -8,6 +8,7 @@ import { SCOPE_BLOB, SEARCH_TYPE_ZOEKT } from '~/search/sidebar/constants/index' | ||||||
| import { parseBoolean } from '~/lib/utils/common_utils'; | import { parseBoolean } from '~/lib/utils/common_utils'; | ||||||
| import { DEFAULT_FETCH_CHUNKS } from '../constants'; | import { DEFAULT_FETCH_CHUNKS } from '../constants'; | ||||||
| import { RECEIVE_NAVIGATION_COUNT } from '../../store/mutation_types'; | import { RECEIVE_NAVIGATION_COUNT } from '../../store/mutation_types'; | ||||||
|  | import EmptyResult from './result_empty.vue'; | ||||||
| import StatusBar from './status_bar.vue'; | import StatusBar from './status_bar.vue'; | ||||||
| 
 | 
 | ||||||
| import ZoektBlobResults from './zoekt_blob_results.vue'; | import ZoektBlobResults from './zoekt_blob_results.vue'; | ||||||
|  | @ -24,6 +25,7 @@ export default { | ||||||
|     ZoektBlobResults, |     ZoektBlobResults, | ||||||
|     StatusBar, |     StatusBar, | ||||||
|     GlAlert, |     GlAlert, | ||||||
|  |     EmptyResult, | ||||||
|   }, |   }, | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|  | @ -96,12 +98,14 @@ export default { | ||||||
|       {{ $options.i18n.blobDataFetchError }} |       {{ $options.i18n.blobDataFetchError }} | ||||||
|     </gl-alert> |     </gl-alert> | ||||||
|     <section v-else-if="isBlobScope && isZoektSearch"> |     <section v-else-if="isBlobScope && isZoektSearch"> | ||||||
|       <status-bar :blob-search="blobSearch" :has-results="hasResults" :is-loading="isLoading" /> |       <status-bar v-if="!isLoading && !hasError" :blob-search="blobSearch" /> | ||||||
|       <zoekt-blob-results |       <zoekt-blob-results | ||||||
|  |         v-if="hasResults" | ||||||
|         :blob-search="blobSearch" |         :blob-search="blobSearch" | ||||||
|         :has-results="hasResults" |         :has-results="hasResults" | ||||||
|         :is-loading="isLoading" |         :is-loading="isLoading" | ||||||
|       /> |       /> | ||||||
|  |       <empty-result v-if="!hasResults && !isLoading" /> | ||||||
|     </section> |     </section> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
|  | @ -19,19 +19,6 @@ export default { | ||||||
|       type: Object, |       type: Object, | ||||||
|       required: true, |       required: true, | ||||||
|     }, |     }, | ||||||
|     hasResults: { |  | ||||||
|       type: Boolean, |  | ||||||
|       required: true, |  | ||||||
|     }, |  | ||||||
|     isLoading: { |  | ||||||
|       type: Boolean, |  | ||||||
|       required: true, |  | ||||||
|     }, |  | ||||||
|     error: { |  | ||||||
|       type: String, |  | ||||||
|       required: false, |  | ||||||
|       default: '', |  | ||||||
|     }, |  | ||||||
|   }, |   }, | ||||||
|   computed: { |   computed: { | ||||||
|     ...mapState(['query', 'groupInitialJson', 'projectInitialJson', 'repositoryRef']), |     ...mapState(['query', 'groupInitialJson', 'projectInitialJson', 'repositoryRef']), | ||||||
|  | @ -53,9 +40,6 @@ export default { | ||||||
|     resultsTotal() { |     resultsTotal() { | ||||||
|       return this.blobSearch?.matchCount; |       return this.blobSearch?.matchCount; | ||||||
|     }, |     }, | ||||||
|     showBar() { |  | ||||||
|       return this.hasResults && !this.hasError && !this.isLoading; |  | ||||||
|     }, |  | ||||||
|     getBaseURL() { |     getBaseURL() { | ||||||
|       return getBaseURL(); |       return getBaseURL(); | ||||||
|     }, |     }, | ||||||
|  | @ -80,9 +64,6 @@ export default { | ||||||
|         this?.resultsTotal ?? 0, |         this?.resultsTotal ?? 0, | ||||||
|       ); |       ); | ||||||
|     }, |     }, | ||||||
|     hasError() { |  | ||||||
|       return Boolean(this.error); |  | ||||||
|     }, |  | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     handleInput(selected) { |     handleInput(selected) { | ||||||
|  | @ -93,7 +74,7 @@ export default { | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|   <div v-if="showBar" class="search-results-status gl-my-4"> |   <div class="search-results-status gl-my-4"> | ||||||
|     <gl-sprintf v-if="!query.project_id && !query.group_id" :message="resultsSimple"> |     <gl-sprintf v-if="!query.project_id && !query.group_id" :message="resultsSimple"> | ||||||
|       <template #resultsTotal>{{ resultsTotal }}</template> |       <template #resultsTotal>{{ resultsTotal }}</template> | ||||||
|       <template #term |       <template #term | ||||||
|  |  | ||||||
|  | @ -5,7 +5,7 @@ import { mapState, mapActions } from 'vuex'; | ||||||
| import BlobHeader from '~/search/results/components/blob_header.vue'; | import BlobHeader from '~/search/results/components/blob_header.vue'; | ||||||
| import BlobFooter from '~/search/results/components/blob_footer.vue'; | import BlobFooter from '~/search/results/components/blob_footer.vue'; | ||||||
| import BlobBody from '~/search/results/components/blob_body.vue'; | import BlobBody from '~/search/results/components/blob_body.vue'; | ||||||
| import EmptyResult from '~/search/results/components/result_empty.vue'; | 
 | ||||||
| import { | import { | ||||||
|   getSystemColorScheme, |   getSystemColorScheme, | ||||||
|   listenSystemColorSchemeChange, |   listenSystemColorSchemeChange, | ||||||
|  | @ -22,7 +22,6 @@ export default { | ||||||
|     BlobFooter, |     BlobFooter, | ||||||
|     BlobBody, |     BlobBody, | ||||||
|     GlPagination, |     GlPagination, | ||||||
|     EmptyResult, |  | ||||||
|     GlLoadingIcon, |     GlLoadingIcon, | ||||||
|   }, |   }, | ||||||
|   props: { |   props: { | ||||||
|  | @ -125,7 +124,6 @@ export default { | ||||||
|         </template> |         </template> | ||||||
|       </gl-card> |       </gl-card> | ||||||
|     </div> |     </div> | ||||||
|     <empty-result v-else-if="!hasResults && !isLoading" /> |  | ||||||
|     <template v-if="hasResults && !isLoading"> |     <template v-if="hasResults && !isLoading"> | ||||||
|       <gl-pagination |       <gl-pagination | ||||||
|         v-model="pagination" |         v-model="pagination" | ||||||
|  |  | ||||||
|  | @ -35,6 +35,12 @@ export default { | ||||||
|         8, |         8, | ||||||
|       ); |       ); | ||||||
| 
 | 
 | ||||||
|  |       const dayOfWeek = this.currentTime.getDay(); | ||||||
|  |       const daysUntilMonday = dayOfWeek === 0 ? 1 : 8 - dayOfWeek; | ||||||
|  |       const untilNextWeek = new Date(this.currentTime); | ||||||
|  |       untilNextWeek.setDate(this.currentTime.getDate() + daysUntilMonday); | ||||||
|  |       untilNextWeek.setHours(8, 0, 0, 0); | ||||||
|  | 
 | ||||||
|       const toTimeString = (date) => localeDateFormat.asTime.format(date); |       const toTimeString = (date) => localeDateFormat.asTime.format(date); | ||||||
| 
 | 
 | ||||||
|       return [ |       return [ | ||||||
|  | @ -81,6 +87,20 @@ export default { | ||||||
|                 this.$emit('snooze-until', untilTomorrow); |                 this.$emit('snooze-until', untilTomorrow); | ||||||
|               }, |               }, | ||||||
|             }, |             }, | ||||||
|  |             { | ||||||
|  |               text: s__('Todos|Until next week'), | ||||||
|  |               formattedDate: sprintf(s__('Todos|%{day}, %{time}'), { | ||||||
|  |                 day: dateFormat(untilNextWeek, 'DDDD'), | ||||||
|  |                 time: toTimeString(untilNextWeek), | ||||||
|  |               }), | ||||||
|  |               action: () => { | ||||||
|  |                 this.track(INSTRUMENT_TODO_ITEM_CLICK, { | ||||||
|  |                   label: 'snooze_until_next_week', | ||||||
|  |                 }); | ||||||
|  | 
 | ||||||
|  |                 this.$emit('snooze-until', untilNextWeek); | ||||||
|  |               }, | ||||||
|  |             }, | ||||||
|           ], |           ], | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|  |  | ||||||
|  | @ -10,7 +10,6 @@ module Ci | ||||||
|     before_validation :assign_project_id, on: :create |     before_validation :assign_project_id, on: :create | ||||||
| 
 | 
 | ||||||
|     validates :name, presence: true, length: { maximum: 255 }, uniqueness: { scope: :pipeline_schedule_id } |     validates :name, presence: true, length: { maximum: 255 }, uniqueness: { scope: :pipeline_schedule_id } | ||||||
|     validates :value, presence: true |  | ||||||
| 
 | 
 | ||||||
|     # We validate the size of the serialized value because encryption is expensive. |     # We validate the size of the serialized value because encryption is expensive. | ||||||
|     # The maximum permitted size is equivalent to the maximum size permitted for an interpolated input value. |     # The maximum permitted size is equivalent to the maximum size permitted for an interpolated input value. | ||||||
|  |  | ||||||
|  | @ -4,6 +4,16 @@ module Namespaces | ||||||
|   class ProjectNamespace < Namespace |   class ProjectNamespace < Namespace | ||||||
|     self.allow_legacy_sti_class = true |     self.allow_legacy_sti_class = true | ||||||
| 
 | 
 | ||||||
|  |     SYNCED_ATTRIBUTES = %w[ | ||||||
|  |       name | ||||||
|  |       path | ||||||
|  |       namespace_id | ||||||
|  |       namespace | ||||||
|  |       visibility_level | ||||||
|  |       shared_runners_enabled | ||||||
|  |       organization_id | ||||||
|  |     ].freeze | ||||||
|  | 
 | ||||||
|     # These aliases are added to make it easier to sync parent/parent_id attribute with |     # These aliases are added to make it easier to sync parent/parent_id attribute with | ||||||
|     # project.namespace/project.namespace_id attribute. |     # project.namespace/project.namespace_id attribute. | ||||||
|     # |     # | ||||||
|  | @ -37,11 +47,9 @@ module Namespaces | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def sync_attributes_from_project(project) |     def sync_attributes_from_project(project) | ||||||
|       attribute_list = %w[name path namespace_id namespace visibility_level shared_runners_enabled organization_id] |  | ||||||
| 
 |  | ||||||
|       attributes_to_sync = project |       attributes_to_sync = project | ||||||
|                              .changes |                              .changes | ||||||
|                              .slice(*attribute_list) |                              .slice(*SYNCED_ATTRIBUTES) | ||||||
|                              .transform_values { |val| val[1] } |                              .transform_values { |val| val[1] } | ||||||
| 
 | 
 | ||||||
|       # if visibility_level is not set explicitly for project, it defaults to 0, |       # if visibility_level is not set explicitly for project, it defaults to 0, | ||||||
|  |  | ||||||
|  | @ -3549,8 +3549,7 @@ class Project < ApplicationRecord | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def job_token_policies_enabled? |   def job_token_policies_enabled? | ||||||
|     Feature.enabled?(:add_policies_to_ci_job_token, self) || |     namespace.root_ancestor.namespace_settings&.job_token_policies_enabled? | ||||||
|       namespace.root_ancestor.namespace_settings&.job_token_policies_enabled? |  | ||||||
|   end |   end | ||||||
|   strong_memoize_attr :job_token_policies_enabled? |   strong_memoize_attr :job_token_policies_enabled? | ||||||
| 
 | 
 | ||||||
|  | @ -3792,7 +3791,7 @@ class Project < ApplicationRecord | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def sync_project_namespace? |   def sync_project_namespace? | ||||||
|     (changes.keys & %w[name path namespace_id namespace visibility_level shared_runners_enabled]).any? && project_namespace.present? |     (changes.keys & Namespaces::ProjectNamespace::SYNCED_ATTRIBUTES).any? && project_namespace.present? | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def reload_project_namespace_details |   def reload_project_namespace_details | ||||||
|  |  | ||||||
|  | @ -2815,7 +2815,7 @@ class User < ApplicationRecord | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def email_allowed_by_restrictions |   def email_allowed_by_restrictions | ||||||
|     return if placeholder? || import_user? |     return if placeholder? || import_user? || security_policy_bot? | ||||||
| 
 | 
 | ||||||
|     error = validate_admin_signup_restrictions(email) |     error = validate_admin_signup_restrictions(email) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -6,15 +6,21 @@ module Members | ||||||
|       raise Gitlab::Access::AccessDeniedError unless can_request_access?(source) |       raise Gitlab::Access::AccessDeniedError unless can_request_access?(source) | ||||||
| 
 | 
 | ||||||
|       source.members.create( |       source.members.create( | ||||||
|         access_level: Gitlab::Access::DEVELOPER, |         access_level: default_access_level, | ||||||
|         user: current_user, |         user: current_user, | ||||||
|         requested_at: Time.current.utc) |         requested_at: Time.current.utc) | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     private |     private | ||||||
| 
 | 
 | ||||||
|  |     def default_access_level | ||||||
|  |       Gitlab::Access::DEVELOPER | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|     def can_request_access?(source) |     def can_request_access?(source) | ||||||
|       can?(current_user, :request_access, source) |       can?(current_user, :request_access, source) | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  | 
 | ||||||
|  | Members::RequestAccessService.prepend_mod_with('Members::RequestAccessService') | ||||||
|  |  | ||||||
|  | @ -25,7 +25,7 @@ module Users | ||||||
| 
 | 
 | ||||||
|       return ::ServiceResponse.success(message: _('User has already been deactivated')) if user.deactivated? |       return ::ServiceResponse.success(message: _('User has already been deactivated')) if user.deactivated? | ||||||
| 
 | 
 | ||||||
|       unless user.can_be_deactivated? |       unless can_be_deactivated?(user) | ||||||
|         message = _( |         message = _( | ||||||
|           'The user you are trying to deactivate has been active in the past %{minimum_inactive_days} days ' \ |           'The user you are trying to deactivate has been active in the past %{minimum_inactive_days} days ' \ | ||||||
|           'and cannot be deactivated') |           'and cannot be deactivated') | ||||||
|  | @ -49,6 +49,11 @@ module Users | ||||||
| 
 | 
 | ||||||
|     attr_reader :current_user |     attr_reader :current_user | ||||||
| 
 | 
 | ||||||
|  |     # Wrapped in a method to allow overriding in subclasses | ||||||
|  |     def can_be_deactivated?(user) | ||||||
|  |       user.can_be_deactivated? | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|     def allowed? |     def allowed? | ||||||
|       return true if @skip_authorization |       return true if @skip_authorization | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -16,6 +16,8 @@ module WorkItems | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       def initialize_callbacks!(work_item) |       def initialize_callbacks!(work_item) | ||||||
|  |         # reset system notes timestamp | ||||||
|  |         work_item.system_note_timestamp = nil | ||||||
|         @callbacks = original_work_item.widgets.filter_map do |widget| |         @callbacks = original_work_item.widgets.filter_map do |widget| | ||||||
|           sync_data_callback_class = widget.class.sync_data_callback_class |           sync_data_callback_class = widget.class.sync_data_callback_class | ||||||
|           next if sync_data_callback_class.nil? |           next if sync_data_callback_class.nil? | ||||||
|  |  | ||||||
|  | @ -17,12 +17,14 @@ module WorkItems | ||||||
|           handle_children |           handle_children | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         # Nothing to delete for children as we relink existing child links |  | ||||||
|         # to the new parent in `relink_children_to_target_work_item` |  | ||||||
|         def post_move_cleanup |         def post_move_cleanup | ||||||
|           return unless work_item.parent_link.present? |           # Cleanup children linked to moved item when that is an issue because we are currently creating those | ||||||
|  |           # child items in the destination namespace anyway. If we decide to relink child items for Issue WIT | ||||||
|  |           # then we should not be deleting them here. | ||||||
|  |           work_item.child_links.each { |child_link| child_link.work_item.destroy! } if work_item.work_item_type.issue? | ||||||
| 
 | 
 | ||||||
|           work_item.parent_link.destroy! |           # cleanup parent link | ||||||
|  |           work_item.parent_link&.destroy! | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         private |         private | ||||||
|  | @ -40,13 +42,13 @@ module WorkItems | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         def handle_children |         def handle_children | ||||||
|  |           # We only support moving child items for the issue work item type for now | ||||||
|  |           return move_children if work_item.work_item_type.issue? | ||||||
|  | 
 | ||||||
|           # Relink child items to the new work item first. This will be used for any work item type other than issue. |           # Relink child items to the new work item first. This will be used for any work item type other than issue. | ||||||
|           # For issue work item type we will relink child items, but then also actually move the child items(tasks) to |           # For issue work item type we actually move the child items(tasks) to | ||||||
|           # the destination namespace. This is to keep feature parity with existing move functionality on issue. |           # the destination namespace. This is to keep feature parity with existing move functionality on issue. | ||||||
|           relink_children_to_target_work_item |           relink_children_to_target_work_item | ||||||
| 
 |  | ||||||
|           # We only support moving child items for the issue work item type for now |  | ||||||
|           move_children if work_item.work_item_type.issue? |  | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         def relink_children_to_target_work_item |         def relink_children_to_target_work_item | ||||||
|  | @ -67,13 +69,9 @@ module WorkItems | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         def move_children |         def move_children | ||||||
|           # Reload as the child_links association was just changed by relinking child items |  | ||||||
|           # in `relink_children_to_target_work_item` |  | ||||||
|           target_work_item.reset |  | ||||||
| 
 |  | ||||||
|           # We iterate over "new work item" child links now, because we have relinked child items from moved work item |           # We iterate over "new work item" child links now, because we have relinked child items from moved work item | ||||||
|           # to the new work item in `relink_children_to_target_work_item`. |           # to the new work item in `relink_children_to_target_work_item`. | ||||||
|           target_work_item.child_links.each do |link| |           work_item.child_links.each do |link| | ||||||
|             # This is going to be moved to an async worker. This is planned as a follow-up up iteration for a bunch of |             # This is going to be moved to an async worker. This is planned as a follow-up up iteration for a bunch of | ||||||
|             # other work item association data. The async implementation for move will be tracked in: |             # other work item association data. The async implementation for move will be tracked in: | ||||||
|             # https://gitlab.com/groups/gitlab-org/-/epics/15934 |             # https://gitlab.com/groups/gitlab-org/-/epics/15934 | ||||||
|  |  | ||||||
|  | @ -1,9 +0,0 @@ | ||||||
| --- |  | ||||||
| name: add_policies_to_ci_job_token |  | ||||||
| feature_issue_url: https://gitlab.com/gitlab-org/govern/authorization/team-tasks/-/issues/69 |  | ||||||
| introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/167872 |  | ||||||
| rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/497754 |  | ||||||
| milestone: '17.5' |  | ||||||
| group: group::authorization |  | ||||||
| type: wip |  | ||||||
| default_enabled: false |  | ||||||
|  | @ -886,6 +886,9 @@ Gitlab.ee do | ||||||
|   Settings.cron_jobs['vulnerability_statistics_schedule_worker'] ||= {} |   Settings.cron_jobs['vulnerability_statistics_schedule_worker'] ||= {} | ||||||
|   Settings.cron_jobs['vulnerability_statistics_schedule_worker']['cron'] ||= '15 1,20 * * *' |   Settings.cron_jobs['vulnerability_statistics_schedule_worker']['cron'] ||= '15 1,20 * * *' | ||||||
|   Settings.cron_jobs['vulnerability_statistics_schedule_worker']['job_class'] = 'Vulnerabilities::Statistics::ScheduleWorker' |   Settings.cron_jobs['vulnerability_statistics_schedule_worker']['job_class'] = 'Vulnerabilities::Statistics::ScheduleWorker' | ||||||
|  |   Settings.cron_jobs['vulnerability_namespace_statistics_schedule_worker'] ||= {} | ||||||
|  |   Settings.cron_jobs['vulnerability_namespace_statistics_schedule_worker']['cron'] ||= '0 8 * * 0' | ||||||
|  |   Settings.cron_jobs['vulnerability_namespace_statistics_schedule_worker']['job_class'] = 'Vulnerabilities::NamespaceStatistics::ScheduleWorker' | ||||||
|   Settings.cron_jobs['vulnerability_historical_statistics_deletion_worker'] ||= {} |   Settings.cron_jobs['vulnerability_historical_statistics_deletion_worker'] ||= {} | ||||||
|   Settings.cron_jobs['vulnerability_historical_statistics_deletion_worker']['cron'] ||= '15 3 * * *' |   Settings.cron_jobs['vulnerability_historical_statistics_deletion_worker']['cron'] ||= '15 3 * * *' | ||||||
|   Settings.cron_jobs['vulnerability_historical_statistics_deletion_worker']['job_class'] = 'Vulnerabilities::HistoricalStatistics::DeletionWorker' |   Settings.cron_jobs['vulnerability_historical_statistics_deletion_worker']['job_class'] = 'Vulnerabilities::HistoricalStatistics::DeletionWorker' | ||||||
|  |  | ||||||
|  | @ -995,6 +995,8 @@ | ||||||
|   - 1 |   - 1 | ||||||
| - - vulnerabilities_namespace_historical_statistics_update_traversal_ids | - - vulnerabilities_namespace_historical_statistics_update_traversal_ids | ||||||
|   - 1 |   - 1 | ||||||
|  | - - vulnerabilities_namespace_statistics_adjustment | ||||||
|  |   - 1 | ||||||
| - - vulnerabilities_process_archived_events | - - vulnerabilities_process_archived_events | ||||||
|   - 1 |   - 1 | ||||||
| - - vulnerabilities_process_bulk_dismissed_events | - - vulnerabilities_process_bulk_dismissed_events | ||||||
|  |  | ||||||
|  | @ -9,5 +9,5 @@ classes: | ||||||
| - GitlabSubscriptions::SeatAssignment | - GitlabSubscriptions::SeatAssignment | ||||||
| gitlab_schema: gitlab_main_cell | gitlab_schema: gitlab_main_cell | ||||||
| sharding_key: | sharding_key: | ||||||
|   namespace_id: namespaces |   organization_id: organizations | ||||||
| table_size: small | table_size: small | ||||||
|  |  | ||||||
|  | @ -0,0 +1,26 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class AddUsernamePasswordToVirtualRegistriesPackagesMavenUpstreams < Gitlab::Database::Migration[2.2] | ||||||
|  |   milestone '17.11' | ||||||
|  |   disable_ddl_transaction! | ||||||
|  | 
 | ||||||
|  |   TABLE_NAME = :virtual_registries_packages_maven_upstreams | ||||||
|  | 
 | ||||||
|  |   def up | ||||||
|  |     with_lock_retries do | ||||||
|  |       add_column TABLE_NAME, :username, :jsonb, null: true, if_not_exists: true | ||||||
|  |       add_column TABLE_NAME, :password, :jsonb, null: true, if_not_exists: true | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     add_check_constraint TABLE_NAME, | ||||||
|  |       'num_nonnulls(username, password) = 2 OR num_nulls(username, password) = 2', | ||||||
|  |       check_constraint_name(TABLE_NAME, 'username_and_password', 'both_set_or_null') | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def down | ||||||
|  |     with_lock_retries do | ||||||
|  |       remove_column(TABLE_NAME, :username, if_exists: true) | ||||||
|  |       remove_column(TABLE_NAME, :password, if_exists: true) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -0,0 +1,12 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class AddOrganizationIdToSubscriptionSeatAssignments < Gitlab::Database::Migration[2.2] | ||||||
|  |   milestone '17.11' | ||||||
|  | 
 | ||||||
|  |   DEFAULT_ORGANIZATION_ID = 1 | ||||||
|  |   enable_lock_retries! | ||||||
|  | 
 | ||||||
|  |   def change | ||||||
|  |     add_column :subscription_seat_assignments, :organization_id, :bigint, default: DEFAULT_ORGANIZATION_ID, null: false | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -0,0 +1,17 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class AddSubscriptionSeatAssignmentOrganizationIdIndex < Gitlab::Database::Migration[2.2] | ||||||
|  |   milestone '17.11' | ||||||
|  | 
 | ||||||
|  |   disable_ddl_transaction! | ||||||
|  | 
 | ||||||
|  |   INDEX_NAME = 'index_subscription_seat_assignments_on_organization_id' | ||||||
|  | 
 | ||||||
|  |   def up | ||||||
|  |     add_concurrent_index :subscription_seat_assignments, :organization_id, name: INDEX_NAME | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def down | ||||||
|  |     remove_concurrent_index_by_name :subscription_seat_assignments, INDEX_NAME | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -0,0 +1,18 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class AddSubscriptionSeatAssignmentOrganizationIdFk < Gitlab::Database::Migration[2.2] | ||||||
|  |   milestone '17.11' | ||||||
|  | 
 | ||||||
|  |   disable_ddl_transaction! | ||||||
|  | 
 | ||||||
|  |   def up | ||||||
|  |     add_concurrent_foreign_key :subscription_seat_assignments, :organizations, column: :organization_id, | ||||||
|  |       on_delete: :cascade | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def down | ||||||
|  |     with_lock_retries do | ||||||
|  |       remove_foreign_key :subscription_seat_assignments, column: :organization_id | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -0,0 +1 @@ | ||||||
|  | 18b2f1d2eea6c3f233dd11ca996fc01ec62c52f755d73a44d69018e215581f93 | ||||||
|  | @ -0,0 +1 @@ | ||||||
|  | 95d47c1d6c693c29fcc3662283dd928686402e1efea087ec52cf998041c3dfc1 | ||||||
|  | @ -0,0 +1 @@ | ||||||
|  | e59336609157a8a5ae1a8b7c8b8aa9c33162be0b01732e0eb043b5752bcc9680 | ||||||
|  | @ -0,0 +1 @@ | ||||||
|  | 104371717571df5325938552b74e7bd137b3b8b38242013bb1aebc4876e03cbf | ||||||
|  | @ -23148,7 +23148,8 @@ CREATE TABLE subscription_seat_assignments ( | ||||||
|     user_id bigint NOT NULL, |     user_id bigint NOT NULL, | ||||||
|     last_activity_on timestamp with time zone, |     last_activity_on timestamp with time zone, | ||||||
|     created_at timestamp with time zone NOT NULL, |     created_at timestamp with time zone NOT NULL, | ||||||
|     updated_at timestamp with time zone NOT NULL |     updated_at timestamp with time zone NOT NULL, | ||||||
|  |     organization_id bigint DEFAULT 1 NOT NULL | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE SEQUENCE subscription_seat_assignments_id_seq | CREATE SEQUENCE subscription_seat_assignments_id_seq | ||||||
|  | @ -24375,8 +24376,11 @@ CREATE TABLE virtual_registries_packages_maven_upstreams ( | ||||||
|     encrypted_username_iv bytea, |     encrypted_username_iv bytea, | ||||||
|     encrypted_password bytea, |     encrypted_password bytea, | ||||||
|     encrypted_password_iv bytea, |     encrypted_password_iv bytea, | ||||||
|  |     username jsonb, | ||||||
|  |     password jsonb, | ||||||
|     CONSTRAINT check_2366658457 CHECK ((octet_length(encrypted_username) <= 1020)), |     CONSTRAINT check_2366658457 CHECK ((octet_length(encrypted_username) <= 1020)), | ||||||
|     CONSTRAINT check_26c0572777 CHECK ((char_length(url) <= 255)), |     CONSTRAINT check_26c0572777 CHECK ((char_length(url) <= 255)), | ||||||
|  |     CONSTRAINT check_4db365ecc9 CHECK (((num_nonnulls(username, password) = 2) OR (num_nulls(username, password) = 2))), | ||||||
|     CONSTRAINT check_a3593dca3a CHECK ((cache_validity_hours >= 0)), |     CONSTRAINT check_a3593dca3a CHECK ((cache_validity_hours >= 0)), | ||||||
|     CONSTRAINT check_c3977cdb0c CHECK ((octet_length(encrypted_username_iv) <= 1020)), |     CONSTRAINT check_c3977cdb0c CHECK ((octet_length(encrypted_username_iv) <= 1020)), | ||||||
|     CONSTRAINT check_e4b6e651bf CHECK ((octet_length(encrypted_password_iv) <= 1020)), |     CONSTRAINT check_e4b6e651bf CHECK ((octet_length(encrypted_password_iv) <= 1020)), | ||||||
|  | @ -37038,6 +37042,8 @@ CREATE UNIQUE INDEX index_subscription_add_ons_on_name ON subscription_add_ons U | ||||||
| 
 | 
 | ||||||
| CREATE INDEX index_subscription_addon_purchases_on_expires_on ON subscription_add_on_purchases USING btree (expires_on); | CREATE INDEX index_subscription_addon_purchases_on_expires_on ON subscription_add_on_purchases USING btree (expires_on); | ||||||
| 
 | 
 | ||||||
|  | CREATE INDEX index_subscription_seat_assignments_on_organization_id ON subscription_seat_assignments USING btree (organization_id); | ||||||
|  | 
 | ||||||
| CREATE INDEX index_subscription_seat_assignments_on_user_id ON subscription_seat_assignments USING btree (user_id); | CREATE INDEX index_subscription_seat_assignments_on_user_id ON subscription_seat_assignments USING btree (user_id); | ||||||
| 
 | 
 | ||||||
| CREATE INDEX index_subscription_user_add_on_assignments_on_organization_id ON subscription_user_add_on_assignments USING btree (organization_id); | CREATE INDEX index_subscription_user_add_on_assignments_on_organization_id ON subscription_user_add_on_assignments USING btree (organization_id); | ||||||
|  | @ -42798,6 +42804,9 @@ ALTER TABLE ONLY duo_workflows_checkpoints | ||||||
| ALTER TABLE ONLY packages_conan_package_revisions | ALTER TABLE ONLY packages_conan_package_revisions | ||||||
|     ADD CONSTRAINT fk_b482b1a2f8 FOREIGN KEY (package_reference_id) REFERENCES packages_conan_package_references(id) ON DELETE CASCADE; |     ADD CONSTRAINT fk_b482b1a2f8 FOREIGN KEY (package_reference_id) REFERENCES packages_conan_package_references(id) ON DELETE CASCADE; | ||||||
| 
 | 
 | ||||||
|  | ALTER TABLE ONLY subscription_seat_assignments | ||||||
|  |     ADD CONSTRAINT fk_b4bdbc61ee FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; | ||||||
|  | 
 | ||||||
| ALTER TABLE ONLY protected_tag_create_access_levels | ALTER TABLE ONLY protected_tag_create_access_levels | ||||||
|     ADD CONSTRAINT fk_b4eb82fe3c FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE; |     ADD CONSTRAINT fk_b4eb82fe3c FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -144,14 +144,6 @@ You can also add a group or project to the allowlist [with the API](../../api/gr | ||||||
| 
 | 
 | ||||||
| {{< /history >}} | {{< /history >}} | ||||||
| 
 | 
 | ||||||
| {{< alert type="flag" >}} |  | ||||||
| 
 |  | ||||||
| The availability of this feature is controlled by a feature flag. |  | ||||||
| For more information, see the history. |  | ||||||
| This feature is available for testing, but not ready for production use. |  | ||||||
| 
 |  | ||||||
| {{< /alert >}} |  | ||||||
| 
 |  | ||||||
| You can populate a project's allowlist using the data from the [job token authentication log](#job-token-authentication-log) | You can populate a project's allowlist using the data from the [job token authentication log](#job-token-authentication-log) | ||||||
| with the UI or a Rake task. | with the UI or a Rake task. | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1629,11 +1629,12 @@ in the cluster back the request in GitLab. | ||||||
| 
 | 
 | ||||||
| ## Development tips | ## Development tips | ||||||
| 
 | 
 | ||||||
| - [Kibana](advanced_search/tips.md#kibana) |  | ||||||
| - [Viewing index status](advanced_search/tips.md#viewing-index-status) |  | ||||||
| - [Creating indices from scratch](advanced_search/tips.md#creating-all-indices-from-scratch-and-populating-with-local-data) | - [Creating indices from scratch](advanced_search/tips.md#creating-all-indices-from-scratch-and-populating-with-local-data) | ||||||
| - [Testing migrations](advanced_search/tips.md#testing-migrations) |  | ||||||
| - [Index data](advanced_search/tips.md#index-data) | - [Index data](advanced_search/tips.md#index-data) | ||||||
|  | - [Kibana](advanced_search/tips.md#kibana) | ||||||
|  | - [Running tests with Elasticsearch](advanced_search/tips.md#testing) | ||||||
|  | - [Testing migrations](advanced_search/tips.md#advanced-search-migrations) | ||||||
|  | - [Viewing index status](advanced_search/tips.md#viewing-index-status) | ||||||
| 
 | 
 | ||||||
| ## Troubleshooting | ## Troubleshooting | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -100,9 +100,18 @@ Elastic::ProcessBookkeepingService.track!(*MergeRequest.all) | ||||||
| Elastic::ProcessBookkeepingService.new.execute | Elastic::ProcessBookkeepingService.new.execute | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ## Testing migrations | ## Testing | ||||||
| 
 | 
 | ||||||
| ### Testing a migration that changes a mapping of an index | {{< alert type="warning" >}} | ||||||
|  | 
 | ||||||
|  | Elasticsearch tests do not run on every merge request. Add `~pipeline:run-search-tests` or `~group::global search` labels to the merge | ||||||
|  | request to run tests with the production versions of Elasticsearch and PostgreSQL.  | ||||||
|  | 
 | ||||||
|  | {{< /alert >}} | ||||||
|  | 
 | ||||||
|  | ### Advanced search migrations | ||||||
|  | 
 | ||||||
|  | #### Testing a migration that changes a mapping of an index | ||||||
| 
 | 
 | ||||||
| 1. Make sure the index doesn't already have the changes applied. Remember the migration cron worker runs in the background so it's possible the migration was already applied. | 1. Make sure the index doesn't already have the changes applied. Remember the migration cron worker runs in the background so it's possible the migration was already applied. | ||||||
|    - You can consider disabling the migration worker to have more control: `Feature.disable(:elastic_migration_worker)`. |    - You can consider disabling the migration worker to have more control: `Feature.disable(:elastic_migration_worker)`. | ||||||
|  |  | ||||||
|  | @ -185,8 +185,11 @@ prompt_template: | ||||||
| 
 | 
 | ||||||
| Once a stable prompt version is added to the AI Gateway it should not be altered. You can create a mutable version of a | Once a stable prompt version is added to the AI Gateway it should not be altered. You can create a mutable version of a | ||||||
| prompt by adding a pre-release suffix to the file name (e.g. `1.0.1-dev.yml`). This will also prevent it from being | prompt by adding a pre-release suffix to the file name (e.g. `1.0.1-dev.yml`). This will also prevent it from being | ||||||
| automatically served to clients. Then you can use a feature flag to control the rollout this new version. If your AI | automatically served to clients. Then you can use a feature flag to control the rollout this new version. For GitLab | ||||||
| action is implemented as a subclass of `AiGateway::Completions::Base`, you can achieve this by overriding the prompt | Duo Self-hosted, forced versions are ignored, and only versions defined in `PromptVersions` are used. This avoids | ||||||
|  | mistakenly enabling versions for models that don't have that specified version. | ||||||
|  | 
 | ||||||
|  | If your AI action is implemented as a subclass of `AiGateway::Completions::Base`, you can achieve this by overriding the prompt | ||||||
| version in your subclass: | version in your subclass: | ||||||
| 
 | 
 | ||||||
| ```ruby | ```ruby | ||||||
|  |  | ||||||
|  | @ -800,6 +800,18 @@ Single database tests run in two modes: | ||||||
| 
 | 
 | ||||||
| If you want to force tests to run with a single database, you can add the `pipeline:run-single-db` label to the merge request. | If you want to force tests to run with a single database, you can add the `pipeline:run-single-db` label to the merge request. | ||||||
| 
 | 
 | ||||||
|  | ### Elasticsearch and OpenSearch versions testing | ||||||
|  | 
 | ||||||
|  | Our test suite runs against Elasticsearch 8 as GitLab.com runs on Elasticsearch 8 when certain conditions are met. | ||||||
|  | 
 | ||||||
|  | We run our test suite against Elasticsearch 7, 8 and OpenSearch 1, 2 on nightly scheduled pipelines. All | ||||||
|  | test suites use PostgreSQL 16 because there is no dependency between the database and search backend. | ||||||
|  | 
 | ||||||
|  | | Where?                                                                                          | Elasticsearch version | OpenSearch Version   | PostgreSQL version   | | ||||||
|  | |-------------------------------------------------------------------------------------------------|-----------------------|----------------------|----------------------| | ||||||
|  | | Merge requests with label `~group::global search` or `~pipeline:run-search-tests`               | 8.X (production)      |                      | 16 (default version) | | ||||||
|  | | `nightly` scheduled pipelines for the `master` branch                                           | 7.X, 8.X (production) | 1.X, 2.X             | 16 (default version) | | ||||||
|  | 
 | ||||||
| ## Monitoring | ## Monitoring | ||||||
| 
 | 
 | ||||||
| The GitLab test suite is [monitored](../performance.md#rspec-profiling) for the `main` branch, and any branch | The GitLab test suite is [monitored](../performance.md#rspec-profiling) for the `main` branch, and any branch | ||||||
|  |  | ||||||
|  | @ -66,6 +66,7 @@ Before you can use Workflow, you must: | ||||||
|   - The repository you want to work with should be small or medium-sized. |   - The repository you want to work with should be small or medium-sized. | ||||||
|     Workflow can be slow or fail for large repositories. |     Workflow can be slow or fail for large repositories. | ||||||
| - [Successfully connect to your repository](#connect-to-your-repository). | - [Successfully connect to your repository](#connect-to-your-repository). | ||||||
|  | - [Ensure an HTTP/2 connection to the Workflow service is possible](troubleshooting.md#network-issues). | ||||||
| 
 | 
 | ||||||
| {{< alert type="note" >}} | {{< alert type="note" >}} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -7,7 +7,7 @@ title: Configure GitLab Duo on a GitLab Self-Managed instance | ||||||
| 
 | 
 | ||||||
| {{< details >}} | {{< details >}} | ||||||
| 
 | 
 | ||||||
| - Offering: GitLab Self-Managed, GitLab Dedicated | - Offering: GitLab Self-Managed | ||||||
| 
 | 
 | ||||||
| {{< /details >}} | {{< /details >}} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -104,6 +104,8 @@ The following actions count as activity: | ||||||
| - Visiting pages in GitLab, such as dashboards, projects, issues, merge requests, or settings. | - Visiting pages in GitLab, such as dashboards, projects, issues, merge requests, or settings. | ||||||
| - Using the REST or GraphQL API in the scope of the group. | - Using the REST or GraphQL API in the scope of the group. | ||||||
| 
 | 
 | ||||||
|  | Dormant [enterprise users](../enterprise_user/_index.md) are not removed, but [deactivated](../../administration/moderate_users.md#deactivate-and-reactivate-users). When these users sign back in, their accounts are reactivated and their access is restored. | ||||||
|  | 
 | ||||||
| {{< alert type="note" >}} | {{< alert type="note" >}} | ||||||
| 
 | 
 | ||||||
| Activity has not been recorded for members added before 2025-01-22. These members will not be removed until 2025-04-22, even if they have been dormant for over 90 days. | Activity has not been recorded for members added before 2025-01-22. These members will not be removed until 2025-04-22, even if they have been dormant for over 90 days. | ||||||
|  |  | ||||||
|  | @ -28,81 +28,59 @@ For more information, see the history. | ||||||
| 
 | 
 | ||||||
| {{< /alert >}} | {{< /alert >}} | ||||||
| 
 | 
 | ||||||
| The Web IDE is an advanced editor with commit staging. | The Web IDE is an advanced editor with commit staging where you can make changes to multiple | ||||||
| You can use the Web IDE to make changes to multiple files directly from the GitLab UI. | files directly from the GitLab UI. It provides a more robust editing experience compared to the | ||||||
| For a more basic implementation, see [Web Editor](../repository/web_editor.md). | [Web Editor](../repository/web_editor.md). | ||||||
| 
 | 
 | ||||||
| Support for [GitLab Flavored Markdown](../../markdown.md) preview in the Web IDE is proposed in | Support for [GitLab Flavored Markdown](../../markdown.md) preview in the Web IDE is proposed in | ||||||
| [issue 645](https://gitlab.com/gitlab-org/gitlab-vscode-extension/-/issues/645). | [issue 645](https://gitlab.com/gitlab-org/gitlab-vscode-extension/-/issues/645). | ||||||
| 
 | 
 | ||||||
| ## Open the Web IDE | ## Open the Web IDE | ||||||
| 
 | 
 | ||||||
| To open the Web IDE: | You can access the Web IDE through several methods. | ||||||
|  | 
 | ||||||
|  | ### With a keyboard shortcut | ||||||
| 
 | 
 | ||||||
| 1. On the left sidebar, select **Search or go to** and find your project. | 1. On the left sidebar, select **Search or go to** and find your project. | ||||||
| 1. Use the <kbd>.</kbd> keyboard shortcut. | 1. Use the <kbd>.</kbd> keyboard shortcut. | ||||||
| 
 | 
 | ||||||
| ### From a file or directory | ### From a file or directory | ||||||
| 
 | 
 | ||||||
| To open the Web IDE from a file or directory: |  | ||||||
| 
 |  | ||||||
| 1. On the left sidebar, select **Search or go to** and find your project. | 1. On the left sidebar, select **Search or go to** and find your project. | ||||||
| 1. Go to your file or directory. | 1. Go to your file or directory. | ||||||
| 1. Select **Edit > Open in Web IDE**. | 1. Select **Edit > Open in Web IDE**. | ||||||
| 
 | 
 | ||||||
| ### From a merge request | ### From a merge request | ||||||
| 
 | 
 | ||||||
| To open the Web IDE from a merge request: |  | ||||||
| 
 |  | ||||||
| 1. On the left sidebar, select **Search or go to** and find your project. | 1. On the left sidebar, select **Search or go to** and find your project. | ||||||
| 1. Go to your merge request. | 1. Go to your merge request. | ||||||
| 1. In the upper right, select **Code > Open in Web IDE**. | 1. In the upper right, select **Code > Open in Web IDE**. | ||||||
| 
 | 
 | ||||||
| The Web IDE opens new and modified files in separate tabs and displays changes side by side. | The Web IDE opens new and modified files in separate tabs, and displays changes side by side. | ||||||
| To reduce load time, only 10 files with the most lines changed open automatically. | To reduce load time, only 10 files with the most lines changed open automatically. | ||||||
| 
 | 
 | ||||||
| The left **Explorer** sidebar adds a merge request icon ({{< icon name="merge-request" >}}) next to new or modified files. | The left **Explorer** sidebar adds a merge request icon ({{< icon name="merge-request" >}}) next to new or modified files. | ||||||
| To view changes to a file, right-click the file and select **Compare with merge request base**. | To view changes to a file, right-click the file and select **Compare with merge request base**. | ||||||
| 
 | 
 | ||||||
| ## Open a file | ## Manage files | ||||||
|  | 
 | ||||||
|  | You can use the Web IDE to open, edit, and upload multiple files. | ||||||
|  | 
 | ||||||
|  | ### Open a file | ||||||
| 
 | 
 | ||||||
| To open a file by name in the Web IDE: | To open a file by name in the Web IDE: | ||||||
| 
 | 
 | ||||||
| 1. Press <kbd>Command</kbd>+<kbd>P</kbd>. | 1. Press <kbd>Command</kbd>+<kbd>P</kbd>. | ||||||
| 1. In the search box, enter the filename. | 1. In the search box, enter the filename. | ||||||
| 
 | 
 | ||||||
| ## Search open files | ### Search open files | ||||||
| 
 | 
 | ||||||
| To search across open files in the Web IDE: | To search across open files in the Web IDE: | ||||||
| 
 | 
 | ||||||
| 1. Press <kbd>Shift</kbd>+<kbd>Command</kbd>+<kbd>F</kbd>. | 1. Press <kbd>Shift</kbd>+<kbd>Command</kbd>+<kbd>F</kbd>. | ||||||
| 1. In the search box, enter your search term. | 1. In the search box, enter your search term. | ||||||
| 
 | 
 | ||||||
| ## View a list of modified files | ### Upload a file | ||||||
| 
 |  | ||||||
| To view a list of files you modified in the Web IDE: |  | ||||||
| 
 |  | ||||||
| - On the left activity bar, select **Source Control**, or |  | ||||||
|   press <kbd>Control</kbd>+<kbd>Shift</kbd>+<kbd>G</kbd>. |  | ||||||
| 
 |  | ||||||
| Your `CHANGES`, `STAGED CHANGES`, and `MERGE CHANGES` are displayed. |  | ||||||
| For more information, see the [VS Code documentation](https://code.visualstudio.com/docs/sourcecontrol/overview#_commit). |  | ||||||
| 
 |  | ||||||
| ## Restore uncommitted changes |  | ||||||
| 
 |  | ||||||
| You do not have to manually save any file you edit in the Web IDE. |  | ||||||
| The Web IDE stages the files you modify, so you can [commit the changes](#commit-changes). |  | ||||||
| Uncommitted changes are saved in your browser's local storage, and persist |  | ||||||
| even if you close the browser tab or refresh the Web IDE. |  | ||||||
| 
 |  | ||||||
| If your uncommitted changes are not available, you can restore the changes from local history. |  | ||||||
| To restore uncommitted changes in the Web IDE: |  | ||||||
| 
 |  | ||||||
| 1. Press <kbd>Shift</kbd>+<kbd>Command</kbd>+<kbd>P</kbd>. |  | ||||||
| 1. In the search box, enter `Local History: Find Entry to Restore`. |  | ||||||
| 1. Select the file that contains the uncommitted changes. |  | ||||||
| 
 |  | ||||||
| ## Upload a file |  | ||||||
| 
 | 
 | ||||||
| To upload a file in the Web IDE: | To upload a file in the Web IDE: | ||||||
| 
 | 
 | ||||||
|  | @ -120,7 +98,36 @@ To upload a file in the Web IDE: | ||||||
| You can upload multiple files at once. | You can upload multiple files at once. | ||||||
| The files are uploaded and automatically added to the repository. | The files are uploaded and automatically added to the repository. | ||||||
| 
 | 
 | ||||||
| ## Switch branches | ### Restore uncommitted changes | ||||||
|  | 
 | ||||||
|  | You do not have to manually save any file you edit in the Web IDE. | ||||||
|  | The Web IDE stages the files you modify, so you can [commit the changes](#commit-changes). | ||||||
|  | Uncommitted changes are saved in your browser's local storage. They persist | ||||||
|  | even if you close the browser tab or refresh the Web IDE. | ||||||
|  | 
 | ||||||
|  | If your uncommitted changes are not available, you can restore the changes from local history. | ||||||
|  | To restore uncommitted changes in the Web IDE: | ||||||
|  | 
 | ||||||
|  | 1. Press <kbd>Shift</kbd>+<kbd>Command</kbd>+<kbd>P</kbd>. | ||||||
|  | 1. In the search box, enter `Local History: Find Entry to Restore`. | ||||||
|  | 1. Select the file that contains the uncommitted changes. | ||||||
|  | 
 | ||||||
|  | ## Use source control | ||||||
|  | 
 | ||||||
|  | You can use source control to view modified files, create and switch branches, | ||||||
|  | commit changes, and create merge requests. | ||||||
|  | 
 | ||||||
|  | ### View modified files | ||||||
|  | 
 | ||||||
|  | To view a list of files you modified in the Web IDE: | ||||||
|  | 
 | ||||||
|  | - On the left activity bar, select **Source Control**, or | ||||||
|  |   press <kbd>Control</kbd>+<kbd>Shift</kbd>+<kbd>G</kbd>. | ||||||
|  | 
 | ||||||
|  | Your `CHANGES`, `STAGED CHANGES`, and `MERGE CHANGES` are displayed. | ||||||
|  | For more information, see the [VS Code documentation](https://code.visualstudio.com/docs/sourcecontrol/overview#_commit). | ||||||
|  | 
 | ||||||
|  | ### Switch branches | ||||||
| 
 | 
 | ||||||
| The Web IDE uses the current branch by default. | The Web IDE uses the current branch by default. | ||||||
| To switch branches in the Web IDE: | To switch branches in the Web IDE: | ||||||
|  | @ -128,7 +135,7 @@ To switch branches in the Web IDE: | ||||||
| 1. On the bottom status bar, on the left, select the current branch name. | 1. On the bottom status bar, on the left, select the current branch name. | ||||||
| 1. Enter or select an existing branch. | 1. Enter or select an existing branch. | ||||||
| 
 | 
 | ||||||
| ## Create a branch | ### Create a branch | ||||||
| 
 | 
 | ||||||
| To create a branch from the current branch in the Web IDE: | To create a branch from the current branch in the Web IDE: | ||||||
| 
 | 
 | ||||||
|  | @ -138,7 +145,7 @@ To create a branch from the current branch in the Web IDE: | ||||||
| 
 | 
 | ||||||
| If you do not have write access to the repository, **Create new branch** is not visible. | If you do not have write access to the repository, **Create new branch** is not visible. | ||||||
| 
 | 
 | ||||||
| ## Commit changes | ### Commit changes | ||||||
| 
 | 
 | ||||||
| To commit changes in the Web IDE: | To commit changes in the Web IDE: | ||||||
| 
 | 
 | ||||||
|  | @ -147,7 +154,7 @@ To commit changes in the Web IDE: | ||||||
| 1. Enter your commit message. | 1. Enter your commit message. | ||||||
| 1. Commit to the current branch or [create a new branch](#create-a-branch). | 1. Commit to the current branch or [create a new branch](#create-a-branch). | ||||||
| 
 | 
 | ||||||
| ## Create a merge request | ### Create a merge request | ||||||
| 
 | 
 | ||||||
| To create a [merge request](../merge_requests/_index.md) in the Web IDE: | To create a [merge request](../merge_requests/_index.md) in the Web IDE: | ||||||
| 
 | 
 | ||||||
|  | @ -156,7 +163,12 @@ To create a [merge request](../merge_requests/_index.md) in the Web IDE: | ||||||
| 
 | 
 | ||||||
| For more information, see [View missed notifications](#view-missed-notifications). | For more information, see [View missed notifications](#view-missed-notifications). | ||||||
| 
 | 
 | ||||||
| ## Use the Command Palette | ## Customize the Web IDE | ||||||
|  | 
 | ||||||
|  | Customize the Web IDE to match your preferences for keyboard shortcuts, | ||||||
|  | themes, settings, and synchronization. | ||||||
|  | 
 | ||||||
|  | ### Use the Command Palette | ||||||
| 
 | 
 | ||||||
| You can use the Command Palette to access many commands. | You can use the Command Palette to access many commands. | ||||||
| To open the Command Palette and run a command in the Web IDE: | To open the Command Palette and run a command in the Web IDE: | ||||||
|  | @ -164,7 +176,7 @@ To open the Command Palette and run a command in the Web IDE: | ||||||
| 1. Press <kbd>Shift</kbd>+<kbd>Command</kbd>+<kbd>P</kbd>. | 1. Press <kbd>Shift</kbd>+<kbd>Command</kbd>+<kbd>P</kbd>. | ||||||
| 1. Enter or select the command. | 1. Enter or select the command. | ||||||
| 
 | 
 | ||||||
| ## Edit settings | ### Edit settings | ||||||
| 
 | 
 | ||||||
| You can use the settings editor to view and edit your user and workspace settings. | You can use the settings editor to view and edit your user and workspace settings. | ||||||
| To open the settings editor in the Web IDE: | To open the settings editor in the Web IDE: | ||||||
|  | @ -174,7 +186,7 @@ To open the settings editor in the Web IDE: | ||||||
| 
 | 
 | ||||||
| In the settings editor, you can search for the settings you want to change. | In the settings editor, you can search for the settings you want to change. | ||||||
| 
 | 
 | ||||||
| ## Edit keyboard shortcuts | ### Edit keyboard shortcuts | ||||||
| 
 | 
 | ||||||
| You can use the keyboard shortcuts editor to view and change | You can use the keyboard shortcuts editor to view and change | ||||||
| the default keybindings for all available commands. | the default keybindings for all available commands. | ||||||
|  | @ -191,16 +203,7 @@ In the keyboard shortcuts editor, you can search for: | ||||||
| Keybindings are based on your keyboard layout. | Keybindings are based on your keyboard layout. | ||||||
| If you change your keyboard layout, existing keybindings are updated automatically. | If you change your keyboard layout, existing keybindings are updated automatically. | ||||||
| 
 | 
 | ||||||
| ### Use Vim keybindings | ### Change the color theme | ||||||
| 
 |  | ||||||
| Use Vim keybindings to navigate and edit text using keyboard shortcuts from the Vim text editor. |  | ||||||
| With the [Extensions Marketplace](#extension-marketplace), you can add Vim keybindings to |  | ||||||
| the Web IDE. |  | ||||||
| 
 |  | ||||||
| To enable Vim keybindings, install the [Vim](https://open-vsx.org/extension/vscodevim/vim) |  | ||||||
| extension. For more information, see [install an extension](#install-an-extension). |  | ||||||
| 
 |  | ||||||
| ## Change the color theme |  | ||||||
| 
 | 
 | ||||||
| You can choose between different color themes for the Web IDE. | You can choose between different color themes for the Web IDE. | ||||||
| The default theme is **GitLab Dark**. | The default theme is **GitLab Dark**. | ||||||
|  | @ -214,7 +217,7 @@ To change the color theme in the Web IDE: | ||||||
| 
 | 
 | ||||||
| The Web IDE stores your active color theme in your [user settings](#edit-settings). | The Web IDE stores your active color theme in your [user settings](#edit-settings). | ||||||
| 
 | 
 | ||||||
| ## Configure sync settings | ### Configure sync settings | ||||||
| 
 | 
 | ||||||
| To configure sync settings in the Web IDE: | To configure sync settings in the Web IDE: | ||||||
| 
 | 
 | ||||||
|  | @ -232,7 +235,7 @@ To configure sync settings in the Web IDE: | ||||||
| These settings sync automatically across multiple Web IDE instances. | These settings sync automatically across multiple Web IDE instances. | ||||||
| You cannot sync user profiles or go back to an earlier version of synced settings. | You cannot sync user profiles or go back to an earlier version of synced settings. | ||||||
| 
 | 
 | ||||||
| ## View missed notifications | ### View missed notifications | ||||||
| 
 | 
 | ||||||
| When you perform actions in the Web IDE, notifications appear in the lower right. | When you perform actions in the Web IDE, notifications appear in the lower right. | ||||||
| To view any notification you might have missed: | To view any notification you might have missed: | ||||||
|  | @ -318,6 +321,14 @@ the Red Hat [YAML](https://open-vsx.org/extension/redhat/vscode-yaml) extension. | ||||||
|       } |       } | ||||||
|       ``` |       ``` | ||||||
| 
 | 
 | ||||||
|  | #### Use Vim keybindings | ||||||
|  | 
 | ||||||
|  | Use Vim keybindings to navigate and edit text using keyboard shortcuts from the Vim text editor. | ||||||
|  | With the Extensions Marketplace, you can add Vim keybindings to the Web IDE. | ||||||
|  | 
 | ||||||
|  | To enable Vim keybindings, install the [Vim](https://open-vsx.org/extension/vscodevim/vim) | ||||||
|  | extension. For more information, see [install an extension](#install-an-extension). | ||||||
|  | 
 | ||||||
| ## Related topics | ## Related topics | ||||||
| 
 | 
 | ||||||
| - [GitLab Duo Chat in the Web IDE](../../gitlab_duo_chat/_index.md#use-gitlab-duo-chat-in-the-web-ide) | - [GitLab Duo Chat in the Web IDE](../../gitlab_duo_chat/_index.md#use-gitlab-duo-chat-in-the-web-ide) | ||||||
|  |  | ||||||
|  | @ -26648,6 +26648,9 @@ msgstr "" | ||||||
| msgid "Geo|There was an error deleting the Geo Site" | msgid "Geo|There was an error deleting the Geo Site" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | msgid "Geo|There was an error fetching the %{replicableType}. The GraphQL API call to the secondary may have failed." | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
| msgid "Geo|There was an error fetching the Geo Settings" | msgid "Geo|There was an error fetching the Geo Settings" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | @ -35416,9 +35419,6 @@ msgstr "" | ||||||
| msgid "Manual Variables" | msgid "Manual Variables" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
| msgid "ManualOrdering|Couldn't save the order of the issues" |  | ||||||
| msgstr "" |  | ||||||
| 
 |  | ||||||
| msgid "ManualVariables|There are no manually-specified variables for this pipeline" | msgid "ManualVariables|There are no manually-specified variables for this pipeline" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | @ -53027,6 +53027,9 @@ msgstr "" | ||||||
| msgid "SecurityInventory|No projects found" | msgid "SecurityInventory|No projects found" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | msgid "SecurityInventory|Project vulnerabilities" | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
| msgid "SecurityInventory|SAST" | msgid "SecurityInventory|SAST" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | @ -59895,9 +59898,6 @@ msgstr "" | ||||||
| msgid "There was an error fetching stage total counts" | msgid "There was an error fetching stage total counts" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
| msgid "There was an error fetching the %{replicableType}" |  | ||||||
| msgstr "" |  | ||||||
| 
 |  | ||||||
| msgid "There was an error fetching the cancelable jobs." | msgid "There was an error fetching the cancelable jobs." | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | @ -61716,6 +61716,9 @@ msgstr "" | ||||||
| msgid "Todos|Until later today" | msgid "Todos|Until later today" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | msgid "Todos|Until next week" | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
| msgid "Todos|Until tomorrow" | msgid "Todos|Until tomorrow" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -10,7 +10,6 @@ ee/spec/frontend/analytics/cycle_analytics/vsa_settings/components/value_stream_ | ||||||
| ee/spec/frontend/analytics/dashboards/ai_impact/components/metric_table_spec.js | ee/spec/frontend/analytics/dashboards/ai_impact/components/metric_table_spec.js | ||||||
| ee/spec/frontend/analytics/devops_reports/devops_adoption/components/devops_adoption_app_spec.js | ee/spec/frontend/analytics/devops_reports/devops_adoption/components/devops_adoption_app_spec.js | ||||||
| ee/spec/frontend/analytics/group_ci_cd_analytics/components/release_stats_card_spec.js | ee/spec/frontend/analytics/group_ci_cd_analytics/components/release_stats_card_spec.js | ||||||
| ee/spec/frontend/analytics/merge_request_analytics/components/throughput_chart_spec.js |  | ||||||
| ee/spec/frontend/approvals/components/security_configuration/unconfigured_security_rules_spec.js | ee/spec/frontend/approvals/components/security_configuration/unconfigured_security_rules_spec.js | ||||||
| ee/spec/frontend/approvals/mr_edit/mr_rules_spec.js | ee/spec/frontend/approvals/mr_edit/mr_rules_spec.js | ||||||
| ee/spec/frontend/approvals/project_settings/project_rules_spec.js | ee/spec/frontend/approvals/project_settings/project_rules_spec.js | ||||||
|  |  | ||||||
|  | @ -224,3 +224,13 @@ function fixtures_directory_exists() { | ||||||
| function upload_fixtures_package() { | function upload_fixtures_package() { | ||||||
|   upload_package "${FIXTURES_PACKAGE}" "${FIXTURES_PACKAGE_URL}" |   upload_package "${FIXTURES_PACKAGE}" "${FIXTURES_PACKAGE_URL}" | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | # Dump auto-explain logs | ||||||
|  | export AUTO_EXPLAIN_PACKAGE="auto-explain-logs.tar.gz" | ||||||
|  | export AUTO_EXPLAIN_PATH="${RSPEC_AUTO_EXPLAIN_LOG_PATH}" | ||||||
|  | export AUTO_EXPLAIN_PACKAGE_URL="${API_PACKAGES_BASE_URL}/auto-explain-logs/master/${AUTO_EXPLAIN_PACKAGE}" | ||||||
|  | 
 | ||||||
|  | function create_and_upload_auto_explain_package() { | ||||||
|  |   create_package "${AUTO_EXPLAIN_PACKAGE}" "${AUTO_EXPLAIN_PATH}" | ||||||
|  |   upload_package "${AUTO_EXPLAIN_PACKAGE}" "${AUTO_EXPLAIN_PACKAGE_URL}" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -95,6 +95,18 @@ describe('WikiNotesApp', () => { | ||||||
| 
 | 
 | ||||||
|     wrapper = shallowMountExtended(WikiNotesApp, { |     wrapper = shallowMountExtended(WikiNotesApp, { | ||||||
|       apolloProvider: fakeApollo, |       apolloProvider: fakeApollo, | ||||||
|  |       data() { | ||||||
|  |         return { | ||||||
|  |           wikiPage: { | ||||||
|  |             id: 'gid://gitlab/WikiPage/1', | ||||||
|  |             title: 'home', | ||||||
|  |             discussions: { | ||||||
|  |               nodes: [mockDiscussion('Discussion 1')], | ||||||
|  |             }, | ||||||
|  |           }, | ||||||
|  |           ...mockQueryResponse, | ||||||
|  |         }; | ||||||
|  |       }, | ||||||
|       provide: { |       provide: { | ||||||
|         containerId: noteableId, |         containerId: noteableId, | ||||||
|         noteCount: 5, |         noteCount: 5, | ||||||
|  | @ -256,8 +268,49 @@ describe('WikiNotesApp', () => { | ||||||
|       const errorAlert = wrapper.findComponent(GlAlert); |       const errorAlert = wrapper.findComponent(GlAlert); | ||||||
|       expect(errorAlert.exists()).toBe(false); |       expect(errorAlert.exists()).toBe(false); | ||||||
|     }); |     }); | ||||||
|  |   }); | ||||||
| 
 | 
 | ||||||
|     it('should delete the note correctly when the WikiDiscussions emits "note-deleted" when there are replies', async () => { |   describe('when "note-deleted" is fired', () => { | ||||||
|  |     let discussions; | ||||||
|  |     beforeEach(async () => { | ||||||
|  |       discussions = { | ||||||
|  |         nodes: [ | ||||||
|  |           mockDiscussion('Discussion 1'), | ||||||
|  |           mockDiscussion('Discussion 2'), | ||||||
|  |           mockDiscussion('Discussion 3 Note 1', 'Discussion 3 Note 2', 'Discussion 3 Note 3'), | ||||||
|  |         ], | ||||||
|  |       }; | ||||||
|  | 
 | ||||||
|  |       await createWrapper({ | ||||||
|  |         mockQueryResponse: { | ||||||
|  |           wikiPage: { | ||||||
|  |             id: 'gid://gitlab/WikiPage/1', | ||||||
|  |             title: 'home', | ||||||
|  |             discussions, | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('should call write query with the correct data', async () => { | ||||||
|  |       wrapper.findComponent(WikiDiscussion).vm.$emit('note-deleted'); | ||||||
|  |       await nextTick(); | ||||||
|  | 
 | ||||||
|  |       expect(apolloCache.writeQuery).toHaveBeenCalledWith({ | ||||||
|  |         query: wikiPageQuery, | ||||||
|  |         variables: queryVariables, | ||||||
|  |         data: { noteableId: '7', wikiPage }, | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('should delete note correctly when there are no replies', async () => { | ||||||
|  |       wrapper.findComponent(WikiDiscussion).vm.$emit('note-deleted'); | ||||||
|  |       await nextTick(); | ||||||
|  | 
 | ||||||
|  |       expect(wrapper.findAllComponents(WikiDiscussion)).toHaveLength(2); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('should delete note correctly when there are replies', async () => { | ||||||
|       const wikiDiscussions = wrapper.findAllComponents(WikiDiscussion); |       const wikiDiscussions = wrapper.findAllComponents(WikiDiscussion); | ||||||
| 
 | 
 | ||||||
|       // delete first note
 |       // delete first note
 | ||||||
|  |  | ||||||
|  | @ -10,6 +10,7 @@ import getBlobSearchQuery from '~/search/graphql/blob_search_zoekt.query.graphql | ||||||
| import GlobalSearchResultsApp from '~/search/results/components/app.vue'; | import GlobalSearchResultsApp from '~/search/results/components/app.vue'; | ||||||
| import ZoektBlobResults from '~/search/results/components/zoekt_blob_results.vue'; | import ZoektBlobResults from '~/search/results/components/zoekt_blob_results.vue'; | ||||||
| import StatusBar from '~/search/results/components/status_bar.vue'; | import StatusBar from '~/search/results/components/status_bar.vue'; | ||||||
|  | import EmptyResult from '~/search/results/components/result_empty.vue'; | ||||||
| import mutations from '~/search/store/mutations'; | import mutations from '~/search/store/mutations'; | ||||||
| import { | import { | ||||||
|   MOCK_QUERY, |   MOCK_QUERY, | ||||||
|  | @ -58,6 +59,7 @@ describe('GlobalSearchResultsApp', () => { | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const findZoektBlobResults = () => wrapper.findComponent(ZoektBlobResults); |   const findZoektBlobResults = () => wrapper.findComponent(ZoektBlobResults); | ||||||
|  |   const findEmptyResult = () => wrapper.findComponent(EmptyResult); | ||||||
|   const findStatusBar = () => wrapper.findComponent(StatusBar); |   const findStatusBar = () => wrapper.findComponent(StatusBar); | ||||||
|   const findAlert = () => wrapper.findComponent(GlAlert); |   const findAlert = () => wrapper.findComponent(GlAlert); | ||||||
| 
 | 
 | ||||||
|  | @ -108,9 +110,14 @@ describe('GlobalSearchResultsApp', () => { | ||||||
|       await waitForPromises(); |       await waitForPromises(); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it(`renders component properly`, async () => { |     it(`Renders empty state`, async () => { | ||||||
|       await waitForPromises(); |       await waitForPromises(); | ||||||
|       expect(findZoektBlobResults().props('hasResults')).toBe(false); |       expect(findZoektBlobResults().exists()).toBe(false); | ||||||
|  |       expect(findEmptyResult().exists()).toBe(true); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('Renders status bar', () => { | ||||||
|  |       expect(findStatusBar().exists()).toBe(true); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -185,6 +185,7 @@ describe('GlobalSearchStatusBar', () => { | ||||||
|         ); |         ); | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
|  | 
 | ||||||
|     describe('single result', () => { |     describe('single result', () => { | ||||||
|       beforeEach(() => { |       beforeEach(() => { | ||||||
|         createComponent({ |         createComponent({ | ||||||
|  | @ -225,27 +226,27 @@ describe('GlobalSearchStatusBar', () => { | ||||||
|     beforeEach(() => { |     beforeEach(() => { | ||||||
|       createComponent({ |       createComponent({ | ||||||
|         propsData: { |         propsData: { | ||||||
|  |           blobSearch: { | ||||||
|  |             perPage: 20, | ||||||
|  |             fileCount: 0, | ||||||
|  |             matchCount: 0, | ||||||
|  |           }, | ||||||
|           hasResults: false, |           hasResults: false, | ||||||
|         }, |         }, | ||||||
|       }); |         initialState: { | ||||||
|     }); |           query: { | ||||||
| 
 |             ...MOCK_QUERY, | ||||||
|     it('does not render the status bar', () => { |             group_id: 1, | ||||||
|       expect(wrapper.text()).toBe(''); |             project_id: null, | ||||||
|     }); |             search: 'test', | ||||||
|   }); |           }, | ||||||
| 
 |           groupInitialJson, | ||||||
|   describe('when loading', () => { |  | ||||||
|     beforeEach(() => { |  | ||||||
|       createComponent({ |  | ||||||
|         propsData: { |  | ||||||
|           isLoading: true, |  | ||||||
|         }, |         }, | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('does not render the status bar', () => { |     it('does not render the status bar', () => { | ||||||
|       expect(wrapper.text()).toBe(''); |       expect(wrapper.text()).toBe('Showing 0 code results for test in group Group Full Name'); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -5,7 +5,6 @@ import { GlLoadingIcon, GlCard } from '@gitlab/ui'; | ||||||
| import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; | import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; | ||||||
| import ZoektBlobResults from '~/search/results/components/zoekt_blob_results.vue'; | import ZoektBlobResults from '~/search/results/components/zoekt_blob_results.vue'; | ||||||
| import waitForPromises from 'helpers/wait_for_promises'; | import waitForPromises from 'helpers/wait_for_promises'; | ||||||
| import EmptyResult from '~/search/results/components/result_empty.vue'; |  | ||||||
| 
 | 
 | ||||||
| import { MOCK_QUERY, mockGetBlobSearchQuery } from '../../mock_data'; | import { MOCK_QUERY, mockGetBlobSearchQuery } from '../../mock_data'; | ||||||
| 
 | 
 | ||||||
|  | @ -44,7 +43,6 @@ describe('ZoektBlobResults', () => { | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); |   const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); | ||||||
|   const findEmptyResult = () => wrapper.findComponent(EmptyResult); |  | ||||||
| 
 | 
 | ||||||
|   beforeEach(() => { |   beforeEach(() => { | ||||||
|     window.gon.user_color_mode = 'gl-light'; |     window.gon.user_color_mode = 'gl-light'; | ||||||
|  | @ -80,19 +78,4 @@ describe('ZoektBlobResults', () => { | ||||||
|       expect(wrapper.element).toMatchSnapshot(); |       expect(wrapper.element).toMatchSnapshot(); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| 
 |  | ||||||
|   describe('when component has no results', () => { |  | ||||||
|     beforeEach(async () => { |  | ||||||
|       createComponent({ |  | ||||||
|         propsData: { hasResults: false }, |  | ||||||
|       }); |  | ||||||
|       jest.advanceTimersByTime(500); |  | ||||||
|       await waitForPromises(); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     it(`renders component properly`, async () => { |  | ||||||
|       await nextTick(); |  | ||||||
|       expect(findEmptyResult().exists()).toBe(true); |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -52,6 +52,10 @@ describe('SnoozeTimePicker', () => { | ||||||
|             formattedDate: 'Tomorrow, 8:00 AM', |             formattedDate: 'Tomorrow, 8:00 AM', | ||||||
|             text: 'Until tomorrow', |             text: 'Until tomorrow', | ||||||
|           }), |           }), | ||||||
|  |           expect.objectContaining({ | ||||||
|  |             formattedDate: 'Monday, 8:00 AM', | ||||||
|  |             text: 'Until next week', | ||||||
|  |           }), | ||||||
|         ], |         ], | ||||||
|         name: 'Snooze', |         name: 'Snooze', | ||||||
|       }, |       }, | ||||||
|  | @ -70,6 +74,7 @@ describe('SnoozeTimePicker', () => { | ||||||
|     ${0}  | ${'2024-12-18T14:24:00.000Z'} | ${'snooze_for_one_hour'} |     ${0}  | ${'2024-12-18T14:24:00.000Z'} | ${'snooze_for_one_hour'} | ||||||
|     ${1}  | ${'2024-12-18T17:24:00.000Z'} | ${'snooze_until_later_today'} |     ${1}  | ${'2024-12-18T17:24:00.000Z'} | ${'snooze_until_later_today'} | ||||||
|     ${2}  | ${'2024-12-19T08:00:00.000Z'} | ${'snooze_until_tomorrow'} |     ${2}  | ${'2024-12-19T08:00:00.000Z'} | ${'snooze_until_tomorrow'} | ||||||
|  |     ${3}  | ${'2024-12-23T08:00:00.000Z'} | ${'snooze_until_next_week'} | ||||||
|   `(
 |   `(
 | ||||||
|     'triggers the snooze action with snoozeUntil = $expectedDate when clicking option #$index', |     'triggers the snooze action with snoozeUntil = $expectedDate when clicking option #$index', | ||||||
|     ({ index, expectedDate, expectedTrackingLabel }) => { |     ({ index, expectedDate, expectedTrackingLabel }) => { | ||||||
|  |  | ||||||
|  | @ -32,6 +32,12 @@ RSpec.describe Mutations::Ci::JobTokenScope::AddGroupOrProject, feature_category | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|  |     before do | ||||||
|  |       allow_next_found_instance_of(Project) do |project| | ||||||
|  |         allow(project).to receive(:job_token_policies_enabled?).and_return(true) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|     context 'when we add a project' do |     context 'when we add a project' do | ||||||
|       let_it_be(:target_project) { create(:project) } |       let_it_be(:target_project) { create(:project) } | ||||||
|       let_it_be(:target_project_path) { target_project.full_path } |       let_it_be(:target_project_path) { target_project.full_path } | ||||||
|  |  | ||||||
|  | @ -288,6 +288,9 @@ RSpec.describe API::Helpers, feature_category: :shared do | ||||||
|           allow(helper).to receive(:route_authentication_setting).and_return({}) |           allow(helper).to receive(:route_authentication_setting).and_return({}) | ||||||
|           allow(helper).to receive(:route_setting).with(:authorization).and_return(job_token_policies: job_token_policy) |           allow(helper).to receive(:route_setting).with(:authorization).and_return(job_token_policies: job_token_policy) | ||||||
|           allow(user).to receive(:ci_job_token_scope).and_return(user.set_ci_job_token_scope!(job)) |           allow(user).to receive(:ci_job_token_scope).and_return(user.set_ci_job_token_scope!(job)) | ||||||
|  |           allow_next_found_instance_of(Project) do |project| | ||||||
|  |             allow(project).to receive(:job_token_policies_enabled?).and_return(true) | ||||||
|  |           end | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         subject(:find_project!) { helper.find_project!(project.id) } |         subject(:find_project!) { helper.find_project!(project.id) } | ||||||
|  |  | ||||||
|  | @ -262,6 +262,7 @@ RSpec.describe 'new tables missing sharding_key', feature_category: :cell do | ||||||
|       "ai_duo_chat_events" => "https://gitlab.com/gitlab-org/gitlab/-/issues/516140", |       "ai_duo_chat_events" => "https://gitlab.com/gitlab-org/gitlab/-/issues/516140", | ||||||
|       "fork_networks" => "https://gitlab.com/gitlab-org/gitlab/-/issues/522958", |       "fork_networks" => "https://gitlab.com/gitlab-org/gitlab/-/issues/522958", | ||||||
|       "merge_request_diff_commit_users" => "https://gitlab.com/gitlab-org/gitlab/-/issues/526725", |       "merge_request_diff_commit_users" => "https://gitlab.com/gitlab-org/gitlab/-/issues/526725", | ||||||
|  |       "subscription_seat_assignments" => "https://gitlab.com/gitlab-org/gitlab/-/issues/526769", | ||||||
|       # All the tables below related to uploads are part of the same work to |       # All the tables below related to uploads are part of the same work to | ||||||
|       # add sharding key to the table |       # add sharding key to the table | ||||||
|       "uploads" => "https://gitlab.com/gitlab-org/gitlab/-/issues/398199", |       "uploads" => "https://gitlab.com/gitlab-org/gitlab/-/issues/398199", | ||||||
|  |  | ||||||
|  | @ -11,6 +11,10 @@ RSpec.describe Ci::JobToken::Allowlist, feature_category: :continuous_integratio | ||||||
|   let(:allowlist) { described_class.new(source_project, direction: direction) } |   let(:allowlist) { described_class.new(source_project, direction: direction) } | ||||||
|   let(:direction) { :outbound } |   let(:direction) { :outbound } | ||||||
| 
 | 
 | ||||||
|  |   before do | ||||||
|  |     allow(source_project).to receive(:job_token_policies_enabled?).and_return(true) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   describe '#projects' do |   describe '#projects' do | ||||||
|     subject(:projects) { allowlist.projects } |     subject(:projects) { allowlist.projects } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -237,6 +237,10 @@ RSpec.describe Ci::JobToken::Scope, feature_category: :continuous_integration, f | ||||||
|     let_it_be(:allowed_policy) { ::Ci::JobToken::Policies::POLICIES.first } |     let_it_be(:allowed_policy) { ::Ci::JobToken::Policies::POLICIES.first } | ||||||
|     let(:accessed_project) { create_inbound_accessible_project_for_policies(target_project, [allowed_policy]) } |     let(:accessed_project) { create_inbound_accessible_project_for_policies(target_project, [allowed_policy]) } | ||||||
| 
 | 
 | ||||||
|  |     before do | ||||||
|  |       allow(accessed_project).to receive(:job_token_policies_enabled?).and_return(true) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|     shared_examples 'capturing job token policies' do |     shared_examples 'capturing job token policies' do | ||||||
|       it 'captures job token policies' do |       it 'captures job token policies' do | ||||||
|         expect(::Ci::JobToken::Authorization).to receive(:capture_job_token_policies).with(policies) |         expect(::Ci::JobToken::Authorization).to receive(:capture_job_token_policies).with(policies) | ||||||
|  |  | ||||||
|  | @ -41,7 +41,15 @@ RSpec.describe Ci::PipelineScheduleInput, feature_category: :continuous_integrat | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     describe 'value' do |     describe 'value' do | ||||||
|       it { is_expected.to validate_presence_of(:value) } |       it 'allows falsey values' do | ||||||
|  |         input.value = false | ||||||
|  | 
 | ||||||
|  |         expect(input).to be_valid | ||||||
|  | 
 | ||||||
|  |         input.value = '' | ||||||
|  | 
 | ||||||
|  |         expect(input).to be_valid | ||||||
|  |       end | ||||||
| 
 | 
 | ||||||
|       context 'when the serialized length of the value is less than the maximum permitted size' do |       context 'when the serialized length of the value is less than the maximum permitted size' do | ||||||
|         it 'is valid' do |         it 'is valid' do | ||||||
|  |  | ||||||
|  | @ -361,25 +361,21 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       context 'with project namespaces' do |       context 'with project namespaces' do | ||||||
|         shared_examples 'creates project namespace' do |         it 'automatically creates a project namespace' do | ||||||
|           it 'automatically creates a project namespace' do |           project = build(:project, path: 'hopefully-valid-path1') | ||||||
|             project = build(:project, path: 'hopefully-valid-path1') |           project.save! | ||||||
|             project.save! |  | ||||||
| 
 | 
 | ||||||
|             expect(project).to be_persisted |           expect(project).to be_persisted | ||||||
|             expect(project.project_namespace).to be_persisted |           expect(project.project_namespace).to be_persisted | ||||||
|             expect(project.project_namespace).to be_in_sync_with_project(project) |           expect(project.project_namespace).to be_in_sync_with_project(project) | ||||||
|             expect(project.reload.project_namespace.traversal_ids).to eq([project.namespace.traversal_ids, project.project_namespace.id].flatten.compact) |           expect(project.reload.project_namespace.traversal_ids).to match_array([project.namespace.traversal_ids, project.project_namespace.id].flatten.compact) | ||||||
|           end |  | ||||||
|         end |         end | ||||||
| 
 |  | ||||||
|         it_behaves_like 'creates project namespace' |  | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     context 'updating a project' do |     context 'updating a project' do | ||||||
|       let_it_be(:project_namespace) { create(:project_namespace) } |       let_it_be(:project_namespace) { create(:project_namespace) } | ||||||
|       let_it_be(:project) { project_namespace.project } |       let_it_be(:project, reload: true) { project_namespace.project } | ||||||
| 
 | 
 | ||||||
|       context 'when project has an associated project namespace' do |       context 'when project has an associated project namespace' do | ||||||
|         # when FF is disabled creating a project does not create a project_namespace, so we create one |         # when FF is disabled creating a project does not create a project_namespace, so we create one | ||||||
|  | @ -396,19 +392,20 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr | ||||||
|           expect(project.reload.project_namespace).to be_in_sync_with_project(project) |           expect(project.reload.project_namespace).to be_in_sync_with_project(project) | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         context 'when same project is being updated in 2 instances' do |         it 'syncs changed attributes' do | ||||||
|           it 'syncs only changed attributes' do |           project.update!(name: "New project name", path: "new_project_path") | ||||||
|             project1 = described_class.last |  | ||||||
|             project2 = described_class.last |  | ||||||
| 
 | 
 | ||||||
|             project_name = project1.name |           expect(project.reload.project_namespace).to be_in_sync_with_project(project) | ||||||
|             project_path = project1.path |         end | ||||||
| 
 | 
 | ||||||
|             project1.update!(name: project_name + "-1") |         # Regression test for edge-case introduced by 0a71dc3f33e809198d522d3cf2a28781aeac5809 | ||||||
|             project2.update!(path: project_path + "-1") |         it 'syncs organization_id even when it is the only change' do | ||||||
|  |           new_org = create(:organization) | ||||||
| 
 | 
 | ||||||
|             expect(project.reload.project_namespace).to be_in_sync_with_project(project) |           project.parent.update_column(:organization_id, new_org.id) | ||||||
|           end |           project.update!(organization_id: new_org.id) | ||||||
|  | 
 | ||||||
|  |           expect(project.reload.project_namespace).to be_in_sync_with_project(project) | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|  | @ -10031,22 +10028,16 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr | ||||||
| 
 | 
 | ||||||
|     subject { project.job_token_policies_enabled? } |     subject { project.job_token_policies_enabled? } | ||||||
| 
 | 
 | ||||||
|     where(:flag_enabled, :setting_enabled, :result) do |     where(:setting_enabled) { [true, false] } | ||||||
|       true  | true  | true |  | ||||||
|       true  | false | true |  | ||||||
|       false | true  | true |  | ||||||
|       false | false | false |  | ||||||
|     end |  | ||||||
| 
 | 
 | ||||||
|     before do |     before do | ||||||
|       project.clear_memoization(:job_token_policies_enabled?) |       project.clear_memoization(:job_token_policies_enabled?) | ||||||
|       stub_feature_flags(add_policies_to_ci_job_token: flag_enabled) |  | ||||||
|       allow(project).to receive_message_chain(:namespace, :root_ancestor, :namespace_settings, |       allow(project).to receive_message_chain(:namespace, :root_ancestor, :namespace_settings, | ||||||
|         :job_token_policies_enabled?).and_return(setting_enabled) |         :job_token_policies_enabled?).and_return(setting_enabled) | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     with_them do |     with_them do | ||||||
|       it { is_expected.to eq(result) } |       it { is_expected.to eq(setting_enabled) } | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -997,12 +997,14 @@ RSpec.describe User, feature_category: :user_profile do | ||||||
|           expect(user.errors.messages[:email].first).to eq(expected_error) |           expect(user.errors.messages[:email].first).to eq(expected_error) | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         it 'allows example@test.com if user is placeholder or import user' do |         it 'allows example@test.com if user is placeholder, import user or security policy bot' do | ||||||
|           placeholder_user = build(:user, :placeholder, email: "example@test.com") |           placeholder_user = build(:user, :placeholder, email: "example@test.com") | ||||||
|           import_user = build(:user, :import_user, email: "example@test.com") |           import_user = build(:user, :import_user, email: "example@test.com") | ||||||
|  |           security_policy_bot = build(:user, :security_policy_bot, email: "example@test.com") | ||||||
| 
 | 
 | ||||||
|           expect(placeholder_user).to be_valid |           expect(placeholder_user).to be_valid | ||||||
|           expect(import_user).to be_valid |           expect(import_user).to be_valid | ||||||
|  |           expect(security_policy_bot).to be_valid | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         it 'does not allow user to update email to a non-allowlisted domain' do |         it 'does not allow user to update email to a non-allowlisted domain' do | ||||||
|  | @ -1012,12 +1014,14 @@ RSpec.describe User, feature_category: :user_profile do | ||||||
|             .to raise_error(StandardError, 'Validation failed: Email is not allowed. Please use your regular email address. Check with your administrator.') |             .to raise_error(StandardError, 'Validation failed: Email is not allowed. Please use your regular email address. Check with your administrator.') | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         it 'allows placeholder and import users to update email to a non-allowlisted domain' do |         it 'allows placeholder, import users and security policy bot to update email to a non-allowlisted domain' do | ||||||
|           placeholder_user = create(:user, :placeholder, email: "info@test.example.com") |           placeholder_user = create(:user, :placeholder, email: "info@test.example.com") | ||||||
|           import_user = create(:user, :import_user, email: "info2@test.example.com") |           import_user = create(:user, :import_user, email: "info2@test.example.com") | ||||||
|  |           security_policy_bot = create(:user, :security_policy_bot, email: "info3@test.example.com") | ||||||
| 
 | 
 | ||||||
|           expect(placeholder_user.update!(email: "test@notexample.com")).to eq(true) |           expect(placeholder_user.update!(email: "test@notexample.com")).to eq(true) | ||||||
|           expect(import_user.update!(email: "test2@notexample.com")).to eq(true) |           expect(import_user.update!(email: "test2@notexample.com")).to eq(true) | ||||||
|  |           expect(security_policy_bot.update!(email: "test3@notexample.com")).to eq(true) | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|  | @ -1176,6 +1180,16 @@ RSpec.describe User, feature_category: :user_profile do | ||||||
|             expect(user).to be_valid |             expect(user).to be_valid | ||||||
|           end |           end | ||||||
| 
 | 
 | ||||||
|  |           it 'allows placeholder, import users and security policy bot to bypass email restrictions' do | ||||||
|  |             placeholder_user = build(:user, :placeholder, email: "info+1@test.com") | ||||||
|  |             import_user = build(:user, :import_user, email: "info+1@test.com") | ||||||
|  |             security_policy_bot = build(:user, :security_policy_bot, email: "info+1@test.com") | ||||||
|  | 
 | ||||||
|  |             expect(placeholder_user).to be_valid | ||||||
|  |             expect(import_user).to be_valid | ||||||
|  |             expect(security_policy_bot).to be_valid | ||||||
|  |           end | ||||||
|  | 
 | ||||||
|           context 'when created_by_id is set' do |           context 'when created_by_id is set' do | ||||||
|             it 'does accept the email address' do |             it 'does accept the email address' do | ||||||
|               user = build(:user, email: 'info+1@test.com', created_by_id: 1) |               user = build(:user, email: 'info+1@test.com', created_by_id: 1) | ||||||
|  |  | ||||||
|  | @ -169,6 +169,12 @@ RSpec.describe 'Querying CI_JOB_TOKEN allowlist for a project', feature_category | ||||||
|         ) |         ) | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|  |       before do | ||||||
|  |         allow_next_found_instance_of(Project) do |project| | ||||||
|  |           allow(project).to receive(:job_token_policies_enabled?).and_return(true) | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|       it 'returns the correct data' do |       it 'returns the correct data' do | ||||||
|         post_graphql(query, current_user: current_user) |         post_graphql(query, current_user: current_user) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -85,6 +85,12 @@ RSpec.describe 'CiJobTokenScopeAddGroupOrProject', feature_category: :continuous | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   before do | ||||||
|  |     allow_next_found_instance_of(Project) do |project| | ||||||
|  |       allow(project).to receive(:job_token_policies_enabled?).and_return(true) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   context 'when we add a group' do |   context 'when we add a group' do | ||||||
|     let_it_be(:target_group) { create(:group, :private) } |     let_it_be(:target_group) { create(:group, :private) } | ||||||
|     let(:target_path) { target_group } |     let(:target_path) { target_group } | ||||||
|  |  | ||||||
|  | @ -43,6 +43,12 @@ RSpec.describe 'CiJobTokenScopeUpdatePolicies', feature_category: :continuous_in | ||||||
| 
 | 
 | ||||||
|   let(:mutation_response) { graphql_mutation_response(:ci_job_token_scope_update_policies) } |   let(:mutation_response) { graphql_mutation_response(:ci_job_token_scope_update_policies) } | ||||||
| 
 | 
 | ||||||
|  |   before do | ||||||
|  |     allow_next_found_instance_of(Project) do |project| | ||||||
|  |       allow(project).to receive(:job_token_policies_enabled?).and_return(true) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   context 'when policies are updated for a target project' do |   context 'when policies are updated for a target project' do | ||||||
|     let_it_be(:target_project) { create(:project, :private) } |     let_it_be(:target_project) { create(:project, :private) } | ||||||
|     let_it_be(:target_path) { target_project.full_path } |     let_it_be(:target_path) { target_project.full_path } | ||||||
|  |  | ||||||
|  | @ -11,6 +11,10 @@ RSpec.describe Ci::JobTokenScope::AddGroupService, feature_category: :continuous | ||||||
|   let(:service) { described_class.new(project, current_user) } |   let(:service) { described_class.new(project, current_user) } | ||||||
| 
 | 
 | ||||||
|   shared_examples 'adds group' do |_context| |   shared_examples 'adds group' do |_context| | ||||||
|  |     before do | ||||||
|  |       allow(project).to receive(:job_token_policies_enabled?).and_return(true) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|     it 'adds the group to the scope', :aggregate_failures do |     it 'adds the group to the scope', :aggregate_failures do | ||||||
|       expect { result }.to change { Ci::JobToken::GroupScopeLink.count }.by(1) |       expect { result }.to change { Ci::JobToken::GroupScopeLink.count }.by(1) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -10,6 +10,10 @@ RSpec.describe Ci::JobTokenScope::AddProjectService, feature_category: :continuo | ||||||
|   let_it_be(:policies) { %w[read_containers read_packages] } |   let_it_be(:policies) { %w[read_containers read_packages] } | ||||||
| 
 | 
 | ||||||
|   shared_examples 'adds project' do |context| |   shared_examples 'adds project' do |context| | ||||||
|  |     before do | ||||||
|  |       allow(project).to receive(:job_token_policies_enabled?).and_return(true) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|     it 'adds the project to the scope', :aggregate_failures do |     it 'adds the project to the scope', :aggregate_failures do | ||||||
|       expect { result }.to change { Ci::JobToken::ProjectScopeLink.count }.by(1) |       expect { result }.to change { Ci::JobToken::ProjectScopeLink.count }.by(1) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -12,6 +12,10 @@ RSpec.describe Ci::JobTokenScope::UpdatePoliciesService, feature_category: :cont | ||||||
|     described_class.new(project, current_user).execute(target, default_permissions, policies) |     described_class.new(project, current_user).execute(target, default_permissions, policies) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   before do | ||||||
|  |     allow(project).to receive(:job_token_policies_enabled?).and_return(true) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   describe '#execute' do |   describe '#execute' do | ||||||
|     shared_examples 'when user is not logged in' do |     shared_examples 'when user is not logged in' do | ||||||
|       let(:current_user) { nil } |       let(:current_user) { nil } | ||||||
|  |  | ||||||
|  | @ -46,8 +46,8 @@ RSpec.describe WorkItems::DataSync::Widgets::Hierarchy, feature_category: :team_ | ||||||
| 
 | 
 | ||||||
|           # these are the newly copied child records |           # these are the newly copied child records | ||||||
|           new_children = target_work_item.reload.work_item_children.where(moved_to_id: nil) |           new_children = target_work_item.reload.work_item_children.where(moved_to_id: nil) | ||||||
|           # these are the originally re-linked child records from source work item that are closed upon move. |           # these are the originally linked child records on source work item that are closed upon move. | ||||||
|           moved_children = target_work_item.reload.work_item_children.where.not(moved_to_id: nil) |           moved_children = work_item.reload.work_item_children.where.not(moved_to_id: nil) | ||||||
| 
 | 
 | ||||||
|           expect(new_children.size).to eq(2) |           expect(new_children.size).to eq(2) | ||||||
|           expect(new_children.map(&:title)).to match_array(expected_child_items_titles) |           expect(new_children.map(&:title)).to match_array(expected_child_items_titles) | ||||||
|  | @ -62,9 +62,6 @@ RSpec.describe WorkItems::DataSync::Widgets::Hierarchy, feature_category: :team_ | ||||||
|           expect(moved_children.map(&:moved_to_id)).to match_array(new_children.map(&:id)) |           expect(moved_children.map(&:moved_to_id)).to match_array(new_children.map(&:id)) | ||||||
|           # new target work item and its 2 child tasks are located within new namespace |           # new target work item and its 2 child tasks are located within new namespace | ||||||
|           expect(target_work_item.namespace.work_items.count).to eq(3) |           expect(target_work_item.namespace.work_items.count).to eq(3) | ||||||
| 
 |  | ||||||
|           # child items are relinked in `after_save_commit` |  | ||||||
|           expect(work_item.reload.work_item_children).to be_empty |  | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -8,11 +8,9 @@ RSpec::Matchers.define :be_in_sync_with_project do |project| | ||||||
|     break true if project.new_record? && !project_namespace.present? |     break true if project.new_record? && !project_namespace.present? | ||||||
| 
 | 
 | ||||||
|     project_namespace.present? && |     project_namespace.present? && | ||||||
|       project.name == project_namespace.name && |       Namespaces::ProjectNamespace::SYNCED_ATTRIBUTES.all? do |attribute| | ||||||
|       project.path == project_namespace.path && |         project[attribute] == project_namespace[attribute] | ||||||
|       project.namespace_id == project_namespace.parent_id && |       end | ||||||
|       project.visibility_level == project_namespace.visibility_level && |  | ||||||
|       project.shared_runners_enabled == project_namespace.shared_runners_enabled |  | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   failure_message_when_negated do |project_namespace| |   failure_message_when_negated do |project_namespace| | ||||||
|  |  | ||||||
|  | @ -13,12 +13,13 @@ RSpec.shared_examples 'enforcing job token policies' do |policies, expected_succ | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   context 'when authenticating with a CI job token from another project' do |   context 'when authenticating with a CI job token from another project' do | ||||||
|     let(:source_project) { project } |     let(:source_project) { project.reload } | ||||||
|     let(:job_user) { user } |     let(:job_user) { user } | ||||||
|     let(:target_job) { create(:ci_build, :running, user: job_user) } |     let(:target_job) { create(:ci_build, :running, user: job_user) } | ||||||
|     let(:allowed_policies) { Array(policies) } |     let(:allowed_policies) { Array(policies) } | ||||||
|     let(:default_permissions) { false } |     let(:default_permissions) { false } | ||||||
|     let(:skip_allowlist_creation) { false } |     let(:skip_allowlist_creation) { false } | ||||||
|  |     let(:job_token_policies_enabled) { true } | ||||||
|     let!(:features_state) do |     let!(:features_state) do | ||||||
|       source_project.project_feature.attributes |       source_project.project_feature.attributes | ||||||
|         .slice(*::ProjectFeature::FEATURES.map { |feature| "#{feature}_access_level" }) |         .slice(*::ProjectFeature::FEATURES.map { |feature| "#{feature}_access_level" }) | ||||||
|  | @ -37,6 +38,10 @@ RSpec.shared_examples 'enforcing job token policies' do |policies, expected_succ | ||||||
|     before do |     before do | ||||||
|       # Make all project features private |       # Make all project features private | ||||||
|       enable_project_features(source_project, nil) |       enable_project_features(source_project, nil) | ||||||
|  |       # Enable fine-grained job token permissions | ||||||
|  |       namespace_settings = source_project.root_ancestor.namespace_settings || | ||||||
|  |         source_project.root_ancestor.build_namespace_settings | ||||||
|  |       namespace_settings.update!(job_token_policies_enabled:) | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     after do |     after do | ||||||
|  | @ -56,10 +61,7 @@ RSpec.shared_examples 'enforcing job token policies' do |policies, expected_succ | ||||||
|       # This test makes sure that endpoints for which we want to enable job token permissions |       # This test makes sure that endpoints for which we want to enable job token permissions | ||||||
|       # are denied access when an allowlist entry is missing. |       # are denied access when an allowlist entry is missing. | ||||||
|       let(:allowlist) { nil } |       let(:allowlist) { nil } | ||||||
| 
 |       let(:job_token_policies_enabled) { false } | ||||||
|       before do |  | ||||||
|         stub_feature_flags(add_policies_to_ci_job_token: false) |  | ||||||
|       end |  | ||||||
| 
 | 
 | ||||||
|       it 'denies access' do |       it 'denies access' do | ||||||
|         expect(do_request).to have_gitlab_http_status(:forbidden) |         expect(do_request).to have_gitlab_http_status(:forbidden) | ||||||
|  | @ -100,11 +102,7 @@ RSpec.shared_examples 'enforcing job token policies' do |policies, expected_succ | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       context 'when job token policies are disabled' do |       context 'when job token policies are disabled' do | ||||||
|         before do |         let(:job_token_policies_enabled) { false } | ||||||
|           allow_next_found_instance_of(Project) do |project| |  | ||||||
|             allow(project).to receive(:job_token_policies_enabled?).and_return(false) |  | ||||||
|           end |  | ||||||
|         end |  | ||||||
| 
 | 
 | ||||||
|         it { is_expected.to have_gitlab_http_status(expected_success_status) } |         it { is_expected.to have_gitlab_http_status(expected_success_status) } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -240,14 +240,20 @@ RSpec.shared_examples 'cloneable and moveable widget data' do | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   let_it_be(:child_items) do |   let_it_be(:child_items) do | ||||||
|  |     namespace_params = if original_work_item.project | ||||||
|  |                          [project: original_work_item.project] | ||||||
|  |                        else | ||||||
|  |                          [:group_level, { namespace: original_work_item.namespace }] | ||||||
|  |                        end | ||||||
|  | 
 | ||||||
|     child_item_type1 =  WorkItems::HierarchyRestriction.where(parent_type: original_work_item.work_item_type).order( |     child_item_type1 =  WorkItems::HierarchyRestriction.where(parent_type: original_work_item.work_item_type).order( | ||||||
|       id: :asc).first.child_type.base_type |       id: :asc).first.child_type.base_type | ||||||
|     child_item_type2 =  WorkItems::HierarchyRestriction.where(parent_type: original_work_item.work_item_type).order( |     child_item_type2 =  WorkItems::HierarchyRestriction.where(parent_type: original_work_item.work_item_type).order( | ||||||
|       id: :asc).last.child_type.base_type |       id: :asc).last.child_type.base_type | ||||||
| 
 | 
 | ||||||
|     child_item1 = create(:work_item, child_item_type1) |     child_item1 = create(:work_item, child_item_type1, *namespace_params) | ||||||
|     create(:parent_link, work_item: child_item1, work_item_parent: original_work_item) |     create(:parent_link, work_item: child_item1, work_item_parent: original_work_item) | ||||||
|     child_item2 = create(:work_item, child_item_type2) |     child_item2 = create(:work_item, child_item_type2, *namespace_params) | ||||||
|     create(:parent_link, work_item: child_item2, work_item_parent: original_work_item) |     create(:parent_link, work_item: child_item2, work_item_parent: original_work_item) | ||||||
| 
 | 
 | ||||||
|     [child_item1, child_item2].pluck(:title) |     [child_item1, child_item2].pluck(:title) | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue