Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
4b198b6289
commit
30b1000678
|
|
@ -153,21 +153,38 @@ export default {
|
|||
@mousedown="handleParallelLineMouseDown"
|
||||
>
|
||||
<template v-for="(line, index) in diffLines">
|
||||
<div
|
||||
v-if="line.isMatchLineLeft || line.isMatchLineRight"
|
||||
:key="`expand-${index}`"
|
||||
class="diff-tr line_expansion match"
|
||||
>
|
||||
<div class="diff-td text-center gl-font-regular">
|
||||
<diff-expansion-cell
|
||||
:file-hash="diffFile.file_hash"
|
||||
:context-lines-path="diffFile.context_lines_path"
|
||||
:line="line.left"
|
||||
:is-top="index === 0"
|
||||
:is-bottom="index + 1 === diffLinesLength"
|
||||
/>
|
||||
<template v-if="line.isMatchLineLeft || line.isMatchLineRight">
|
||||
<div :key="`expand-${index}`" class="diff-tr line_expansion match">
|
||||
<div class="diff-td text-center gl-font-regular">
|
||||
<diff-expansion-cell
|
||||
:file-hash="diffFile.file_hash"
|
||||
:context-lines-path="diffFile.context_lines_path"
|
||||
:line="line.left"
|
||||
:is-top="index === 0"
|
||||
:is-bottom="index + 1 === diffLinesLength"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="line.left.rich_text"
|
||||
:key="`expand-definition-${index}`"
|
||||
class="diff-grid-row diff-tr line_holder match"
|
||||
>
|
||||
<div class="diff-grid-left diff-grid-3-col left-side">
|
||||
<div class="diff-td diff-line-num"></div>
|
||||
<div v-if="inline" class="diff-td diff-line-num"></div>
|
||||
<div class="diff-td line_content left-side gl-white-space-normal!">
|
||||
{{ line.left.rich_text }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!inline" class="diff-grid-right diff-grid-3-col right-side">
|
||||
<div class="diff-td diff-line-num"></div>
|
||||
<div class="diff-td line_content right-side gl-white-space-normal!">
|
||||
{{ line.left.rich_text }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<diff-row
|
||||
v-if="!line.isMatchLineLeft && !line.isMatchLineRight"
|
||||
:key="line.line_code"
|
||||
|
|
|
|||
|
|
@ -30,7 +30,8 @@ export default {
|
|||
page: 1,
|
||||
totalItems: 0,
|
||||
errorMessage: null,
|
||||
searchTerm: '',
|
||||
userSearchTerm: '',
|
||||
searchValue: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -45,16 +46,11 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
loadGroups() {
|
||||
// fetchGroups returns no results for search terms 0 < {length} < 3.
|
||||
// The desired UX is to return the unfiltered results for searches {length} < 3.
|
||||
// Here, we set the search to an empty string if {length} < 3
|
||||
const search = this.searchTerm?.length < MINIMUM_SEARCH_TERM_LENGTH ? '' : this.searchTerm;
|
||||
|
||||
this.isLoadingMore = true;
|
||||
return fetchGroups(this.groupsPath, {
|
||||
page: this.page,
|
||||
perPage: this.$options.DEFAULT_GROUPS_PER_PAGE,
|
||||
search,
|
||||
search: this.searchValue,
|
||||
})
|
||||
.then((response) => {
|
||||
const { page, total } = parseIntPagination(normalizeHeaders(response.headers));
|
||||
|
|
@ -69,12 +65,24 @@ export default {
|
|||
this.isLoadingMore = false;
|
||||
});
|
||||
},
|
||||
onGroupSearch(searchTerm) {
|
||||
// keep a copy of the search term for pagination
|
||||
this.searchTerm = searchTerm;
|
||||
// reset the current page
|
||||
onGroupSearch(userSearchTerm = '') {
|
||||
this.userSearchTerm = userSearchTerm;
|
||||
|
||||
// fetchGroups returns no results for search terms 0 < {length} < 3.
|
||||
// The desired UX is to return the unfiltered results for searches {length} < 3.
|
||||
// Here, we set the search to an empty string '' if {length} < 3
|
||||
const newSearchValue =
|
||||
this.userSearchTerm.length < MINIMUM_SEARCH_TERM_LENGTH ? '' : this.userSearchTerm;
|
||||
|
||||
// don't fetch new results if the search value didn't change.
|
||||
if (newSearchValue === this.searchValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
// reset the page.
|
||||
this.page = 1;
|
||||
return this.loadGroups();
|
||||
this.searchValue = newSearchValue;
|
||||
this.loadGroups();
|
||||
},
|
||||
},
|
||||
DEFAULT_GROUPS_PER_PAGE,
|
||||
|
|
@ -92,7 +100,7 @@ export default {
|
|||
debounce="500"
|
||||
:placeholder="__('Search by name')"
|
||||
:is-loading="isLoadingMore"
|
||||
:value="searchTerm"
|
||||
:value="userSearchTerm"
|
||||
@input="onGroupSearch"
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -603,6 +603,14 @@ table.code {
|
|||
grid-template-columns: 50px 8px 0 1fr;
|
||||
}
|
||||
|
||||
.diff-grid-3-col {
|
||||
grid-template-columns: 50px 1fr !important;
|
||||
}
|
||||
|
||||
&.inline-diff-view .diff-grid-3-col {
|
||||
grid-template-columns: 50px 50px 1fr !important;
|
||||
}
|
||||
|
||||
.diff-grid-comments {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
|
|
|||
|
|
@ -168,8 +168,8 @@ $dark-il: #de935f;
|
|||
}
|
||||
}
|
||||
|
||||
.diff-grid-left:hover,
|
||||
.diff-grid-right:hover,
|
||||
&:not(.match) .diff-grid-left:hover,
|
||||
&:not(.match) .diff-grid-right:hover,
|
||||
&.code-search-line:hover {
|
||||
.diff-line-num:not(.empty-cell) {
|
||||
@include line-number-hover;
|
||||
|
|
|
|||
|
|
@ -169,8 +169,8 @@ $monokai-gh: #75715e;
|
|||
}
|
||||
}
|
||||
|
||||
.diff-grid-left:hover,
|
||||
.diff-grid-right:hover,
|
||||
&:not(.match) .diff-grid-left:hover,
|
||||
&:not(.match) .diff-grid-right:hover,
|
||||
&.code-search-line:hover {
|
||||
.diff-line-num:not(.empty-cell) {
|
||||
@include line-number-hover;
|
||||
|
|
|
|||
|
|
@ -66,9 +66,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
.diff-grid-left:hover,
|
||||
.diff-grid-right:hover,
|
||||
&.code-search-line:hover {
|
||||
&:not(.match) .diff-grid-left:hover,
|
||||
&:not(.match) .diff-grid-right:hover,
|
||||
&:not(.match) &.code-search-line:hover {
|
||||
.diff-line-num:not(.empty-cell) {
|
||||
@include line-number-hover;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -148,8 +148,8 @@ $solarized-dark-il: #2aa198;
|
|||
@include line-coverage-border-color($solarized-dark-coverage, $solarized-dark-no-coverage);
|
||||
}
|
||||
|
||||
.diff-grid-left:hover,
|
||||
.diff-grid-right:hover,
|
||||
&:not(.match) .diff-grid-left:hover,
|
||||
&:not(.match) .diff-grid-right:hover,
|
||||
&.code-search-line:hover {
|
||||
.diff-line-num:not(.empty-cell) {
|
||||
@include line-number-hover;
|
||||
|
|
|
|||
|
|
@ -168,8 +168,8 @@ $solarized-light-il: #2aa198;
|
|||
}
|
||||
}
|
||||
|
||||
.diff-grid-left:hover,
|
||||
.diff-grid-right:hover,
|
||||
&:not(.match) .diff-grid-left:hover,
|
||||
&:not(.match) .diff-grid-right:hover,
|
||||
&.code-search-line:hover {
|
||||
.diff-line-num:not(.empty-cell) {
|
||||
@include line-number-hover;
|
||||
|
|
|
|||
|
|
@ -118,6 +118,15 @@ pre.code,
|
|||
|
||||
.line_expansion {
|
||||
@include diff-expansion($gray-light, $border-color, $blue-600);
|
||||
|
||||
&.diff-tr:last-child {
|
||||
border-bottom-right-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
|
||||
.diff-td {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Diff line
|
||||
|
|
@ -128,8 +137,8 @@ pre.code,
|
|||
@include match-line;
|
||||
}
|
||||
|
||||
.diff-grid-left:hover,
|
||||
.diff-grid-right:hover,
|
||||
&:not(.match) .diff-grid-left:hover,
|
||||
&:not(.match) .diff-grid-right:hover,
|
||||
&.code-search-line:hover {
|
||||
.diff-line-num:not(.empty-cell):not(.conflict_marker_their):not(.conflict_marker_our) {
|
||||
@include line-number-hover;
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ module Mutations
|
|||
project = issue.project
|
||||
|
||||
authorize_escalation_status!(project)
|
||||
check_feature_availability!(project, issue)
|
||||
check_feature_availability!(issue)
|
||||
|
||||
::Issues::UpdateService.new(
|
||||
project: project,
|
||||
|
|
@ -36,8 +36,8 @@ module Mutations
|
|||
raise_resource_not_available_error!
|
||||
end
|
||||
|
||||
def check_feature_availability!(project, issue)
|
||||
return if Feature.enabled?(:incident_escalations, project) && issue.supports_escalation?
|
||||
def check_feature_availability!(issue)
|
||||
return if issue.supports_escalation?
|
||||
|
||||
raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature unavailable for provided issue'
|
||||
end
|
||||
|
|
|
|||
|
|
@ -173,9 +173,7 @@ module Types
|
|||
end
|
||||
|
||||
def escalation_status
|
||||
return unless Feature.enabled?(:incident_escalations, object.project) && object.supports_escalation?
|
||||
|
||||
object.escalation_status&.status_name
|
||||
object.supports_escalation? ? object.escalation_status&.status_name : nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -194,6 +194,8 @@ module Issuable
|
|||
end
|
||||
|
||||
def supports_escalation?
|
||||
return false unless ::Feature.enabled?(:incident_escalations, project)
|
||||
|
||||
incident?
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -125,17 +125,6 @@ module Timebox
|
|||
fuzzy_search(query, [:title, :description])
|
||||
end
|
||||
|
||||
# Searches for timeboxes with a matching title.
|
||||
#
|
||||
# This method uses ILIKE on PostgreSQL
|
||||
#
|
||||
# query - The search query as a String
|
||||
#
|
||||
# Returns an ActiveRecord::Relation.
|
||||
def search_title(query)
|
||||
fuzzy_search(query, [:title])
|
||||
end
|
||||
|
||||
def filter_by_state(timeboxes, state)
|
||||
case state
|
||||
when 'closed' then timeboxes.closed
|
||||
|
|
|
|||
|
|
@ -52,6 +52,17 @@ class Milestone < ApplicationRecord
|
|||
state :active
|
||||
end
|
||||
|
||||
# Searches for timeboxes with a matching title.
|
||||
#
|
||||
# This method uses ILIKE on PostgreSQL
|
||||
#
|
||||
# query - The search query as a String
|
||||
#
|
||||
# Returns an ActiveRecord::Relation.
|
||||
def self.search_title(query)
|
||||
fuzzy_search(query, [:title])
|
||||
end
|
||||
|
||||
def self.min_chars_for_partial_matching
|
||||
2
|
||||
end
|
||||
|
|
|
|||
|
|
@ -110,6 +110,7 @@ class IssuableSidebarBasicEntity < Grape::Entity
|
|||
expose :supports_time_tracking?, as: :supports_time_tracking
|
||||
expose :supports_milestone?, as: :supports_milestone
|
||||
expose :supports_severity?, as: :supports_severity
|
||||
expose :supports_escalation?, as: :supports_escalation
|
||||
|
||||
private
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,12 @@ class IssueSidebarBasicEntity < IssuableSidebarBasicEntity
|
|||
expose :due_date
|
||||
expose :confidential
|
||||
expose :severity
|
||||
|
||||
expose :current_user, merge: true do
|
||||
expose :can_update_escalation_status, if: -> (issue, _) { issue.supports_escalation? } do |issue|
|
||||
can?(current_user, :update_escalation_status, issue.project)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
IssueSidebarBasicEntity.prepend_mod_with('IssueSidebarBasicEntity')
|
||||
|
|
|
|||
|
|
@ -155,8 +155,7 @@ module AlertManagement
|
|||
end
|
||||
|
||||
def should_sync_to_incident?
|
||||
Feature.enabled?(:incident_escalations, project) &&
|
||||
alert.issue &&
|
||||
alert.issue &&
|
||||
alert.issue.supports_escalation? &&
|
||||
alert.issue.escalation_status &&
|
||||
alert.issue.escalation_status.status != alert.status
|
||||
|
|
|
|||
|
|
@ -33,9 +33,8 @@ module IncidentManagement
|
|||
attr_reader :issuable, :current_user, :params, :project
|
||||
|
||||
def available?
|
||||
Feature.enabled?(:incident_escalations, project) &&
|
||||
issuable.supports_escalation? &&
|
||||
user_has_permissions? &&
|
||||
issuable.supports_escalation? &&
|
||||
escalation_status.present?
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -39,15 +39,15 @@
|
|||
= _('Namespace:')
|
||||
%strong
|
||||
- if @project.namespace
|
||||
= link_to @project.namespace.human_name, [:admin, @project.group || @project.owner]
|
||||
= link_to @project.namespace.human_name, [:admin, @project.personal? ? @project.namespace.owner : @project.group]
|
||||
- else
|
||||
= s_('ProjectSettings|Global')
|
||||
%li
|
||||
%span.light
|
||||
= _('Owned by:')
|
||||
%strong
|
||||
- if @project.owner
|
||||
= link_to @project.owner_name, [:admin, @project.owner]
|
||||
- if @project.owners.any?
|
||||
= safe_join(@project.owners.map { |owner| link_to(owner.name, [:admin, owner]) }, ", ".html_safe)
|
||||
- else
|
||||
= _('(deleted)')
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,11 @@
|
|||
.block.reviewer.qa-reviewer-block
|
||||
= render "shared/issuable/sidebar_reviewers", issuable_sidebar: issuable_sidebar, reviewers: reviewers, signed_in: signed_in
|
||||
|
||||
- if issuable_sidebar[:supports_escalation]
|
||||
.block.escalation-status{ data: { testid: 'escalation_status_container' } }
|
||||
#js-escalation-status{ data: { can_edit: issuable_sidebar.dig(:current_user, :can_update_escalation_status).to_s, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid] } }
|
||||
= render_if_exists 'shared/issuable/sidebar_escalation_policy', issuable_sidebar: issuable_sidebar
|
||||
|
||||
- if @project.group.present?
|
||||
= render_if_exists 'shared/issuable/sidebar_item_epic', issuable_sidebar: issuable_sidebar, group_path: @project.group.full_path, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid], issuable_type: issuable_type
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: ci_secure_files
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/78227
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/350748
|
||||
milestone: '14.8'
|
||||
type: development
|
||||
group: group::incubation
|
||||
default_enabled: false
|
||||
|
|
@ -404,6 +404,23 @@ production: &base
|
|||
# aws_signature_version: 4 # For creation of signed URLs. Set to 2 if provider does not support v4.
|
||||
# path_style: true # Use 'host/bucket_name/object' instead of 'bucket_name.host/object'
|
||||
|
||||
## CI Secure Files
|
||||
ci_secure_files:
|
||||
enabled: true
|
||||
# storage_path: shared/ci_secure_files
|
||||
object_store:
|
||||
enabled: false
|
||||
remote_directory: ci_secure_files # The bucket name
|
||||
connection:
|
||||
provider: AWS
|
||||
aws_access_key_id: AWS_ACCESS_KEY_ID
|
||||
aws_secret_access_key: AWS_SECRET_ACCESS_KEY
|
||||
region: us-east-1
|
||||
# host: 'localhost' # default: s3.amazonaws.com
|
||||
# endpoint: 'http://127.0.0.1:9000' # default: nil
|
||||
# aws_signature_version: 4 # For creation of signed URLs. Set to 2 if provider does not support v4.
|
||||
# path_style: true # Use 'host/bucket_name/object' instead of 'bucket_name.host/object'
|
||||
|
||||
## GitLab Pages
|
||||
pages:
|
||||
enabled: false
|
||||
|
|
|
|||
|
|
@ -5,6 +5,6 @@
|
|||
issue_url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/300862'
|
||||
breaking_change: true
|
||||
body: |
|
||||
In GitLab 14.0, we will update the [Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/stages.html#auto-deploy) CI template to the latest version. This includes new features, bug fixes, and performance improvements with a dependency on the v2 [auto-deploy-image](https://gitlab.com/gitlab-org/cluster-integration/auto-deploy-image). Auto Deploy CI tempalte v1 will is deprecated going forward.
|
||||
In GitLab 14.0, we will update the [Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/stages.html#auto-deploy) CI template to the latest version. This includes new features, bug fixes, and performance improvements with a dependency on the v2 [auto-deploy-image](https://gitlab.com/gitlab-org/cluster-integration/auto-deploy-image). Auto Deploy CI template v1 will is deprecated going forward.
|
||||
|
||||
Since the v1 and v2 versions are not backward-compatible, your project might encounter an unexpected failure if you already have a deployed application. Follow the [upgrade guide](https://docs.gitlab.com/ee/topics/autodevops/upgrading_auto_deploy_dependencies.html#upgrade-guide) to upgrade your environments. You can also start using the latest template today by following the [early adoption guide](https://docs.gitlab.com/ee/topics/autodevops/upgrading_auto_deploy_dependencies.html#early-adopters).
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
self-managed: true
|
||||
gitlab-com: true
|
||||
packages: [Free, Premium, Ultimate]
|
||||
url: 'https://docs.gitlab.com/ee/user/profile/#user-profile-readme'
|
||||
url: 'https://docs.gitlab.com/ee/user/profile/#add-details-to-your-profile-with-a-readme'
|
||||
image_url: https://about.gitlab.com/images/14_5/user_profiles_readme.png
|
||||
published_at: 2021-11-22
|
||||
release: 14.5
|
||||
|
|
|
|||
|
|
@ -11064,12 +11064,15 @@ four standard [pagination arguments](#connection-pagination-arguments):
|
|||
| <a id="groupiterationsenddate"></a>`endDate` **{warning-solid}** | [`Time`](#time) | **Deprecated** in 13.5. Use timeframe.end. |
|
||||
| <a id="groupiterationsid"></a>`id` | [`ID`](#id) | Global ID of the Iteration to look up. |
|
||||
| <a id="groupiterationsiid"></a>`iid` | [`ID`](#id) | Internal ID of the Iteration to look up. |
|
||||
| <a id="groupiterationsin"></a>`in` | [`[IterationSearchableField!]`](#iterationsearchablefield) | Fields in which the fuzzy-search should be performed with the query given in the argument `search`. Defaults to `[title]`. |
|
||||
| <a id="groupiterationsincludeancestors"></a>`includeAncestors` | [`Boolean`](#boolean) | Whether to include ancestor iterations. Defaults to true. |
|
||||
| <a id="groupiterationsiterationcadenceids"></a>`iterationCadenceIds` | [`[IterationsCadenceID!]`](#iterationscadenceid) | Global iteration cadence IDs by which to look up the iterations. |
|
||||
| <a id="groupiterationssearch"></a>`search` | [`String`](#string) | Query used for fuzzy-searching in the fields selected in the argument `in`. |
|
||||
| <a id="groupiterationssort"></a>`sort` | [`IterationSort`](#iterationsort) | List iterations by sort order. If unspecified, an arbitrary order (subject to change) is used. |
|
||||
| <a id="groupiterationsstartdate"></a>`startDate` **{warning-solid}** | [`Time`](#time) | **Deprecated** in 13.5. Use timeframe.start. |
|
||||
| <a id="groupiterationsstate"></a>`state` | [`IterationState`](#iterationstate) | Filter iterations by state. |
|
||||
| <a id="groupiterationstimeframe"></a>`timeframe` | [`Timeframe`](#timeframe) | List items overlapping the given timeframe. |
|
||||
| <a id="groupiterationstitle"></a>`title` | [`String`](#string) | Fuzzy search by title. |
|
||||
| <a id="groupiterationstitle"></a>`title` **{warning-solid}** | [`String`](#string) | **Deprecated** in 15.4. The argument will be removed in 15.4. Please use `search` and `in` fields instead. |
|
||||
|
||||
##### `Group.label`
|
||||
|
||||
|
|
@ -13762,12 +13765,15 @@ four standard [pagination arguments](#connection-pagination-arguments):
|
|||
| <a id="projectiterationsenddate"></a>`endDate` **{warning-solid}** | [`Time`](#time) | **Deprecated** in 13.5. Use timeframe.end. |
|
||||
| <a id="projectiterationsid"></a>`id` | [`ID`](#id) | Global ID of the Iteration to look up. |
|
||||
| <a id="projectiterationsiid"></a>`iid` | [`ID`](#id) | Internal ID of the Iteration to look up. |
|
||||
| <a id="projectiterationsin"></a>`in` | [`[IterationSearchableField!]`](#iterationsearchablefield) | Fields in which the fuzzy-search should be performed with the query given in the argument `search`. Defaults to `[title]`. |
|
||||
| <a id="projectiterationsincludeancestors"></a>`includeAncestors` | [`Boolean`](#boolean) | Whether to include ancestor iterations. Defaults to true. |
|
||||
| <a id="projectiterationsiterationcadenceids"></a>`iterationCadenceIds` | [`[IterationsCadenceID!]`](#iterationscadenceid) | Global iteration cadence IDs by which to look up the iterations. |
|
||||
| <a id="projectiterationssearch"></a>`search` | [`String`](#string) | Query used for fuzzy-searching in the fields selected in the argument `in`. |
|
||||
| <a id="projectiterationssort"></a>`sort` | [`IterationSort`](#iterationsort) | List iterations by sort order. If unspecified, an arbitrary order (subject to change) is used. |
|
||||
| <a id="projectiterationsstartdate"></a>`startDate` **{warning-solid}** | [`Time`](#time) | **Deprecated** in 13.5. Use timeframe.start. |
|
||||
| <a id="projectiterationsstate"></a>`state` | [`IterationState`](#iterationstate) | Filter iterations by state. |
|
||||
| <a id="projectiterationstimeframe"></a>`timeframe` | [`Timeframe`](#timeframe) | List items overlapping the given timeframe. |
|
||||
| <a id="projectiterationstitle"></a>`title` | [`String`](#string) | Fuzzy search by title. |
|
||||
| <a id="projectiterationstitle"></a>`title` **{warning-solid}** | [`String`](#string) | **Deprecated** in 15.4. The argument will be removed in 15.4. Please use `search` and `in` fields instead. |
|
||||
|
||||
##### `Project.jobs`
|
||||
|
||||
|
|
@ -17000,6 +17006,23 @@ Issue type.
|
|||
| <a id="issuetyperequirement"></a>`REQUIREMENT` | Requirement issue type. |
|
||||
| <a id="issuetypetest_case"></a>`TEST_CASE` | Test Case issue type. |
|
||||
|
||||
### `IterationSearchableField`
|
||||
|
||||
Fields to perform the search in.
|
||||
|
||||
| Value | Description |
|
||||
| ----- | ----------- |
|
||||
| <a id="iterationsearchablefieldcadence_title"></a>`CADENCE_TITLE` | Search in cadence_title field. |
|
||||
| <a id="iterationsearchablefieldtitle"></a>`TITLE` | Search in title field. |
|
||||
|
||||
### `IterationSort`
|
||||
|
||||
Iteration sort values.
|
||||
|
||||
| Value | Description |
|
||||
| ----- | ----------- |
|
||||
| <a id="iterationsortcadence_and_due_date_asc"></a>`CADENCE_AND_DUE_DATE_ASC` | Sort by cadence id and due date in ascending order. |
|
||||
|
||||
### `IterationState`
|
||||
|
||||
State of a GitLab iteration.
|
||||
|
|
|
|||
|
|
@ -408,7 +408,7 @@ for batched background migration:
|
|||
1. Change the index pattern to `pubsub-postgres-inf-gprd*`.
|
||||
1. Add filter for `json.endpoint_id.keyword: Database::BatchedBackgroundMigrationWorker`.
|
||||
1. Optional. To see only updates, add a filter for `json.command_tag.keyword: UPDATE`.
|
||||
1. Optional. To see only failed statements, add a filter for `json.error_severiry.keyword: ERROR`.
|
||||
1. Optional. To see only failed statements, add a filter for `json.error_severity.keyword: ERROR`.
|
||||
1. Optional. Add a filter by table name.
|
||||
|
||||
#### Grafana dashboards
|
||||
|
|
|
|||
|
|
@ -368,7 +368,7 @@ then the query might perform worse than the non-optimized query. The `milestone_
|
|||
"index_issues_on_milestone_id" btree (milestone_id)
|
||||
```
|
||||
|
||||
Adding the `miletone_id = X` filter to the `scope` argument or to the optimized scope causes bad performance.
|
||||
Adding the `milestone_id = X` filter to the `scope` argument or to the optimized scope causes bad performance.
|
||||
|
||||
Example (bad):
|
||||
|
||||
|
|
@ -618,7 +618,7 @@ The following example shows the final `ORDER BY` clause:
|
|||
ORDER BY extract('epoch' FROM epics.closed_at - epics.created_at) DESC, epics.id DESC
|
||||
```
|
||||
|
||||
Snippet for loading records ordered by the calcualted duration:
|
||||
Snippet for loading records ordered by the calculated duration:
|
||||
|
||||
```ruby
|
||||
arel_table = Epic.arel_table
|
||||
|
|
@ -641,7 +641,7 @@ records = Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder.new(
|
|||
array_mapping_scope: -> (id_expression) { Epic.where(Epic.arel_table[:group_id].eq(id_expression)) }
|
||||
).execute.limit(20)
|
||||
|
||||
puts records.pluck(:duration_in_seconds, :id) # other columnns are not available
|
||||
puts records.pluck(:duration_in_seconds, :id) # other columns are not available
|
||||
```
|
||||
|
||||
Building the query requires quite a bit of configuration. For the order configuration you
|
||||
|
|
|
|||
|
|
@ -596,7 +596,7 @@ NOTE:
|
|||
This method of stubbing in Jest specs will not automatically un-stub itself at the end of the test. We merge our stubbed experiment in with all the other global data in `window.gl`. If you need to remove the stubbed experiment(s) after your test or ensure a clean global object before your test, you'll need to manage the global object directly yourself:
|
||||
|
||||
```javascript
|
||||
desribe('tests that care about global state', () => {
|
||||
describe('tests that care about global state', () => {
|
||||
const originalObjects = [];
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
|||
|
|
@ -245,7 +245,7 @@ For now, CI will try to fetch the branch on the [GitLab JH mirror](https://gitla
|
|||
The `rspec:undercoverage` job runs [`undercover`](https://rubygems.org/gems/undercover)
|
||||
to detect, and fail if any changes introduced in the merge request has zero coverage.
|
||||
|
||||
The `rsepc:undercoverage` job obtains coverage data from the `rspec:coverage`
|
||||
The `rspec:undercoverage` job obtains coverage data from the `rspec:coverage`
|
||||
job.
|
||||
|
||||
In the event of an emergency, or false positive from this job, add the
|
||||
|
|
|
|||
|
|
@ -268,7 +268,7 @@ On the EC2 dashboard, look for Load Balancer in the left navigation bar:
|
|||
1. Click **Configure Health Check** and set up a health check for your EC2 instances.
|
||||
1. For **Ping Protocol**, select HTTP.
|
||||
1. For **Ping Port**, enter 80.
|
||||
1. For **Ping Path** - we recommend that you [use the Readiness check endpoint](../../administration/load_balancer.md#readiness-check). You'll need to add [the VPC IP Adddress Range (CIDR)](https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/elb-security-groups.html#elb-vpc-nacl) to the [IP Allowlist](../../administration/monitoring/ip_whitelist.md) for the [Health Check endpoints](../../user/admin_area/monitoring/health_check.md)
|
||||
1. For **Ping Path** - we recommend that you [use the Readiness check endpoint](../../administration/load_balancer.md#readiness-check). You'll need to add [the VPC IP Address Range (CIDR)](https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/elb-security-groups.html#elb-vpc-nacl) to the [IP Allowlist](../../administration/monitoring/ip_whitelist.md) for the [Health Check endpoints](../../user/admin_area/monitoring/health_check.md)
|
||||
1. Keep the default **Advanced Details** or adjust them according to your needs.
|
||||
1. Click **Add EC2 Instances** - don't add anything as we will create an Auto Scaling Group later to manage instances for us.
|
||||
1. Click **Add Tags** and add any tags you need.
|
||||
|
|
@ -588,8 +588,8 @@ Let's create an EC2 instance where we'll install Gitaly:
|
|||
1. In the **Subnet** dropdown, select `gitlab-private-10.0.1.0` from the list of subnets we created earlier.
|
||||
1. Double check that **Auto-assign Public IP** is set to `Use subnet setting (Disable)`.
|
||||
1. Click **Add Storage**.
|
||||
1. Increase the Root volume size to `20 GiB` and change the **Volume Type** to `Provisoned IOPS SSD (io1)`. (This is an arbitrary size. Create a volume big enough for your repository storage requirements.)
|
||||
1. For **IOPS** set `1000` (20 GiB x 50 IOPS). You can provision up to 50 IOPS per GiB. If you select a larger volume, increase the IOPS accordingly. Workloads where many small files are written in a serialized manner, like `git`, requires performant storage, hence the choice of `Provisoned IOPS SSD (io1)`.
|
||||
1. Increase the Root volume size to `20 GiB` and change the **Volume Type** to `Provisioned IOPS SSD (io1)`. (This is an arbitrary size. Create a volume big enough for your repository storage requirements.)
|
||||
1. For **IOPS** set `1000` (20 GiB x 50 IOPS). You can provision up to 50 IOPS per GiB. If you select a larger volume, increase the IOPS accordingly. Workloads where many small files are written in a serialized manner, like `git`, requires performant storage, hence the choice of `Provisioned IOPS SSD (io1)`.
|
||||
1. Click on **Add Tags** and add your tags. In our case, we'll only set `Key: Name` and `Value: Gitaly`.
|
||||
1. Click on **Configure Security Group** and let's **Create a new security group**.
|
||||
1. Give your security group a name and description. We'll use `gitlab-gitaly-sec-group` for both.
|
||||
|
|
|
|||
|
|
@ -63,4 +63,4 @@ Use GitLab [Releases](../user/project/releases/index.md) to plan, build, and del
|
|||
|
||||
### Feature flags
|
||||
|
||||
Use [feature flags](../operations/feature_flags.md) to control and strategically roullout application deployments.
|
||||
Use [feature flags](../operations/feature_flags.md) to control and strategically rollout application deployments.
|
||||
|
|
|
|||
|
|
@ -624,7 +624,7 @@ as a [breaking change](https://docs.gitlab.com/ee/development/contributing/#brea
|
|||
Before updating GitLab, review the details carefully to determine if you need to make any
|
||||
changes to your code, settings, or workflow.
|
||||
|
||||
In GitLab 14.0, we will update the [Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/stages.html#auto-deploy) CI template to the latest version. This includes new features, bug fixes, and performance improvements with a dependency on the v2 [auto-deploy-image](https://gitlab.com/gitlab-org/cluster-integration/auto-deploy-image). Auto Deploy CI tempalte v1 will is deprecated going forward.
|
||||
In GitLab 14.0, we will update the [Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/stages.html#auto-deploy) CI template to the latest version. This includes new features, bug fixes, and performance improvements with a dependency on the v2 [auto-deploy-image](https://gitlab.com/gitlab-org/cluster-integration/auto-deploy-image). Auto Deploy CI template v1 will is deprecated going forward.
|
||||
|
||||
Since the v1 and v2 versions are not backward-compatible, your project might encounter an unexpected failure if you already have a deployed application. Follow the [upgrade guide](https://docs.gitlab.com/ee/topics/autodevops/upgrading_auto_deploy_dependencies.html#upgrade-guide) to upgrade your environments. You can also start using the latest template today by following the [early adoption guide](https://docs.gitlab.com/ee/topics/autodevops/upgrading_auto_deploy_dependencies.html#early-adopters).
|
||||
|
||||
|
|
|
|||
|
|
@ -102,6 +102,8 @@ user profiles are only visible to signed-in users.
|
|||
|
||||
## Add details to your profile with a README
|
||||
|
||||
### *Add personal README to profile*
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/232157) in GitLab 14.5.
|
||||
|
||||
If you want to add more information to your profile page, you can create a README file. When you populate the README file with information, it's included on your profile page.
|
||||
|
|
|
|||
|
|
@ -171,6 +171,7 @@ module API
|
|||
mount ::API::Ci::ResourceGroups
|
||||
mount ::API::Ci::Runner
|
||||
mount ::API::Ci::Runners
|
||||
mount ::API::Ci::SecureFiles
|
||||
mount ::API::Ci::Triggers
|
||||
mount ::API::Ci::Variables
|
||||
mount ::API::Commits
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Ci
|
||||
class SecureFiles < ::API::Base
|
||||
include PaginationParams
|
||||
|
||||
before do
|
||||
authenticate!
|
||||
authorize! :admin_build, user_project
|
||||
feature_flag_enabled?
|
||||
end
|
||||
|
||||
feature_category :pipeline_authoring
|
||||
|
||||
default_format :json
|
||||
|
||||
params do
|
||||
requires :id, type: String, desc: 'The ID of a project'
|
||||
end
|
||||
|
||||
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
|
||||
desc 'List all Secure Files for a Project'
|
||||
params do
|
||||
use :pagination
|
||||
end
|
||||
route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: true
|
||||
get ':id/secure_files' do
|
||||
secure_files = user_project.secure_files
|
||||
present paginate(secure_files), with: Entities::Ci::SecureFile
|
||||
end
|
||||
|
||||
desc 'Get an individual Secure File'
|
||||
params do
|
||||
requires :id, type: Integer, desc: 'The Secure File ID'
|
||||
end
|
||||
|
||||
route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: true
|
||||
get ':id/secure_files/:secure_file_id' do
|
||||
secure_file = user_project.secure_files.find(params[:secure_file_id])
|
||||
present secure_file, with: Entities::Ci::SecureFile
|
||||
end
|
||||
|
||||
desc 'Download a Secure File'
|
||||
route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: true
|
||||
get ':id/secure_files/:secure_file_id/download' do
|
||||
secure_file = user_project.secure_files.find(params[:secure_file_id])
|
||||
|
||||
content_type 'application/octet-stream'
|
||||
env['api.format'] = :binary
|
||||
header['Content-Disposition'] = "attachment; filename=#{secure_file.name}"
|
||||
body secure_file.file.read
|
||||
end
|
||||
|
||||
desc 'Upload a Secure File'
|
||||
params do
|
||||
requires :name, type: String, desc: 'The name of the file'
|
||||
requires :file, types: [Rack::Multipart::UploadedFile, ::API::Validations::Types::WorkhorseFile], desc: 'The secure file to be uploaded'
|
||||
optional :permissions, type: String, desc: 'The file permissions', default: 'read_only', values: %w[read_only read_write execute]
|
||||
end
|
||||
|
||||
route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: true
|
||||
post ':id/secure_files' do
|
||||
secure_file = user_project.secure_files.new(
|
||||
name: params[:name],
|
||||
permissions: params[:permissions] || :read_only
|
||||
)
|
||||
|
||||
secure_file.file = params[:file]
|
||||
|
||||
file_too_large! unless secure_file.file.size < ::Ci::SecureFile::FILE_SIZE_LIMIT.to_i
|
||||
|
||||
if secure_file.save
|
||||
present secure_file, with: Entities::Ci::SecureFile
|
||||
else
|
||||
render_validation_error!(secure_file)
|
||||
end
|
||||
end
|
||||
|
||||
desc 'Delete an individual Secure File'
|
||||
route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: true
|
||||
delete ':id/secure_files/:secure_file_id' do
|
||||
secure_file = user_project.secure_files.find(params[:secure_file_id])
|
||||
|
||||
secure_file.destroy!
|
||||
|
||||
no_content!
|
||||
end
|
||||
end
|
||||
|
||||
helpers do
|
||||
def feature_flag_enabled?
|
||||
service_unavailable! unless Feature.enabled?(:ci_secure_files, user_project, default_enabled: :yaml)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
module Ci
|
||||
class SecureFile < Grape::Entity
|
||||
expose :id
|
||||
expose :name
|
||||
expose :permissions
|
||||
expose :checksum
|
||||
expose :checksum_algorithm
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -6228,6 +6228,15 @@ msgstr ""
|
|||
msgid "By default, all projects and groups will use the global notifications setting."
|
||||
msgstr ""
|
||||
|
||||
msgid "By month"
|
||||
msgstr ""
|
||||
|
||||
msgid "By quarter"
|
||||
msgstr ""
|
||||
|
||||
msgid "By week"
|
||||
msgstr ""
|
||||
|
||||
msgid "ByAuthor|by"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -50,8 +50,11 @@ describe('DiffView', () => {
|
|||
};
|
||||
|
||||
it('renders a match line', () => {
|
||||
const wrapper = createWrapper({ diffLines: [{ isMatchLineLeft: true }] });
|
||||
const wrapper = createWrapper({
|
||||
diffLines: [{ isMatchLineLeft: true, left: { rich_text: 'matched text', lineDraft: {} } }],
|
||||
});
|
||||
expect(wrapper.find(DiffExpansionCell).exists()).toBe(true);
|
||||
expect(wrapper.text()).toContain('matched text');
|
||||
});
|
||||
|
||||
it.each`
|
||||
|
|
|
|||
|
|
@ -101,6 +101,8 @@ describe('GroupsList', () => {
|
|||
});
|
||||
createComponent();
|
||||
|
||||
// wait for the initial loadGroups
|
||||
// to finish.
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
|
|
@ -137,6 +139,8 @@ describe('GroupsList', () => {
|
|||
describe('while groups are loading', () => {
|
||||
beforeEach(async () => {
|
||||
fetchGroups.mockClear();
|
||||
// return a never-ending promise to make test
|
||||
// deterministic.
|
||||
fetchGroups.mockReturnValue(new Promise(() => {}));
|
||||
|
||||
findSearchBox().vm.$emit('input', mockSearchTeam);
|
||||
|
|
@ -173,7 +177,7 @@ describe('GroupsList', () => {
|
|||
describe('when group search finishes loading', () => {
|
||||
beforeEach(async () => {
|
||||
fetchGroups.mockResolvedValue({ data: [mockGroup1] });
|
||||
findSearchBox().vm.$emit('input');
|
||||
findSearchBox().vm.$emit('input', mockSearchTeam);
|
||||
|
||||
await waitForPromises();
|
||||
});
|
||||
|
|
@ -184,32 +188,48 @@ describe('GroupsList', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it.each`
|
||||
userSearchTerm | finalSearchTerm
|
||||
${'gitl'} | ${'gitl'}
|
||||
${'git'} | ${'git'}
|
||||
${'gi'} | ${''}
|
||||
${'g'} | ${''}
|
||||
${''} | ${''}
|
||||
${undefined} | ${undefined}
|
||||
describe.each`
|
||||
previousSearch | newSearch | shouldSearch | expectedSearchValue
|
||||
${''} | ${'git'} | ${true} | ${'git'}
|
||||
${'g'} | ${'git'} | ${true} | ${'git'}
|
||||
${'git'} | ${'gitl'} | ${true} | ${'gitl'}
|
||||
${'git'} | ${'gi'} | ${true} | ${''}
|
||||
${'gi'} | ${'g'} | ${false} | ${undefined}
|
||||
${'g'} | ${''} | ${false} | ${undefined}
|
||||
${''} | ${'g'} | ${false} | ${undefined}
|
||||
`(
|
||||
'searches for "$finalSearchTerm" when user enters "$userSearchTerm"',
|
||||
async ({ userSearchTerm, finalSearchTerm }) => {
|
||||
fetchGroups.mockResolvedValue({
|
||||
data: [mockGroup1],
|
||||
headers: { 'X-PAGE': 1, 'X-TOTAL': 1 },
|
||||
'when previous search was "$previousSearch" and user enters "$newSearch"',
|
||||
({ previousSearch, newSearch, shouldSearch, expectedSearchValue }) => {
|
||||
beforeEach(async () => {
|
||||
fetchGroups.mockResolvedValue({
|
||||
data: [mockGroup1],
|
||||
headers: { 'X-PAGE': 1, 'X-TOTAL': 1 },
|
||||
});
|
||||
|
||||
// wait for initial load
|
||||
createComponent();
|
||||
await waitForPromises();
|
||||
|
||||
// set up the "previous search"
|
||||
findSearchBox().vm.$emit('input', previousSearch);
|
||||
await waitForPromises();
|
||||
|
||||
fetchGroups.mockClear();
|
||||
});
|
||||
|
||||
createComponent();
|
||||
await waitForPromises();
|
||||
it(`${shouldSearch ? 'should' : 'should not'} execute fetch new results`, () => {
|
||||
// enter the new search
|
||||
findSearchBox().vm.$emit('input', newSearch);
|
||||
|
||||
const searchBox = findSearchBox();
|
||||
searchBox.vm.$emit('input', userSearchTerm);
|
||||
|
||||
expect(fetchGroups).toHaveBeenLastCalledWith(mockGroupsPath, {
|
||||
page: 1,
|
||||
perPage: DEFAULT_GROUPS_PER_PAGE,
|
||||
search: finalSearchTerm,
|
||||
if (shouldSearch) {
|
||||
expect(fetchGroups).toHaveBeenCalledWith(mockGroupsPath, {
|
||||
page: 1,
|
||||
perPage: DEFAULT_GROUPS_PER_PAGE,
|
||||
search: expectedSearchValue,
|
||||
});
|
||||
} else {
|
||||
expect(fetchGroups).not.toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
|
@ -227,7 +247,13 @@ describe('GroupsList', () => {
|
|||
await waitForPromises();
|
||||
|
||||
const paginationEl = findPagination();
|
||||
paginationEl.vm.$emit('input', 2);
|
||||
|
||||
// mock the response from page 2
|
||||
fetchGroups.mockResolvedValue({
|
||||
headers: { 'X-TOTAL': totalItems, 'X-PAGE': 2 },
|
||||
data: mockGroups,
|
||||
});
|
||||
await paginationEl.vm.$emit('input', 2);
|
||||
});
|
||||
|
||||
it('should load results for page 2', () => {
|
||||
|
|
@ -238,18 +264,23 @@ describe('GroupsList', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('resets page to 1 on search `input` event', () => {
|
||||
const mockSearchTerm = 'gitlab';
|
||||
const searchBox = findSearchBox();
|
||||
it.each`
|
||||
scenario | searchTerm | expectedPage | expectedSearchTerm
|
||||
${'preserves current page'} | ${'gi'} | ${2} | ${''}
|
||||
${'resets page to 1'} | ${'gitlab'} | ${1} | ${'gitlab'}
|
||||
`(
|
||||
'$scenario when search term is $searchTerm',
|
||||
({ searchTerm, expectedPage, expectedSearchTerm }) => {
|
||||
const searchBox = findSearchBox();
|
||||
searchBox.vm.$emit('input', searchTerm);
|
||||
|
||||
searchBox.vm.$emit('input', mockSearchTerm);
|
||||
|
||||
expect(fetchGroups).toHaveBeenLastCalledWith(mockGroupsPath, {
|
||||
page: 1,
|
||||
perPage: DEFAULT_GROUPS_PER_PAGE,
|
||||
search: mockSearchTerm,
|
||||
});
|
||||
});
|
||||
expect(fetchGroups).toHaveBeenLastCalledWith(mockGroupsPath, {
|
||||
page: expectedPage,
|
||||
perPage: DEFAULT_GROUPS_PER_PAGE,
|
||||
search: expectedSearchTerm,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -935,6 +935,14 @@ RSpec.describe Issuable do
|
|||
subject { issuable.supports_escalation? }
|
||||
|
||||
it { is_expected.to eq(supports_escalation) }
|
||||
|
||||
context 'with feature disabled' do
|
||||
before do
|
||||
stub_feature_flags(incident_escalations: false)
|
||||
end
|
||||
|
||||
it { is_expected.to eq(false) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,314 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe API::Ci::SecureFiles do
|
||||
before do
|
||||
stub_ci_secure_file_object_storage
|
||||
stub_feature_flags(ci_secure_files: true)
|
||||
end
|
||||
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:user2) { create(:user) }
|
||||
let_it_be(:project) { create(:project, creator_id: user.id) }
|
||||
let_it_be(:maintainer) { create(:project_member, :maintainer, user: user, project: project) }
|
||||
let_it_be(:developer) { create(:project_member, :developer, user: user2, project: project) }
|
||||
let_it_be(:secure_file) { create(:ci_secure_file, project: project) }
|
||||
|
||||
describe 'GET /projects/:id/secure_files' do
|
||||
context 'feature flag' do
|
||||
it 'returns a 503 when the feature flag is disabled' do
|
||||
stub_feature_flags(ci_secure_files: false)
|
||||
|
||||
get api("/projects/#{project.id}/secure_files", user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:service_unavailable)
|
||||
end
|
||||
|
||||
it 'returns a 200 when the feature flag is enabled' do
|
||||
get api("/projects/#{project.id}/secure_files", user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response).to be_a(Array)
|
||||
end
|
||||
end
|
||||
|
||||
context 'authorized user with proper permissions' do
|
||||
it 'returns project secure files' do
|
||||
get api("/projects/#{project.id}/secure_files", user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response).to be_a(Array)
|
||||
end
|
||||
end
|
||||
|
||||
context 'authorized user with invalid permissions' do
|
||||
it 'does not return project secure files' do
|
||||
get api("/projects/#{project.id}/secure_files", user2)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:forbidden)
|
||||
end
|
||||
end
|
||||
|
||||
context 'unauthorized user' do
|
||||
it 'does not return project secure files' do
|
||||
get api("/projects/#{project.id}/secure_files")
|
||||
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /projects/:id/secure_files/:secure_file_id' do
|
||||
context 'authorized user with proper permissions' do
|
||||
it 'returns project secure file details' do
|
||||
get api("/projects/#{project.id}/secure_files/#{secure_file.id}", user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response['name']).to eq(secure_file.name)
|
||||
expect(json_response['permissions']).to eq(secure_file.permissions)
|
||||
end
|
||||
|
||||
it 'responds with 404 Not Found if requesting non-existing secure file' do
|
||||
get api("/projects/#{project.id}/secure_files/99999", user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
context 'authorized user with invalid permissions' do
|
||||
it 'does not return project secure file details' do
|
||||
get api("/projects/#{project.id}/secure_files/#{secure_file.id}", user2)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:forbidden)
|
||||
end
|
||||
end
|
||||
|
||||
context 'unauthorized user' do
|
||||
it 'does not return project secure file details' do
|
||||
get api("/projects/#{project.id}/secure_files/#{secure_file.id}")
|
||||
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /projects/:id/secure_files/:secure_file_id/download' do
|
||||
context 'authorized user with proper permissions' do
|
||||
it 'returns a secure file' do
|
||||
sample_file = fixture_file('ci_secure_files/upload-keystore.jks')
|
||||
secure_file.file = CarrierWaveStringFile.new(sample_file)
|
||||
secure_file.save!
|
||||
|
||||
get api("/projects/#{project.id}/secure_files/#{secure_file.id}/download", user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(Base64.encode64(response.body)).to eq(Base64.encode64(sample_file))
|
||||
end
|
||||
|
||||
it 'responds with 404 Not Found if requesting non-existing secure file' do
|
||||
get api("/projects/#{project.id}/secure_files/99999/download", user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
context 'authorized user with invalid permissions' do
|
||||
it 'does not return project secure file details' do
|
||||
get api("/projects/#{project.id}/secure_files/#{secure_file.id}/download", user2)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:forbidden)
|
||||
end
|
||||
end
|
||||
|
||||
context 'unauthorized user' do
|
||||
it 'does not return project secure file details' do
|
||||
get api("/projects/#{project.id}/secure_files/#{secure_file.id}/download")
|
||||
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /projects/:id/secure_files' do
|
||||
context 'authorized user with proper permissions' do
|
||||
it 'creates a secure file' do
|
||||
params = {
|
||||
file: fixture_file_upload('spec/fixtures/ci_secure_files/upload-keystore.jks'),
|
||||
name: 'upload-keystore.jks',
|
||||
permissions: 'execute'
|
||||
}
|
||||
|
||||
expect do
|
||||
post api("/projects/#{project.id}/secure_files", user), params: params
|
||||
end.to change {project.secure_files.count}.by(1)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:created)
|
||||
expect(json_response['name']).to eq('upload-keystore.jks')
|
||||
expect(json_response['permissions']).to eq('execute')
|
||||
expect(json_response['checksum']).to eq(secure_file.checksum)
|
||||
expect(json_response['checksum_algorithm']).to eq('sha256')
|
||||
|
||||
secure_file = Ci::SecureFile.find(json_response['id'])
|
||||
expect(secure_file.checksum).to eq(
|
||||
Digest::SHA256.hexdigest(fixture_file('ci_secure_files/upload-keystore.jks'))
|
||||
)
|
||||
expect(json_response['id']).to eq(secure_file.id)
|
||||
end
|
||||
|
||||
it 'creates a secure file with read_only permissions by default' do
|
||||
params = {
|
||||
file: fixture_file_upload('spec/fixtures/ci_secure_files/upload-keystore.jks'),
|
||||
name: 'upload-keystore.jks'
|
||||
}
|
||||
|
||||
expect do
|
||||
post api("/projects/#{project.id}/secure_files", user), params: params
|
||||
end.to change {project.secure_files.count}.by(1)
|
||||
|
||||
expect(json_response['permissions']).to eq('read_only')
|
||||
end
|
||||
|
||||
it 'uploads and downloads a secure file' do
|
||||
post_params = {
|
||||
file: fixture_file_upload('spec/fixtures/ci_secure_files/upload-keystore.jks'),
|
||||
name: 'upload-keystore.jks',
|
||||
permissions: 'read_write'
|
||||
}
|
||||
|
||||
post api("/projects/#{project.id}/secure_files", user), params: post_params
|
||||
|
||||
secure_file_id = json_response['id']
|
||||
|
||||
get api("/projects/#{project.id}/secure_files/#{secure_file_id}/download", user)
|
||||
|
||||
expect(Base64.encode64(response.body)).to eq(Base64.encode64(fixture_file_upload('spec/fixtures/ci_secure_files/upload-keystore.jks').read))
|
||||
end
|
||||
|
||||
it 'returns an error when the file checksum fails to validate' do
|
||||
secure_file.update!(checksum: 'foo')
|
||||
|
||||
get api("/projects/#{project.id}/secure_files/#{secure_file.id}/download", user)
|
||||
|
||||
expect(response.code).to eq("500")
|
||||
end
|
||||
|
||||
it 'returns an error when no file is uploaded' do
|
||||
post_params = {
|
||||
name: 'upload-keystore.jks'
|
||||
}
|
||||
|
||||
post api("/projects/#{project.id}/secure_files", user), params: post_params
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
expect(json_response['error']).to eq('file is missing')
|
||||
end
|
||||
|
||||
it 'returns an error when the file name is missing' do
|
||||
post_params = {
|
||||
file: fixture_file_upload('spec/fixtures/ci_secure_files/upload-keystore.jks')
|
||||
}
|
||||
|
||||
post api("/projects/#{project.id}/secure_files", user), params: post_params
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
expect(json_response['error']).to eq('name is missing')
|
||||
end
|
||||
|
||||
it 'returns an error when an unexpected permission is supplied' do
|
||||
post_params = {
|
||||
file: fixture_file_upload('spec/fixtures/ci_secure_files/upload-keystore.jks'),
|
||||
name: 'upload-keystore.jks',
|
||||
permissions: 'foo'
|
||||
}
|
||||
|
||||
post api("/projects/#{project.id}/secure_files", user), params: post_params
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
expect(json_response['error']).to eq('permissions does not have a valid value')
|
||||
end
|
||||
|
||||
it 'returns an error when an unexpected validation failure happens' do
|
||||
allow_next_instance_of(Ci::SecureFile) do |instance|
|
||||
allow(instance).to receive(:valid?).and_return(false)
|
||||
allow(instance).to receive_message_chain(:errors, :any?).and_return(true)
|
||||
allow(instance).to receive_message_chain(:errors, :messages).and_return(['Error 1', 'Error 2'])
|
||||
end
|
||||
|
||||
post_params = {
|
||||
file: fixture_file_upload('spec/fixtures/ci_secure_files/upload-keystore.jks'),
|
||||
name: 'upload-keystore.jks'
|
||||
}
|
||||
|
||||
post api("/projects/#{project.id}/secure_files", user), params: post_params
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
end
|
||||
|
||||
it 'returns a 413 error when the file size is too large' do
|
||||
allow_next_instance_of(Ci::SecureFile) do |instance|
|
||||
allow(instance).to receive_message_chain(:file, :size).and_return(6.megabytes.to_i)
|
||||
end
|
||||
|
||||
post_params = {
|
||||
file: fixture_file_upload('spec/fixtures/ci_secure_files/upload-keystore.jks'),
|
||||
name: 'upload-keystore.jks'
|
||||
}
|
||||
|
||||
post api("/projects/#{project.id}/secure_files", user), params: post_params
|
||||
|
||||
expect(response).to have_gitlab_http_status(:payload_too_large)
|
||||
end
|
||||
end
|
||||
|
||||
context 'authorized user with invalid permissions' do
|
||||
it 'does not create a secure file' do
|
||||
post api("/projects/#{project.id}/secure_files", user2)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:forbidden)
|
||||
end
|
||||
end
|
||||
|
||||
context 'unauthorized user' do
|
||||
it 'does not create a secure file' do
|
||||
post api("/projects/#{project.id}/secure_files")
|
||||
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE /projects/:id/secure_files/:secure_file_id' do
|
||||
context 'authorized user with proper permissions' do
|
||||
it 'deletes the secure file' do
|
||||
expect do
|
||||
delete api("/projects/#{project.id}/secure_files/#{secure_file.id}", user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:no_content)
|
||||
end.to change {project.secure_files.count}.by(-1)
|
||||
end
|
||||
|
||||
it 'responds with 404 Not Found if requesting non-existing secure_file' do
|
||||
delete api("/projects/#{project.id}/secure_files/99999", user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
context 'authorized user with invalid permissions' do
|
||||
it 'does not delete the secure_file' do
|
||||
delete api("/projects/#{project.id}/secure_files/#{secure_file.id}", user2)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:forbidden)
|
||||
end
|
||||
end
|
||||
|
||||
context 'unauthorized user' do
|
||||
it 'does not delete the secure_file' do
|
||||
delete api("/projects/#{project.id}/secure_files/#{secure_file.id}")
|
||||
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe IssueSidebarBasicEntity do
|
||||
let_it_be(:project) { create(:project, :repository) }
|
||||
let_it_be(:user) { create(:user, developer_projects: [project]) }
|
||||
let_it_be(:issue) { create(:issue, project: project, assignees: [user]) }
|
||||
|
||||
let(:serializer) { IssueSerializer.new(current_user: user, project: project) }
|
||||
|
||||
subject(:entity) { serializer.represent(issue, serializer: 'sidebar') }
|
||||
|
||||
it 'contains keys related to issuables' do
|
||||
expect(entity).to include(
|
||||
:id, :iid, :type, :author_id, :project_id, :discussion_locked, :reference, :milestone,
|
||||
:labels, :current_user, :issuable_json_path, :namespace_path, :project_path,
|
||||
:project_full_path, :project_issuables_path, :create_todo_path, :project_milestones_path,
|
||||
:project_labels_path, :toggle_subscription_path, :move_issue_path, :projects_autocomplete_path,
|
||||
:project_emails_disabled, :create_note_email, :supports_time_tracking, :supports_milestone,
|
||||
:supports_severity, :supports_escalation
|
||||
)
|
||||
end
|
||||
|
||||
it 'contains attributes related to the issue' do
|
||||
expect(entity).to include(:due_date, :confidential, :severity)
|
||||
end
|
||||
|
||||
describe 'current_user' do
|
||||
it 'contains attributes related to the current user' do
|
||||
expect(entity[:current_user]).to include(
|
||||
:id, :name, :username, :state, :avatar_url, :web_url, :todo,
|
||||
:can_edit, :can_move, :can_admin_label
|
||||
)
|
||||
end
|
||||
|
||||
describe 'can_update_escalation_status' do
|
||||
context 'for a standard issue' do
|
||||
it 'is not present' do
|
||||
expect(entity[:current_user]).not_to have_key(:can_update_escalation_status)
|
||||
end
|
||||
end
|
||||
|
||||
context 'for an incident issue' do
|
||||
before do
|
||||
issue.update!(issue_type: Issue.issue_types[:incident])
|
||||
end
|
||||
|
||||
it 'is present and true' do
|
||||
expect(entity[:current_user][:can_update_escalation_status]).to be(true)
|
||||
end
|
||||
|
||||
context 'without permissions' do
|
||||
let(:serializer) { IssueSerializer.new(current_user: create(:user), project: project) }
|
||||
|
||||
it 'is present and false' do
|
||||
expect(entity[:current_user]).to have_key(:can_update_escalation_status)
|
||||
expect(entity[:current_user][:can_update_escalation_status]).to be(false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with :incident_escalations feature flag disabled' do
|
||||
before do
|
||||
stub_feature_flags(incident_escalations: false)
|
||||
end
|
||||
|
||||
it 'is not present' do
|
||||
expect(entity[:current_user]).not_to include(:can_update_escalation_status)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -146,7 +146,7 @@ RSpec.describe Releases::CreateService do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when multiple miletone titles are passed in but one of them does not exist' do
|
||||
context 'when multiple milestone titles are passed in but one of them does not exist' do
|
||||
let(:title) { 'v1.0' }
|
||||
let(:inexistent_title) { 'v111.0' }
|
||||
let!(:milestone) { create(:milestone, :active, project: project, title: title) }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'shared/issuable/_sidebar.html.haml' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
subject(:rendered) do
|
||||
render 'shared/issuable/sidebar', issuable_sidebar: IssueSerializer.new(current_user: user)
|
||||
.represent(issuable, serializer: 'sidebar'), assignees: []
|
||||
end
|
||||
|
||||
context 'project in a group' do
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:project) { create(:project, group: group) }
|
||||
let_it_be(:issue) { create(:issue, project: project) }
|
||||
let_it_be(:incident) { create(:incident, project: project) }
|
||||
|
||||
before do
|
||||
assign(:project, project)
|
||||
end
|
||||
|
||||
context 'issuable that does not support escalations' do
|
||||
let(:issuable) { incident }
|
||||
|
||||
it 'shows escalation policy dropdown' do
|
||||
expect(rendered).to have_css('[data-testid="escalation_status_container"]')
|
||||
end
|
||||
end
|
||||
|
||||
context 'issuable that supports escalations' do
|
||||
let(:issuable) { issue }
|
||||
|
||||
it 'does not show escalation policy dropdown' do
|
||||
expect(rendered).not_to have_css('[data-testid="escalation_status_container"]')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Reference in New Issue