diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-08-26 18:11:58 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-08-26 18:11:58 +0300 |
commit | 485728af8d6692d2df36f340b896dea79939ae0c (patch) | |
tree | 7595e61d1a5c92591f1065de648f7af70e26dcad | |
parent | 93c1e0e4c231b0b13000a587a6949067ef7fb128 (diff) |
Add latest changes from gitlab-org/gitlab@master
39 files changed, 167 insertions, 68 deletions
diff --git a/.gitlab/agents/review-apps/config.yaml b/.gitlab/agents/review-apps/config.yaml new file mode 100644 index 00000000000..945b3a18d30 --- /dev/null +++ b/.gitlab/agents/review-apps/config.yaml @@ -0,0 +1 @@ +# This empty file is used for agent-based integration with Kubernetes diff --git a/app/assets/javascripts/alerts_settings/components/alerts_form.vue b/app/assets/javascripts/alerts_settings/components/alerts_form.vue index 696e7f359d1..388d925196b 100644 --- a/app/assets/javascripts/alerts_settings/components/alerts_form.vue +++ b/app/assets/javascripts/alerts_settings/components/alerts_form.vue @@ -109,7 +109,7 @@ export default { v-for="template in templates" :key="template.key" data-qa-selector="incident_templates_item" - :is-check-item="true" + is-check-item :is-checked="isTemplateSelected(template.key)" @click="selectIssueTemplate(template.key)" > diff --git a/app/assets/javascripts/confidential_merge_request/components/dropdown.vue b/app/assets/javascripts/confidential_merge_request/components/dropdown.vue index 6bb654a434f..9cb7cd9607f 100644 --- a/app/assets/javascripts/confidential_merge_request/components/dropdown.vue +++ b/app/assets/javascripts/confidential_merge_request/components/dropdown.vue @@ -40,7 +40,7 @@ export default { <gl-dropdown-item v-for="project in projects" :key="project.id" - :is-check-item="true" + is-check-item :is-checked="project.id === selectedProject.id" @click="selectProject(project)" > diff --git a/app/assets/javascripts/content_editor/components/bubble_menus/code_block_bubble_menu.vue b/app/assets/javascripts/content_editor/components/bubble_menus/code_block_bubble_menu.vue index 5e3bf5350ba..a9668ebdb69 100644 --- a/app/assets/javascripts/content_editor/components/bubble_menus/code_block_bubble_menu.vue +++ b/app/assets/javascripts/content_editor/components/bubble_menus/code_block_bubble_menu.vue @@ -182,7 +182,7 @@ export default { </template> <template v-if="!showCustomLanguageInput" #highlighted-items> - <gl-dropdown-item :key="selectedLanguage.syntax" is-check-item :is-checked="true"> + <gl-dropdown-item :key="selectedLanguage.syntax" is-check-item is-checked> {{ selectedLanguage.label }} </gl-dropdown-item> </template> diff --git a/app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue b/app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue index 816d7ac7abf..f10545faea6 100644 --- a/app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue +++ b/app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue @@ -73,8 +73,8 @@ export default { <gl-dropdown-item v-for="(version, index) in allVersions" :key="version.id" - :is-check-item="true" - :is-check-centered="true" + is-check-item + is-check-centered :is-checked="findVersionId(version.id) === currentVersionId" :avatar-url="getAvatarUrl(version)" @click="routeToVersion(version.id)" diff --git a/app/assets/javascripts/diffs/components/compare_dropdown_layout.vue b/app/assets/javascripts/diffs/components/compare_dropdown_layout.vue index fd219a7d00f..4501988ee4f 100644 --- a/app/assets/javascripts/diffs/components/compare_dropdown_layout.vue +++ b/app/assets/javascripts/diffs/components/compare_dropdown_layout.vue @@ -37,7 +37,7 @@ export default { :class="{ 'is-active': version.selected, }" - :is-check-item="true" + is-check-item :is-checked="version.selected" :href="version.href" > diff --git a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue index 52593aabfea..d40aab8ee4f 100644 --- a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue +++ b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue @@ -50,7 +50,7 @@ export default { <gl-dropdown-item v-for="mode in modeDropdownItems" :key="mode.viewerType" - :is-check-item="true" + is-check-item :is-checked="viewer === mode.viewerType" @click="changeMode(mode.viewerType)" > diff --git a/app/assets/javascripts/milestones/components/milestone_combobox.vue b/app/assets/javascripts/milestones/components/milestone_combobox.vue index 59d2a2b29b3..5c3b969655b 100644 --- a/app/assets/javascripts/milestones/components/milestone_combobox.vue +++ b/app/assets/javascripts/milestones/components/milestone_combobox.vue @@ -243,7 +243,7 @@ export default { v-for="(item, idx) in extraLinks" :key="idx" :href="item.url" - :is-check-item="true" + is-check-item data-testid="milestone-combobox-extra-links" > {{ item.text }} diff --git a/app/assets/javascripts/monitoring/components/dashboard_header.vue b/app/assets/javascripts/monitoring/components/dashboard_header.vue index 3338635bf96..8956fe166e8 100644 --- a/app/assets/javascripts/monitoring/components/dashboard_header.vue +++ b/app/assets/javascripts/monitoring/components/dashboard_header.vue @@ -202,7 +202,7 @@ export default { <gl-dropdown-item v-for="environment in filteredEnvironments" :key="environment.id" - :is-check-item="true" + is-check-item :is-checked="environment.name === currentEnvironmentName" :href="getEnvironmentPath(environment.id)" > diff --git a/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue b/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue index 568c66cf152..7fae684315c 100644 --- a/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue +++ b/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue @@ -86,7 +86,7 @@ export default { <gl-dropdown-item v-for="dashboard in starredDashboards" :key="dashboard.path" - :is-check-item="true" + is-check-item :is-checked="dashboard.path === selectedDashboardPath" @click="selectDashboard(dashboard)" > @@ -105,7 +105,7 @@ export default { <gl-dropdown-item v-for="dashboard in nonStarredDashboards" :key="dashboard.path" - :is-check-item="true" + is-check-item :is-checked="dashboard.path === selectedDashboardPath" @click="selectDashboard(dashboard)" > diff --git a/app/assets/javascripts/monitoring/components/refresh_button.vue b/app/assets/javascripts/monitoring/components/refresh_button.vue index 0b80043a92c..544fe10f26e 100644 --- a/app/assets/javascripts/monitoring/components/refresh_button.vue +++ b/app/assets/javascripts/monitoring/components/refresh_button.vue @@ -163,7 +163,7 @@ export default { :text="dropdownText" > <gl-dropdown-item - :is-check-item="true" + is-check-item :is-checked="refreshInterval === null" @click="removeRefreshInterval()" >{{ __('Off') }}</gl-dropdown-item @@ -172,7 +172,7 @@ export default { <gl-dropdown-item v-for="(option, i) in $options.refreshIntervals" :key="i" - :is-check-item="true" + is-check-item :is-checked="isChecked(option)" @click="setRefreshInterval(option)" >{{ option.label }}</gl-dropdown-item diff --git a/app/assets/javascripts/notes/components/discussion_filter.vue b/app/assets/javascripts/notes/components/discussion_filter.vue index 10e651033e8..8a42fb6bd85 100644 --- a/app/assets/javascripts/notes/components/discussion_filter.vue +++ b/app/assets/javascripts/notes/components/discussion_filter.vue @@ -177,7 +177,7 @@ export default { v-for="{ text, key, cls } in $options.SORT_OPTIONS" :key="text" :class="cls" - :is-check-item="true" + is-check-item :is-checked="isSortDropdownItemActive(key)" @click="fetchSortedDiscussions(key)" > @@ -192,7 +192,7 @@ export default { <gl-dropdown-item v-for="filter in filters" :key="filter.value" - :is-check-item="true" + is-check-item :is-checked="filter.value === currentValue" :class="{ 'is-active': filter.value === currentValue }" :data-filter-type="filterType(filter.value)" diff --git a/app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue b/app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue index d7e68484143..08d24344ffc 100644 --- a/app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue +++ b/app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue @@ -180,7 +180,7 @@ export default { v-for="({ group_name }, index) in dailyCoverageData" :key="index" :value="group_name" - :is-check-item="true" + is-check-item :is-checked="index === selectedCoverageIndex" @click="setSelectedCoverage(index)" > diff --git a/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue b/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue index 4398ba67d47..1f8ddae3696 100644 --- a/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue +++ b/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue @@ -237,7 +237,7 @@ export default { v-for="branch in availableBranches" :key="branch" :is-checked="currentBranch === branch" - :is-check-item="true" + is-check-item data-qa-selector="branch_menu_item_button" @click="selectBranch(branch)" > diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_template_dropdown.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_template_dropdown.vue index bdd9f940d79..315f0743b53 100644 --- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_template_dropdown.vue +++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_template_dropdown.vue @@ -100,7 +100,7 @@ export default { <gl-dropdown-item v-for="template in item" :key="template.key" - :is-check-item="true" + is-check-item :is-checked=" template.project_id === selectedFileTemplateProjectId && template.name === selectedTemplate diff --git a/app/assets/javascripts/search/topbar/components/searchable_dropdown.vue b/app/assets/javascripts/search/topbar/components/searchable_dropdown.vue index 5653cddda60..ff639d538b3 100644 --- a/app/assets/javascripts/search/topbar/components/searchable_dropdown.vue +++ b/app/assets/javascripts/search/topbar/components/searchable_dropdown.vue @@ -144,9 +144,9 @@ export default { /> <gl-dropdown-item class="gl-border-b-solid gl-border-b-gray-100 gl-border-b-1 gl-pb-2! gl-mb-2" - :is-check-item="true" + is-check-item :is-checked="isSelected($options.ANY_OPTION)" - :is-check-centered="true" + is-check-centered @click="updateDropdown($options.ANY_OPTION)" > <span data-testid="item-title">{{ $options.ANY_OPTION.name }}</span> diff --git a/app/assets/javascripts/search/topbar/components/searchable_dropdown_item.vue b/app/assets/javascripts/search/topbar/components/searchable_dropdown_item.vue index a4254a355a2..70156142365 100644 --- a/app/assets/javascripts/search/topbar/components/searchable_dropdown_item.vue +++ b/app/assets/javascripts/search/topbar/components/searchable_dropdown_item.vue @@ -53,9 +53,9 @@ export default { <template> <gl-dropdown-item - :is-check-item="true" + is-check-item :is-checked="isSelected" - :is-check-centered="true" + is-check-centered @click="$emit('change', item)" > <div class="gl-display-flex gl-align-items-center"> diff --git a/app/assets/javascripts/sidebar/components/incidents/escalation_status.vue b/app/assets/javascripts/sidebar/components/incidents/escalation_status.vue index aeaac76cff4..9c41db98c63 100644 --- a/app/assets/javascripts/sidebar/components/incidents/escalation_status.vue +++ b/app/assets/javascripts/sidebar/components/incidents/escalation_status.vue @@ -62,7 +62,7 @@ export default { v-for="status in $options.STATUS_LIST" :key="status" data-testid="status-dropdown-item" - :is-check-item="true" + is-check-item :is-checked="status === value" @click="$emit('input', status)" > diff --git a/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue b/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue index bf4ba715f85..a562df4ecd6 100644 --- a/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue +++ b/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue @@ -179,7 +179,7 @@ export default { v-for="option in severitiesList" :key="option.value" data-testid="severityDropdownItem" - :is-check-item="true" + is-check-item :is-checked="option.value === severity" @click="updateSeverity(option.value)" > diff --git a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue index e50d4ae79c8..b62a2c7bcd1 100644 --- a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue +++ b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue @@ -369,7 +369,7 @@ export default { <gl-search-box-by-type ref="search" v-model="searchTerm" /> <gl-dropdown-item :data-testid="`no-${formatIssuableAttribute.kebab}-item`" - :is-check-item="true" + is-check-item :is-checked="isAttributeChecked($options.noAttributeId)" @click="updateAttribute($options.noAttributeId)" > @@ -396,7 +396,7 @@ export default { <gl-dropdown-item v-for="attrItem in attributesList" :key="attrItem.id" - :is-check-item="true" + is-check-item :is-checked="isAttributeChecked(attrItem.id)" :data-testid="`${formatIssuableAttribute.kebab}-items`" @click="updateAttribute(attrItem.id)" diff --git a/app/assets/javascripts/vue_shared/components/actions_button.vue b/app/assets/javascripts/vue_shared/components/actions_button.vue index 6db18afe51c..c6c22f9c61f 100644 --- a/app/assets/javascripts/vue_shared/components/actions_button.vue +++ b/app/assets/javascripts/vue_shared/components/actions_button.vue @@ -77,7 +77,7 @@ export default { <template v-for="(action, index) in actions"> <gl-dropdown-item :key="action.key" - :is-check-item="true" + is-check-item :is-checked="action.key === selectedAction.key" :secondary-text="action.secondaryText" :data-qa-selector="`${action.key}_menu_item`" diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_contents_color_view.vue b/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_contents_color_view.vue index 91906388049..22f3c35b9c3 100644 --- a/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_contents_color_view.vue +++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_contents_color_view.vue @@ -42,8 +42,8 @@ export default { v-for="color in colors" :key="color.color" :is-checked="isColorSelected(color)" - :is-check-centered="true" - :is-check-item="true" + is-check-centered + is-check-item @click.native.capture.stop="handleColorClick(color)" > <color-item :color="color.color" :title="color.title" /> diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue index 840911dc99c..faa50a50c69 100644 --- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue +++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue @@ -149,8 +149,8 @@ export default { v-for="option in presetOptions" :key="option.id" :is-checked="isSelected(option)" - :is-check-centered="true" - :is-check-item="true" + is-check-centered + is-check-item @click.native.capture.stop="selectOption(option)" > <slot name="preset-item" :item="option"> diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue index 33d507dad57..e311df6e66f 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue @@ -369,7 +369,7 @@ export default { <gl-dropdown-item v-for="sortBy in sortOptions" :key="sortBy.id" - :is-check-item="true" + is-check-item :is-checked="sortBy.id === selectedSortOption.id" @click="handleSortOptionClick(sortBy)" >{{ sortBy.title }}</gl-dropdown-item diff --git a/app/assets/javascripts/vue_shared/components/registry/persisted_dropdown_selection.vue b/app/assets/javascripts/vue_shared/components/registry/persisted_dropdown_selection.vue index 43a8e241d77..32d7cdad568 100644 --- a/app/assets/javascripts/vue_shared/components/registry/persisted_dropdown_selection.vue +++ b/app/assets/javascripts/vue_shared/components/registry/persisted_dropdown_selection.vue @@ -49,7 +49,7 @@ export default { v-for="option in parsedOptions" :key="option.value" :is-checked="option.selected" - :is-check-item="true" + is-check-item @click="setSelected(option.value)" > {{ option.label }} diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue index bfaf3b92c34..c5d3704ead9 100644 --- a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue +++ b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue @@ -253,7 +253,7 @@ export default { <gl-dropdown-item v-for="architecture in architectures" :key="architecture.name" - :is-check-item="true" + is-check-item :is-checked="selectedArchitecture === architecture.name" data-testid="architecture-dropdown-item" @click="selectArchitecture(architecture.name)" diff --git a/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue b/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue index dfa2ca2d20c..0f5560ff628 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue @@ -180,7 +180,7 @@ export default { <gl-dropdown-item v-for="project in projects" :key="project.id" - :is-check-item="true" + is-check-item :is-checked="isSelectedProject(project)" @click.stop.prevent="handleProjectSelect(project)" >{{ project.name_with_namespace }}</gl-dropdown-item diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue index f595e635f2c..8d3d4d5f86a 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue @@ -154,8 +154,8 @@ export default { v-for="(label, index) in visibleLabels" :key="label.id" :is-checked="isLabelSelected(label)" - :is-check-centered="true" - :is-check-item="true" + is-check-centered + is-check-item :active="shouldHighlightFirstItem && index === 0" active-class="is-focused" data-testid="labels-list" diff --git a/app/assets/javascripts/vue_shared/components/split_button.vue b/app/assets/javascripts/vue_shared/components/split_button.vue index 994fa68fb1a..c0aef42b0f2 100644 --- a/app/assets/javascripts/vue_shared/components/split_button.vue +++ b/app/assets/javascripts/vue_shared/components/split_button.vue @@ -68,7 +68,7 @@ export default { <template v-for="(item, itemIndex) in actionItems"> <gl-dropdown-item :key="item.eventName" - :is-check-item="true" + is-check-item :is-checked="selectedItem === item" @click="changeSelectedItem(item)" > diff --git a/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue b/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue index 42334d80eec..ce65266cbc9 100644 --- a/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue +++ b/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue @@ -72,7 +72,7 @@ export default { v-for="timezone in filteredResults" :key="timezone.formattedTimezone" :is-checked="isSelected(timezone)" - :is-check-item="true" + is-check-item @click="selectTimezone(timezone)" > {{ timezone.formattedTimezone }} diff --git a/app/assets/javascripts/vue_shared/components/user_select/user_select.vue b/app/assets/javascripts/vue_shared/components/user_select/user_select.vue index 43a590c2367..3180bd0d283 100644 --- a/app/assets/javascripts/vue_shared/components/user_select/user_select.vue +++ b/app/assets/javascripts/vue_shared/components/user_select/user_select.vue @@ -320,7 +320,7 @@ export default { <gl-dropdown-item v-if="isSearchEmpty" :is-checked="selectedIsEmpty" - :is-check-centered="true" + is-check-centered data-testid="unassign" @click.native.capture.stop="$emit('input', [])" > diff --git a/doc/api/projects.md b/doc/api/projects.md index 32b26f32726..56fa36a44f8 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -2248,6 +2248,19 @@ Returned object: } ``` +## Remove a project avatar + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/92604) in GitLab 15.4. + +To remove a project avatar, use a blank value for the `avatar` attribute. + +Example request: + +```shell +curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" \ + --data "avatar=" "https://gitlab.example.com/api/v4/projects/5" +``` + ## Share project with group Allow to share project with group. diff --git a/doc/ci/pipelines/job_artifacts.md b/doc/ci/pipelines/job_artifacts.md index c8babe3320d..6ca42479e8e 100644 --- a/doc/ci/pipelines/job_artifacts.md +++ b/doc/ci/pipelines/job_artifacts.md @@ -451,3 +451,12 @@ test-job: reports: dotenv: build.env ``` + +### Job artifacts are not expired + +If some job artifacts are not expiring as expected, check if the +[**Keep artifacts from most recent successful jobs**](#keep-artifacts-from-most-recent-successful-jobs) +setting is enabled. + +When this setting is enabled, job artifacts from the latest successful pipeline +of each ref do not expire and are not deleted. diff --git a/doc/development/backend/create_source_code_be/index.md b/doc/development/backend/create_source_code_be/index.md index e1ee78731de..a1322b3fa25 100644 --- a/doc/development/backend/create_source_code_be/index.md +++ b/doc/development/backend/create_source_code_be/index.md @@ -33,6 +33,9 @@ GitLab Shell handles Git SSH sessions for GitLab and modifies the list of author For more information, [refer to the README](https://gitlab.com/gitlab-org/gitlab-shell/-/blob/main/README.md). for GitLab Shell. +To learn about the reasoning behind our creation of `gitlab-sshd`, read the blog post +[Why we implemented our own SSHD solution](https://about.gitlab.com/blog/2022/08/17/why-we-have-implemented-our-own-sshd-solution-on-gitlab-sass/). + ## GitLab Rails ### Gitaly touch points diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb index 628182ad1ab..2d0e8ae4bfd 100644 --- a/lib/api/helpers/projects_helpers.rb +++ b/lib/api/helpers/projects_helpers.rb @@ -58,7 +58,7 @@ module API optional :tag_list, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'Deprecated: Use :topics instead' optional :topics, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'The list of topics for a project' # TODO: remove rubocop disable - https://gitlab.com/gitlab-org/gitlab/issues/14960 - optional :avatar, type: File, desc: 'Avatar image for project' # rubocop:disable Scalability/FileUploads + optional :avatar, type: ::API::Validations::Types::WorkhorseFile, desc: 'Avatar image for project' optional :printing_merge_request_link_enabled, type: Boolean, desc: 'Show link to create/view merge request when pushing from the command line' optional :merge_method, type: String, values: %w(ff rebase_merge merge), desc: 'The merge method used when merging merge requests' optional :suggestion_commit_message, type: String, desc: 'The commit message used to apply merge request suggestions' diff --git a/lib/api/projects.rb b/lib/api/projects.rb index f8dc3d45a5c..8c58cc585d8 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -453,6 +453,8 @@ module API filter_attributes_using_license!(attrs) verify_update_project_attrs!(user_project, attrs) + user_project.remove_avatar! if attrs.key?(:avatar) && attrs[:avatar].nil? + result = ::Projects::UpdateService.new(user_project, current_user, attrs).execute if result[:status] == :success diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index fcbd0cc784b..ac534b4e711 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -48,6 +48,7 @@ end RSpec.describe API::Projects do include ProjectForksHelper + include WorkhorseHelpers include StubRequests let_it_be(:user) { create(:user) } @@ -1349,7 +1350,12 @@ RSpec.describe API::Projects do it 'uploads avatar for project a project' do project = attributes_for(:project, avatar: fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif')) - post api('/projects', user), params: project + workhorse_form_with_file( + api('/projects', user), + method: :post, + file_key: :avatar, + params: project + ) project_id = json_response['id'] expect(json_response['avatar_url']).to eq("http://localhost/uploads/-/system/project/avatar/#{project_id}/banana_sample.gif") @@ -1925,8 +1931,6 @@ RSpec.describe API::Projects do end describe "POST /projects/:id/uploads/authorize" do - include WorkhorseHelpers - let(:headers) { workhorse_internal_api_request_header.merge({ 'HTTP_GITLAB_WORKHORSE' => 1 }) } context 'with authorized user' do @@ -3584,18 +3588,77 @@ RSpec.describe API::Projects do end end - it 'updates avatar' do - project_param = { - avatar: fixture_file_upload('spec/fixtures/banana_sample.gif', - 'image/gif') - } + context 'with changes to the avatar' do + let_it_be(:avatar_file) { fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif') } + let_it_be(:alternate_avatar_file) { fixture_file_upload('spec/fixtures/rails_sample.png', 'image/png') } + let_it_be(:project_with_avatar, reload: true) do + create(:project, + :private, + :repository, + name: 'project-with-avatar', + creator_id: user.id, + namespace: user.namespace, + avatar: avatar_file) + end - put api("/projects/#{project3.id}", user), params: project_param + it 'uploads avatar to project without an avatar' do + workhorse_form_with_file( + api("/projects/#{project3.id}", user), + method: :put, + file_key: :avatar, + params: { avatar: avatar_file } + ) - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['avatar_url']).to eq('http://localhost/uploads/'\ - '-/system/project/avatar/'\ - "#{project3.id}/banana_sample.gif") + aggregate_failures "testing response" do + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['avatar_url']).to eq('http://localhost/uploads/'\ + '-/system/project/avatar/'\ + "#{project3.id}/banana_sample.gif") + end + end + + it 'uploads and changes avatar to project with an avatar' do + workhorse_form_with_file( + api("/projects/#{project_with_avatar.id}", user), + method: :put, + file_key: :avatar, + params: { avatar: alternate_avatar_file } + ) + + aggregate_failures "testing response" do + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['avatar_url']).to eq('http://localhost/uploads/'\ + '-/system/project/avatar/'\ + "#{project_with_avatar.id}/rails_sample.png") + end + end + + it 'uploads and changes avatar to project among other changes' do + workhorse_form_with_file( + api("/projects/#{project_with_avatar.id}", user), + method: :put, + file_key: :avatar, + params: { description: 'changed description', avatar: avatar_file } + ) + + aggregate_failures "testing response" do + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['description']).to eq('changed description') + expect(json_response['avatar_url']).to eq('http://localhost/uploads/'\ + '-/system/project/avatar/'\ + "#{project_with_avatar.id}/banana_sample.gif") + end + end + + it 'removes avatar from project with an avatar' do + put api("/projects/#{project_with_avatar.id}", user), params: { avatar: '' } + + aggregate_failures "testing response" do + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['avatar_url']).to be_nil + expect(project_with_avatar.reload.avatar_url).to be_nil + end + end end it 'updates auto_devops_deploy_strategy' do diff --git a/workhorse/internal/upstream/routes.go b/workhorse/internal/upstream/routes.go index 311028750a5..0bd86cfe039 100644 --- a/workhorse/internal/upstream/routes.go +++ b/workhorse/internal/upstream/routes.go @@ -51,7 +51,7 @@ const ( gitProjectPattern = `^/.+\.git/` geoGitProjectPattern = `^/[^-].+\.git/` // Prevent matching routes like /-/push_from_secondary projectPattern = `^/([^/]+/){1,}[^/]+/` - apiProjectPattern = apiPattern + `v4/projects/[^/]+/` // API: Projects can be encoded via group%2Fsubgroup%2Fproject + apiProjectPattern = apiPattern + `v4/projects/[^/]+` // API: Projects can be encoded via group%2Fsubgroup%2Fproject apiTopicPattern = apiPattern + `v4/topics` snippetUploadPattern = `^/uploads/personal_snippet` userUploadPattern = `^/uploads/user` @@ -269,40 +269,40 @@ func configureRoutes(u *upstream) { // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56731. // Maven Artifact Repository - u.route("PUT", apiProjectPattern+`packages/maven/`, requestBodyUploader), + u.route("PUT", apiProjectPattern+`/packages/maven/`, requestBodyUploader), // Conan Artifact Repository u.route("PUT", apiPattern+`v4/packages/conan/`, requestBodyUploader), - u.route("PUT", apiProjectPattern+`packages/conan/`, requestBodyUploader), + u.route("PUT", apiProjectPattern+`/packages/conan/`, requestBodyUploader), // Generic Packages Repository - u.route("PUT", apiProjectPattern+`packages/generic/`, requestBodyUploader), + u.route("PUT", apiProjectPattern+`/packages/generic/`, requestBodyUploader), // NuGet Artifact Repository - u.route("PUT", apiProjectPattern+`packages/nuget/`, mimeMultipartUploader), + u.route("PUT", apiProjectPattern+`/packages/nuget/`, mimeMultipartUploader), // PyPI Artifact Repository - u.route("POST", apiProjectPattern+`packages/pypi`, mimeMultipartUploader), + u.route("POST", apiProjectPattern+`/packages/pypi`, mimeMultipartUploader), // Debian Artifact Repository - u.route("PUT", apiProjectPattern+`packages/debian/`, requestBodyUploader), + u.route("PUT", apiProjectPattern+`/packages/debian/`, requestBodyUploader), // RPM Artifact Repository u.route("POST", apiProjectPattern+`packages/rpm/`, requestBodyUploader), // Gem Artifact Repository - u.route("POST", apiProjectPattern+`packages/rubygems/`, requestBodyUploader), + u.route("POST", apiProjectPattern+`/packages/rubygems/`, requestBodyUploader), // Terraform Module Package Repository - u.route("PUT", apiProjectPattern+`packages/terraform/modules/`, requestBodyUploader), + u.route("PUT", apiProjectPattern+`/packages/terraform/modules/`, requestBodyUploader), // Helm Artifact Repository - u.route("POST", apiProjectPattern+`packages/helm/api/[^/]+/charts\z`, mimeMultipartUploader), + u.route("POST", apiProjectPattern+`/packages/helm/api/[^/]+/charts\z`, mimeMultipartUploader), // We are porting API to disk acceleration // we need to declare each routes until we have fixed all the routes on the rails codebase. // Overall status can be seen at https://gitlab.com/groups/gitlab-org/-/epics/1802#current-status - u.route("POST", apiProjectPattern+`wikis/attachments\z`, tempfileMultipartProxy), + u.route("POST", apiProjectPattern+`/wikis/attachments\z`, tempfileMultipartProxy), u.route("POST", apiPattern+`graphql\z`, tempfileMultipartProxy), u.route("POST", apiTopicPattern, tempfileMultipartProxy), u.route("PUT", apiTopicPattern, tempfileMultipartProxy), @@ -315,16 +315,20 @@ func configureRoutes(u *upstream) { u.route("POST", importPattern+`gitlab_group`, mimeMultipartUploader), // Issuable Metric image upload - u.route("POST", apiProjectPattern+`issues/[0-9]+/metric_images\z`, mimeMultipartUploader), + u.route("POST", apiProjectPattern+`/issues/[0-9]+/metric_images\z`, mimeMultipartUploader), // Alert Metric image upload - u.route("POST", apiProjectPattern+`alert_management_alerts/[0-9]+/metric_images\z`, mimeMultipartUploader), + u.route("POST", apiProjectPattern+`/alert_management_alerts/[0-9]+/metric_images\z`, mimeMultipartUploader), // Requirements Import via UI upload acceleration u.route("POST", projectPattern+`requirements_management/requirements/import_csv`, mimeMultipartUploader), // Uploads via API - u.route("POST", apiProjectPattern+`uploads\z`, mimeMultipartUploader), + u.route("POST", apiProjectPattern+`/uploads\z`, mimeMultipartUploader), + + // Project Avatar + u.route("POST", apiPattern+`v4/projects\z`, tempfileMultipartProxy), + u.route("PUT", apiProjectPattern+`\z`, tempfileMultipartProxy), // Explicitly proxy API requests u.route("", apiPattern, proxy), diff --git a/workhorse/upload_test.go b/workhorse/upload_test.go index 2d7806b1790..fd4844220b9 100644 --- a/workhorse/upload_test.go +++ b/workhorse/upload_test.go @@ -122,6 +122,10 @@ func TestAcceleratedUpload(t *testing.T) { {"POST", `/example`, false}, {"POST", `/uploads/personal_snippet`, true}, {"POST", `/uploads/user`, true}, + {"POST", `/api/v4/projects`, false}, + {"PUT", `/api/v4/projects/group%2Fproject`, false}, + {"PUT", `/api/v4/projects/group%2Fsubgroup%2Fproject`, false}, + {"PUT", `/api/v4/projects/39`, false}, {"POST", `/api/v4/projects/1/uploads`, true}, {"POST", `/api/v4/projects/group%2Fproject/uploads`, true}, {"POST", `/api/v4/projects/group%2Fsubgroup%2Fproject/uploads`, true}, |