Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
93c1e0e4c2
commit
485728af8d
|
@ -0,0 +1 @@
|
||||||
|
# This empty file is used for agent-based integration with Kubernetes
|
|
@ -109,7 +109,7 @@ export default {
|
||||||
v-for="template in templates"
|
v-for="template in templates"
|
||||||
:key="template.key"
|
:key="template.key"
|
||||||
data-qa-selector="incident_templates_item"
|
data-qa-selector="incident_templates_item"
|
||||||
:is-check-item="true"
|
is-check-item
|
||||||
:is-checked="isTemplateSelected(template.key)"
|
:is-checked="isTemplateSelected(template.key)"
|
||||||
@click="selectIssueTemplate(template.key)"
|
@click="selectIssueTemplate(template.key)"
|
||||||
>
|
>
|
||||||
|
|
|
@ -40,7 +40,7 @@ export default {
|
||||||
<gl-dropdown-item
|
<gl-dropdown-item
|
||||||
v-for="project in projects"
|
v-for="project in projects"
|
||||||
:key="project.id"
|
:key="project.id"
|
||||||
:is-check-item="true"
|
is-check-item
|
||||||
:is-checked="project.id === selectedProject.id"
|
:is-checked="project.id === selectedProject.id"
|
||||||
@click="selectProject(project)"
|
@click="selectProject(project)"
|
||||||
>
|
>
|
||||||
|
|
|
@ -182,7 +182,7 @@ export default {
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="!showCustomLanguageInput" #highlighted-items>
|
<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 }}
|
{{ selectedLanguage.label }}
|
||||||
</gl-dropdown-item>
|
</gl-dropdown-item>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -73,8 +73,8 @@ export default {
|
||||||
<gl-dropdown-item
|
<gl-dropdown-item
|
||||||
v-for="(version, index) in allVersions"
|
v-for="(version, index) in allVersions"
|
||||||
:key="version.id"
|
:key="version.id"
|
||||||
:is-check-item="true"
|
is-check-item
|
||||||
:is-check-centered="true"
|
is-check-centered
|
||||||
:is-checked="findVersionId(version.id) === currentVersionId"
|
:is-checked="findVersionId(version.id) === currentVersionId"
|
||||||
:avatar-url="getAvatarUrl(version)"
|
:avatar-url="getAvatarUrl(version)"
|
||||||
@click="routeToVersion(version.id)"
|
@click="routeToVersion(version.id)"
|
||||||
|
|
|
@ -37,7 +37,7 @@ export default {
|
||||||
:class="{
|
:class="{
|
||||||
'is-active': version.selected,
|
'is-active': version.selected,
|
||||||
}"
|
}"
|
||||||
:is-check-item="true"
|
is-check-item
|
||||||
:is-checked="version.selected"
|
:is-checked="version.selected"
|
||||||
:href="version.href"
|
:href="version.href"
|
||||||
>
|
>
|
||||||
|
|
|
@ -50,7 +50,7 @@ export default {
|
||||||
<gl-dropdown-item
|
<gl-dropdown-item
|
||||||
v-for="mode in modeDropdownItems"
|
v-for="mode in modeDropdownItems"
|
||||||
:key="mode.viewerType"
|
:key="mode.viewerType"
|
||||||
:is-check-item="true"
|
is-check-item
|
||||||
:is-checked="viewer === mode.viewerType"
|
:is-checked="viewer === mode.viewerType"
|
||||||
@click="changeMode(mode.viewerType)"
|
@click="changeMode(mode.viewerType)"
|
||||||
>
|
>
|
||||||
|
|
|
@ -243,7 +243,7 @@ export default {
|
||||||
v-for="(item, idx) in extraLinks"
|
v-for="(item, idx) in extraLinks"
|
||||||
:key="idx"
|
:key="idx"
|
||||||
:href="item.url"
|
:href="item.url"
|
||||||
:is-check-item="true"
|
is-check-item
|
||||||
data-testid="milestone-combobox-extra-links"
|
data-testid="milestone-combobox-extra-links"
|
||||||
>
|
>
|
||||||
{{ item.text }}
|
{{ item.text }}
|
||||||
|
|
|
@ -202,7 +202,7 @@ export default {
|
||||||
<gl-dropdown-item
|
<gl-dropdown-item
|
||||||
v-for="environment in filteredEnvironments"
|
v-for="environment in filteredEnvironments"
|
||||||
:key="environment.id"
|
:key="environment.id"
|
||||||
:is-check-item="true"
|
is-check-item
|
||||||
:is-checked="environment.name === currentEnvironmentName"
|
:is-checked="environment.name === currentEnvironmentName"
|
||||||
:href="getEnvironmentPath(environment.id)"
|
:href="getEnvironmentPath(environment.id)"
|
||||||
>
|
>
|
||||||
|
|
|
@ -86,7 +86,7 @@ export default {
|
||||||
<gl-dropdown-item
|
<gl-dropdown-item
|
||||||
v-for="dashboard in starredDashboards"
|
v-for="dashboard in starredDashboards"
|
||||||
:key="dashboard.path"
|
:key="dashboard.path"
|
||||||
:is-check-item="true"
|
is-check-item
|
||||||
:is-checked="dashboard.path === selectedDashboardPath"
|
:is-checked="dashboard.path === selectedDashboardPath"
|
||||||
@click="selectDashboard(dashboard)"
|
@click="selectDashboard(dashboard)"
|
||||||
>
|
>
|
||||||
|
@ -105,7 +105,7 @@ export default {
|
||||||
<gl-dropdown-item
|
<gl-dropdown-item
|
||||||
v-for="dashboard in nonStarredDashboards"
|
v-for="dashboard in nonStarredDashboards"
|
||||||
:key="dashboard.path"
|
:key="dashboard.path"
|
||||||
:is-check-item="true"
|
is-check-item
|
||||||
:is-checked="dashboard.path === selectedDashboardPath"
|
:is-checked="dashboard.path === selectedDashboardPath"
|
||||||
@click="selectDashboard(dashboard)"
|
@click="selectDashboard(dashboard)"
|
||||||
>
|
>
|
||||||
|
|
|
@ -163,7 +163,7 @@ export default {
|
||||||
:text="dropdownText"
|
:text="dropdownText"
|
||||||
>
|
>
|
||||||
<gl-dropdown-item
|
<gl-dropdown-item
|
||||||
:is-check-item="true"
|
is-check-item
|
||||||
:is-checked="refreshInterval === null"
|
:is-checked="refreshInterval === null"
|
||||||
@click="removeRefreshInterval()"
|
@click="removeRefreshInterval()"
|
||||||
>{{ __('Off') }}</gl-dropdown-item
|
>{{ __('Off') }}</gl-dropdown-item
|
||||||
|
@ -172,7 +172,7 @@ export default {
|
||||||
<gl-dropdown-item
|
<gl-dropdown-item
|
||||||
v-for="(option, i) in $options.refreshIntervals"
|
v-for="(option, i) in $options.refreshIntervals"
|
||||||
:key="i"
|
:key="i"
|
||||||
:is-check-item="true"
|
is-check-item
|
||||||
:is-checked="isChecked(option)"
|
:is-checked="isChecked(option)"
|
||||||
@click="setRefreshInterval(option)"
|
@click="setRefreshInterval(option)"
|
||||||
>{{ option.label }}</gl-dropdown-item
|
>{{ option.label }}</gl-dropdown-item
|
||||||
|
|
|
@ -177,7 +177,7 @@ export default {
|
||||||
v-for="{ text, key, cls } in $options.SORT_OPTIONS"
|
v-for="{ text, key, cls } in $options.SORT_OPTIONS"
|
||||||
:key="text"
|
:key="text"
|
||||||
:class="cls"
|
:class="cls"
|
||||||
:is-check-item="true"
|
is-check-item
|
||||||
:is-checked="isSortDropdownItemActive(key)"
|
:is-checked="isSortDropdownItemActive(key)"
|
||||||
@click="fetchSortedDiscussions(key)"
|
@click="fetchSortedDiscussions(key)"
|
||||||
>
|
>
|
||||||
|
@ -192,7 +192,7 @@ export default {
|
||||||
<gl-dropdown-item
|
<gl-dropdown-item
|
||||||
v-for="filter in filters"
|
v-for="filter in filters"
|
||||||
:key="filter.value"
|
:key="filter.value"
|
||||||
:is-check-item="true"
|
is-check-item
|
||||||
:is-checked="filter.value === currentValue"
|
:is-checked="filter.value === currentValue"
|
||||||
:class="{ 'is-active': filter.value === currentValue }"
|
:class="{ 'is-active': filter.value === currentValue }"
|
||||||
:data-filter-type="filterType(filter.value)"
|
:data-filter-type="filterType(filter.value)"
|
||||||
|
|
|
@ -180,7 +180,7 @@ export default {
|
||||||
v-for="({ group_name }, index) in dailyCoverageData"
|
v-for="({ group_name }, index) in dailyCoverageData"
|
||||||
:key="index"
|
:key="index"
|
||||||
:value="group_name"
|
:value="group_name"
|
||||||
:is-check-item="true"
|
is-check-item
|
||||||
:is-checked="index === selectedCoverageIndex"
|
:is-checked="index === selectedCoverageIndex"
|
||||||
@click="setSelectedCoverage(index)"
|
@click="setSelectedCoverage(index)"
|
||||||
>
|
>
|
||||||
|
|
|
@ -237,7 +237,7 @@ export default {
|
||||||
v-for="branch in availableBranches"
|
v-for="branch in availableBranches"
|
||||||
:key="branch"
|
:key="branch"
|
||||||
:is-checked="currentBranch === branch"
|
:is-checked="currentBranch === branch"
|
||||||
:is-check-item="true"
|
is-check-item
|
||||||
data-qa-selector="branch_menu_item_button"
|
data-qa-selector="branch_menu_item_button"
|
||||||
@click="selectBranch(branch)"
|
@click="selectBranch(branch)"
|
||||||
>
|
>
|
||||||
|
|
|
@ -100,7 +100,7 @@ export default {
|
||||||
<gl-dropdown-item
|
<gl-dropdown-item
|
||||||
v-for="template in item"
|
v-for="template in item"
|
||||||
:key="template.key"
|
:key="template.key"
|
||||||
:is-check-item="true"
|
is-check-item
|
||||||
:is-checked="
|
:is-checked="
|
||||||
template.project_id === selectedFileTemplateProjectId &&
|
template.project_id === selectedFileTemplateProjectId &&
|
||||||
template.name === selectedTemplate
|
template.name === selectedTemplate
|
||||||
|
|
|
@ -144,9 +144,9 @@ export default {
|
||||||
/>
|
/>
|
||||||
<gl-dropdown-item
|
<gl-dropdown-item
|
||||||
class="gl-border-b-solid gl-border-b-gray-100 gl-border-b-1 gl-pb-2! gl-mb-2"
|
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-checked="isSelected($options.ANY_OPTION)"
|
||||||
:is-check-centered="true"
|
is-check-centered
|
||||||
@click="updateDropdown($options.ANY_OPTION)"
|
@click="updateDropdown($options.ANY_OPTION)"
|
||||||
>
|
>
|
||||||
<span data-testid="item-title">{{ $options.ANY_OPTION.name }}</span>
|
<span data-testid="item-title">{{ $options.ANY_OPTION.name }}</span>
|
||||||
|
|
|
@ -53,9 +53,9 @@ export default {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<gl-dropdown-item
|
<gl-dropdown-item
|
||||||
:is-check-item="true"
|
is-check-item
|
||||||
:is-checked="isSelected"
|
:is-checked="isSelected"
|
||||||
:is-check-centered="true"
|
is-check-centered
|
||||||
@click="$emit('change', item)"
|
@click="$emit('change', item)"
|
||||||
>
|
>
|
||||||
<div class="gl-display-flex gl-align-items-center">
|
<div class="gl-display-flex gl-align-items-center">
|
||||||
|
|
|
@ -62,7 +62,7 @@ export default {
|
||||||
v-for="status in $options.STATUS_LIST"
|
v-for="status in $options.STATUS_LIST"
|
||||||
:key="status"
|
:key="status"
|
||||||
data-testid="status-dropdown-item"
|
data-testid="status-dropdown-item"
|
||||||
:is-check-item="true"
|
is-check-item
|
||||||
:is-checked="status === value"
|
:is-checked="status === value"
|
||||||
@click="$emit('input', status)"
|
@click="$emit('input', status)"
|
||||||
>
|
>
|
||||||
|
|
|
@ -179,7 +179,7 @@ export default {
|
||||||
v-for="option in severitiesList"
|
v-for="option in severitiesList"
|
||||||
:key="option.value"
|
:key="option.value"
|
||||||
data-testid="severityDropdownItem"
|
data-testid="severityDropdownItem"
|
||||||
:is-check-item="true"
|
is-check-item
|
||||||
:is-checked="option.value === severity"
|
:is-checked="option.value === severity"
|
||||||
@click="updateSeverity(option.value)"
|
@click="updateSeverity(option.value)"
|
||||||
>
|
>
|
||||||
|
|
|
@ -369,7 +369,7 @@ export default {
|
||||||
<gl-search-box-by-type ref="search" v-model="searchTerm" />
|
<gl-search-box-by-type ref="search" v-model="searchTerm" />
|
||||||
<gl-dropdown-item
|
<gl-dropdown-item
|
||||||
:data-testid="`no-${formatIssuableAttribute.kebab}-item`"
|
:data-testid="`no-${formatIssuableAttribute.kebab}-item`"
|
||||||
:is-check-item="true"
|
is-check-item
|
||||||
:is-checked="isAttributeChecked($options.noAttributeId)"
|
:is-checked="isAttributeChecked($options.noAttributeId)"
|
||||||
@click="updateAttribute($options.noAttributeId)"
|
@click="updateAttribute($options.noAttributeId)"
|
||||||
>
|
>
|
||||||
|
@ -396,7 +396,7 @@ export default {
|
||||||
<gl-dropdown-item
|
<gl-dropdown-item
|
||||||
v-for="attrItem in attributesList"
|
v-for="attrItem in attributesList"
|
||||||
:key="attrItem.id"
|
:key="attrItem.id"
|
||||||
:is-check-item="true"
|
is-check-item
|
||||||
:is-checked="isAttributeChecked(attrItem.id)"
|
:is-checked="isAttributeChecked(attrItem.id)"
|
||||||
:data-testid="`${formatIssuableAttribute.kebab}-items`"
|
:data-testid="`${formatIssuableAttribute.kebab}-items`"
|
||||||
@click="updateAttribute(attrItem.id)"
|
@click="updateAttribute(attrItem.id)"
|
||||||
|
|
|
@ -77,7 +77,7 @@ export default {
|
||||||
<template v-for="(action, index) in actions">
|
<template v-for="(action, index) in actions">
|
||||||
<gl-dropdown-item
|
<gl-dropdown-item
|
||||||
:key="action.key"
|
:key="action.key"
|
||||||
:is-check-item="true"
|
is-check-item
|
||||||
:is-checked="action.key === selectedAction.key"
|
:is-checked="action.key === selectedAction.key"
|
||||||
:secondary-text="action.secondaryText"
|
:secondary-text="action.secondaryText"
|
||||||
:data-qa-selector="`${action.key}_menu_item`"
|
:data-qa-selector="`${action.key}_menu_item`"
|
||||||
|
|
|
@ -42,8 +42,8 @@ export default {
|
||||||
v-for="color in colors"
|
v-for="color in colors"
|
||||||
:key="color.color"
|
:key="color.color"
|
||||||
:is-checked="isColorSelected(color)"
|
:is-checked="isColorSelected(color)"
|
||||||
:is-check-centered="true"
|
is-check-centered
|
||||||
:is-check-item="true"
|
is-check-item
|
||||||
@click.native.capture.stop="handleColorClick(color)"
|
@click.native.capture.stop="handleColorClick(color)"
|
||||||
>
|
>
|
||||||
<color-item :color="color.color" :title="color.title" />
|
<color-item :color="color.color" :title="color.title" />
|
||||||
|
|
|
@ -149,8 +149,8 @@ export default {
|
||||||
v-for="option in presetOptions"
|
v-for="option in presetOptions"
|
||||||
:key="option.id"
|
:key="option.id"
|
||||||
:is-checked="isSelected(option)"
|
:is-checked="isSelected(option)"
|
||||||
:is-check-centered="true"
|
is-check-centered
|
||||||
:is-check-item="true"
|
is-check-item
|
||||||
@click.native.capture.stop="selectOption(option)"
|
@click.native.capture.stop="selectOption(option)"
|
||||||
>
|
>
|
||||||
<slot name="preset-item" :item="option">
|
<slot name="preset-item" :item="option">
|
||||||
|
|
|
@ -369,7 +369,7 @@ export default {
|
||||||
<gl-dropdown-item
|
<gl-dropdown-item
|
||||||
v-for="sortBy in sortOptions"
|
v-for="sortBy in sortOptions"
|
||||||
:key="sortBy.id"
|
:key="sortBy.id"
|
||||||
:is-check-item="true"
|
is-check-item
|
||||||
:is-checked="sortBy.id === selectedSortOption.id"
|
:is-checked="sortBy.id === selectedSortOption.id"
|
||||||
@click="handleSortOptionClick(sortBy)"
|
@click="handleSortOptionClick(sortBy)"
|
||||||
>{{ sortBy.title }}</gl-dropdown-item
|
>{{ sortBy.title }}</gl-dropdown-item
|
||||||
|
|
|
@ -49,7 +49,7 @@ export default {
|
||||||
v-for="option in parsedOptions"
|
v-for="option in parsedOptions"
|
||||||
:key="option.value"
|
:key="option.value"
|
||||||
:is-checked="option.selected"
|
:is-checked="option.selected"
|
||||||
:is-check-item="true"
|
is-check-item
|
||||||
@click="setSelected(option.value)"
|
@click="setSelected(option.value)"
|
||||||
>
|
>
|
||||||
{{ option.label }}
|
{{ option.label }}
|
||||||
|
|
|
@ -253,7 +253,7 @@ export default {
|
||||||
<gl-dropdown-item
|
<gl-dropdown-item
|
||||||
v-for="architecture in architectures"
|
v-for="architecture in architectures"
|
||||||
:key="architecture.name"
|
:key="architecture.name"
|
||||||
:is-check-item="true"
|
is-check-item
|
||||||
:is-checked="selectedArchitecture === architecture.name"
|
:is-checked="selectedArchitecture === architecture.name"
|
||||||
data-testid="architecture-dropdown-item"
|
data-testid="architecture-dropdown-item"
|
||||||
@click="selectArchitecture(architecture.name)"
|
@click="selectArchitecture(architecture.name)"
|
||||||
|
|
|
@ -180,7 +180,7 @@ export default {
|
||||||
<gl-dropdown-item
|
<gl-dropdown-item
|
||||||
v-for="project in projects"
|
v-for="project in projects"
|
||||||
:key="project.id"
|
:key="project.id"
|
||||||
:is-check-item="true"
|
is-check-item
|
||||||
:is-checked="isSelectedProject(project)"
|
:is-checked="isSelectedProject(project)"
|
||||||
@click.stop.prevent="handleProjectSelect(project)"
|
@click.stop.prevent="handleProjectSelect(project)"
|
||||||
>{{ project.name_with_namespace }}</gl-dropdown-item
|
>{{ project.name_with_namespace }}</gl-dropdown-item
|
||||||
|
|
|
@ -154,8 +154,8 @@ export default {
|
||||||
v-for="(label, index) in visibleLabels"
|
v-for="(label, index) in visibleLabels"
|
||||||
:key="label.id"
|
:key="label.id"
|
||||||
:is-checked="isLabelSelected(label)"
|
:is-checked="isLabelSelected(label)"
|
||||||
:is-check-centered="true"
|
is-check-centered
|
||||||
:is-check-item="true"
|
is-check-item
|
||||||
:active="shouldHighlightFirstItem && index === 0"
|
:active="shouldHighlightFirstItem && index === 0"
|
||||||
active-class="is-focused"
|
active-class="is-focused"
|
||||||
data-testid="labels-list"
|
data-testid="labels-list"
|
||||||
|
|
|
@ -68,7 +68,7 @@ export default {
|
||||||
<template v-for="(item, itemIndex) in actionItems">
|
<template v-for="(item, itemIndex) in actionItems">
|
||||||
<gl-dropdown-item
|
<gl-dropdown-item
|
||||||
:key="item.eventName"
|
:key="item.eventName"
|
||||||
:is-check-item="true"
|
is-check-item
|
||||||
:is-checked="selectedItem === item"
|
:is-checked="selectedItem === item"
|
||||||
@click="changeSelectedItem(item)"
|
@click="changeSelectedItem(item)"
|
||||||
>
|
>
|
||||||
|
|
|
@ -72,7 +72,7 @@ export default {
|
||||||
v-for="timezone in filteredResults"
|
v-for="timezone in filteredResults"
|
||||||
:key="timezone.formattedTimezone"
|
:key="timezone.formattedTimezone"
|
||||||
:is-checked="isSelected(timezone)"
|
:is-checked="isSelected(timezone)"
|
||||||
:is-check-item="true"
|
is-check-item
|
||||||
@click="selectTimezone(timezone)"
|
@click="selectTimezone(timezone)"
|
||||||
>
|
>
|
||||||
{{ timezone.formattedTimezone }}
|
{{ timezone.formattedTimezone }}
|
||||||
|
|
|
@ -320,7 +320,7 @@ export default {
|
||||||
<gl-dropdown-item
|
<gl-dropdown-item
|
||||||
v-if="isSearchEmpty"
|
v-if="isSearchEmpty"
|
||||||
:is-checked="selectedIsEmpty"
|
:is-checked="selectedIsEmpty"
|
||||||
:is-check-centered="true"
|
is-check-centered
|
||||||
data-testid="unassign"
|
data-testid="unassign"
|
||||||
@click.native.capture.stop="$emit('input', [])"
|
@click.native.capture.stop="$emit('input', [])"
|
||||||
>
|
>
|
||||||
|
|
|
@ -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
|
## Share project with group
|
||||||
|
|
||||||
Allow to share project with group.
|
Allow to share project with group.
|
||||||
|
|
|
@ -451,3 +451,12 @@ test-job:
|
||||||
reports:
|
reports:
|
||||||
dotenv: build.env
|
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.
|
||||||
|
|
|
@ -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 more information, [refer to the README](https://gitlab.com/gitlab-org/gitlab-shell/-/blob/main/README.md).
|
||||||
for GitLab Shell.
|
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
|
## GitLab Rails
|
||||||
|
|
||||||
### Gitaly touch points
|
### Gitaly touch points
|
||||||
|
|
|
@ -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 :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'
|
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
|
# 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 :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 :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'
|
optional :suggestion_commit_message, type: String, desc: 'The commit message used to apply merge request suggestions'
|
||||||
|
|
|
@ -453,6 +453,8 @@ module API
|
||||||
filter_attributes_using_license!(attrs)
|
filter_attributes_using_license!(attrs)
|
||||||
verify_update_project_attrs!(user_project, 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
|
result = ::Projects::UpdateService.new(user_project, current_user, attrs).execute
|
||||||
|
|
||||||
if result[:status] == :success
|
if result[:status] == :success
|
||||||
|
|
|
@ -48,6 +48,7 @@ end
|
||||||
|
|
||||||
RSpec.describe API::Projects do
|
RSpec.describe API::Projects do
|
||||||
include ProjectForksHelper
|
include ProjectForksHelper
|
||||||
|
include WorkhorseHelpers
|
||||||
include StubRequests
|
include StubRequests
|
||||||
|
|
||||||
let_it_be(:user) { create(:user) }
|
let_it_be(:user) { create(:user) }
|
||||||
|
@ -1349,7 +1350,12 @@ RSpec.describe API::Projects do
|
||||||
it 'uploads avatar for project a project' do
|
it 'uploads avatar for project a project' do
|
||||||
project = attributes_for(:project, avatar: fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif'))
|
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']
|
project_id = json_response['id']
|
||||||
expect(json_response['avatar_url']).to eq("http://localhost/uploads/-/system/project/avatar/#{project_id}/banana_sample.gif")
|
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
|
end
|
||||||
|
|
||||||
describe "POST /projects/:id/uploads/authorize" do
|
describe "POST /projects/:id/uploads/authorize" do
|
||||||
include WorkhorseHelpers
|
|
||||||
|
|
||||||
let(:headers) { workhorse_internal_api_request_header.merge({ 'HTTP_GITLAB_WORKHORSE' => 1 }) }
|
let(:headers) { workhorse_internal_api_request_header.merge({ 'HTTP_GITLAB_WORKHORSE' => 1 }) }
|
||||||
|
|
||||||
context 'with authorized user' do
|
context 'with authorized user' do
|
||||||
|
@ -3584,19 +3588,78 @@ RSpec.describe API::Projects do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'updates avatar' do
|
context 'with changes to the avatar' do
|
||||||
project_param = {
|
let_it_be(:avatar_file) { fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif') }
|
||||||
avatar: fixture_file_upload('spec/fixtures/banana_sample.gif',
|
let_it_be(:alternate_avatar_file) { fixture_file_upload('spec/fixtures/rails_sample.png', 'image/png') }
|
||||||
'image/gif')
|
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 }
|
||||||
|
)
|
||||||
|
|
||||||
|
aggregate_failures "testing response" do
|
||||||
expect(response).to have_gitlab_http_status(:ok)
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
expect(json_response['avatar_url']).to eq('http://localhost/uploads/'\
|
expect(json_response['avatar_url']).to eq('http://localhost/uploads/'\
|
||||||
'-/system/project/avatar/'\
|
'-/system/project/avatar/'\
|
||||||
"#{project3.id}/banana_sample.gif")
|
"#{project3.id}/banana_sample.gif")
|
||||||
end
|
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
|
it 'updates auto_devops_deploy_strategy' do
|
||||||
project_param = { auto_devops_deploy_strategy: 'timed_incremental' }
|
project_param = { auto_devops_deploy_strategy: 'timed_incremental' }
|
||||||
|
|
|
@ -51,7 +51,7 @@ const (
|
||||||
gitProjectPattern = `^/.+\.git/`
|
gitProjectPattern = `^/.+\.git/`
|
||||||
geoGitProjectPattern = `^/[^-].+\.git/` // Prevent matching routes like /-/push_from_secondary
|
geoGitProjectPattern = `^/[^-].+\.git/` // Prevent matching routes like /-/push_from_secondary
|
||||||
projectPattern = `^/([^/]+/){1,}[^/]+/`
|
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`
|
apiTopicPattern = apiPattern + `v4/topics`
|
||||||
snippetUploadPattern = `^/uploads/personal_snippet`
|
snippetUploadPattern = `^/uploads/personal_snippet`
|
||||||
userUploadPattern = `^/uploads/user`
|
userUploadPattern = `^/uploads/user`
|
||||||
|
@ -269,40 +269,40 @@ func configureRoutes(u *upstream) {
|
||||||
// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56731.
|
// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56731.
|
||||||
|
|
||||||
// Maven Artifact Repository
|
// Maven Artifact Repository
|
||||||
u.route("PUT", apiProjectPattern+`packages/maven/`, requestBodyUploader),
|
u.route("PUT", apiProjectPattern+`/packages/maven/`, requestBodyUploader),
|
||||||
|
|
||||||
// Conan Artifact Repository
|
// Conan Artifact Repository
|
||||||
u.route("PUT", apiPattern+`v4/packages/conan/`, requestBodyUploader),
|
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
|
// Generic Packages Repository
|
||||||
u.route("PUT", apiProjectPattern+`packages/generic/`, requestBodyUploader),
|
u.route("PUT", apiProjectPattern+`/packages/generic/`, requestBodyUploader),
|
||||||
|
|
||||||
// NuGet Artifact Repository
|
// NuGet Artifact Repository
|
||||||
u.route("PUT", apiProjectPattern+`packages/nuget/`, mimeMultipartUploader),
|
u.route("PUT", apiProjectPattern+`/packages/nuget/`, mimeMultipartUploader),
|
||||||
|
|
||||||
// PyPI Artifact Repository
|
// PyPI Artifact Repository
|
||||||
u.route("POST", apiProjectPattern+`packages/pypi`, mimeMultipartUploader),
|
u.route("POST", apiProjectPattern+`/packages/pypi`, mimeMultipartUploader),
|
||||||
|
|
||||||
// Debian Artifact Repository
|
// Debian Artifact Repository
|
||||||
u.route("PUT", apiProjectPattern+`packages/debian/`, requestBodyUploader),
|
u.route("PUT", apiProjectPattern+`/packages/debian/`, requestBodyUploader),
|
||||||
|
|
||||||
// RPM Artifact Repository
|
// RPM Artifact Repository
|
||||||
u.route("POST", apiProjectPattern+`packages/rpm/`, requestBodyUploader),
|
u.route("POST", apiProjectPattern+`packages/rpm/`, requestBodyUploader),
|
||||||
|
|
||||||
// Gem Artifact Repository
|
// Gem Artifact Repository
|
||||||
u.route("POST", apiProjectPattern+`packages/rubygems/`, requestBodyUploader),
|
u.route("POST", apiProjectPattern+`/packages/rubygems/`, requestBodyUploader),
|
||||||
|
|
||||||
// Terraform Module Package Repository
|
// Terraform Module Package Repository
|
||||||
u.route("PUT", apiProjectPattern+`packages/terraform/modules/`, requestBodyUploader),
|
u.route("PUT", apiProjectPattern+`/packages/terraform/modules/`, requestBodyUploader),
|
||||||
|
|
||||||
// Helm Artifact Repository
|
// 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 are porting API to disk acceleration
|
||||||
// we need to declare each routes until we have fixed all the routes on the rails codebase.
|
// 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
|
// 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", apiPattern+`graphql\z`, tempfileMultipartProxy),
|
||||||
u.route("POST", apiTopicPattern, tempfileMultipartProxy),
|
u.route("POST", apiTopicPattern, tempfileMultipartProxy),
|
||||||
u.route("PUT", apiTopicPattern, tempfileMultipartProxy),
|
u.route("PUT", apiTopicPattern, tempfileMultipartProxy),
|
||||||
|
@ -315,16 +315,20 @@ func configureRoutes(u *upstream) {
|
||||||
u.route("POST", importPattern+`gitlab_group`, mimeMultipartUploader),
|
u.route("POST", importPattern+`gitlab_group`, mimeMultipartUploader),
|
||||||
|
|
||||||
// Issuable Metric image upload
|
// 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
|
// 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
|
// Requirements Import via UI upload acceleration
|
||||||
u.route("POST", projectPattern+`requirements_management/requirements/import_csv`, mimeMultipartUploader),
|
u.route("POST", projectPattern+`requirements_management/requirements/import_csv`, mimeMultipartUploader),
|
||||||
|
|
||||||
// Uploads via API
|
// 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
|
// Explicitly proxy API requests
|
||||||
u.route("", apiPattern, proxy),
|
u.route("", apiPattern, proxy),
|
||||||
|
|
|
@ -122,6 +122,10 @@ func TestAcceleratedUpload(t *testing.T) {
|
||||||
{"POST", `/example`, false},
|
{"POST", `/example`, false},
|
||||||
{"POST", `/uploads/personal_snippet`, true},
|
{"POST", `/uploads/personal_snippet`, true},
|
||||||
{"POST", `/uploads/user`, 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/1/uploads`, true},
|
||||||
{"POST", `/api/v4/projects/group%2Fproject/uploads`, true},
|
{"POST", `/api/v4/projects/group%2Fproject/uploads`, true},
|
||||||
{"POST", `/api/v4/projects/group%2Fsubgroup%2Fproject/uploads`, true},
|
{"POST", `/api/v4/projects/group%2Fsubgroup%2Fproject/uploads`, true},
|
||||||
|
|
Loading…
Reference in New Issue