Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
94c1ea6190
commit
72875e4a37
48 changed files with 1041 additions and 355 deletions
|
@ -194,12 +194,17 @@ Dangerfile @gl-quality/eng-prod
|
|||
|
||||
# Secure & Threat Management ownership delineation
|
||||
# https://about.gitlab.com/handbook/engineering/development/threat-management/delineate-secure-threat-management.html#technical-boundaries
|
||||
[Secure]
|
||||
[Threat Insights]
|
||||
/ee/app/finders/security/ @gitlab-org/secure/threat-insights-backend-team
|
||||
/ee/app/models/security/ @gitlab-org/secure/threat-insights-backend-team
|
||||
/ee/app/models/vulnerabilities/ @gitlab-org/secure/threat-insights-backend-team
|
||||
/ee/app/models/vulnerability.rb @gitlab-org/secure/threat-insights-backend-team
|
||||
/ee/app/policies/vulnerabilities/ @gitlab-org/secure/threat-insights-backend-team
|
||||
/ee/app/policies/vulnerability*.rb @gitlab-org/secure/threat-insights-backend-team
|
||||
/ee/lib/api/vulnerabilit*.rb @gitlab-org/secure/threat-insights-backend-team
|
||||
/ee/spec/policies/vulnerabilities/ @gitlab-org/secure/threat-insights-backend-team
|
||||
/ee/spec/policies/vulnerabilities/vulnerability*.rb @gitlab-org/secure/threat-insights-backend-team
|
||||
[Secure]
|
||||
/ee/lib/gitlab/ci/parsers/license_compliance/ @gitlab-org/secure/composition-analysis-be
|
||||
/ee/lib/gitlab/ci/parsers/security/ @gitlab-org/secure/composition-analysis-be @gitlab-org/secure/dynamic-analysis-be @gitlab-org/secure/static-analysis-be @gitlab-org/secure/fuzzing-be
|
||||
/ee/lib/gitlab/ci/reports/coverage_fuzzing/ @gitlab-org/secure/fuzzing-be
|
||||
|
|
|
@ -1,20 +1,48 @@
|
|||
<script>
|
||||
import $ from 'jquery';
|
||||
import { escape } from 'lodash';
|
||||
import { GlButton, GlModalDirective } from '@gitlab/ui';
|
||||
import { GlButton, GlModalDirective, GlSkeletonLoader } from '@gitlab/ui';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
import { mouseenter, debouncedMouseleave, togglePopover } from '~/shared/popover';
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
|
||||
import StatusIcon from '../mr_widget_status_icon.vue';
|
||||
import userPermissionsQuery from '../../queries/permissions.query.graphql';
|
||||
import conflictsStateQuery from '../../queries/states/conflicts.query.graphql';
|
||||
|
||||
export default {
|
||||
name: 'MRWidgetConflicts',
|
||||
components: {
|
||||
GlSkeletonLoader,
|
||||
StatusIcon,
|
||||
GlButton,
|
||||
},
|
||||
directives: {
|
||||
GlModalDirective,
|
||||
},
|
||||
mixins: [glFeatureFlagMixin(), mergeRequestQueryVariablesMixin],
|
||||
apollo: {
|
||||
userPermissions: {
|
||||
query: userPermissionsQuery,
|
||||
skip() {
|
||||
return !this.glFeatures.mergeRequestWidgetGraphql;
|
||||
},
|
||||
variables() {
|
||||
return this.mergeRequestQueryVariables;
|
||||
},
|
||||
update: data => data.project.mergeRequest.userPermissions,
|
||||
},
|
||||
stateData: {
|
||||
query: conflictsStateQuery,
|
||||
skip() {
|
||||
return !this.glFeatures.mergeRequestWidgetGraphql;
|
||||
},
|
||||
variables() {
|
||||
return this.mergeRequestQueryVariables;
|
||||
},
|
||||
update: data => data.project.mergeRequest,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
/* TODO: This is providing all store and service down when it
|
||||
only needs a few props */
|
||||
|
@ -24,21 +52,72 @@ export default {
|
|||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
userPermissions: {},
|
||||
stateData: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isLoading() {
|
||||
return (
|
||||
this.glFeatures.mergeRequestWidgetGraphql &&
|
||||
this.$apollo.queries.userPermissions.loading &&
|
||||
this.$apollo.queries.stateData.loading
|
||||
);
|
||||
},
|
||||
canPushToSourceBranch() {
|
||||
if (this.glFeatures.mergeRequestWidgetGraphql) {
|
||||
return this.userPermissions.pushToSourceBranch;
|
||||
}
|
||||
|
||||
return this.mr.canPushToSourceBranch;
|
||||
},
|
||||
canMerge() {
|
||||
if (this.glFeatures.mergeRequestWidgetGraphql) {
|
||||
return this.userPermissions.canMerge;
|
||||
}
|
||||
|
||||
return this.mr.canMerge;
|
||||
},
|
||||
shouldBeRebased() {
|
||||
if (this.glFeatures.mergeRequestWidgetGraphql) {
|
||||
return this.stateData.shouldBeRebased;
|
||||
}
|
||||
|
||||
return this.mr.shouldBeRebased;
|
||||
},
|
||||
sourceBranchProtected() {
|
||||
if (this.glFeatures.mergeRequestWidgetGraphql) {
|
||||
return this.stateData.sourceBranchProtected;
|
||||
}
|
||||
|
||||
return this.mr.sourceBranchProtected;
|
||||
},
|
||||
popoverTitle() {
|
||||
return s__(
|
||||
'mrWidget|This feature merges changes from the target branch to the source branch. You cannot use this feature since the source branch is protected.',
|
||||
);
|
||||
},
|
||||
showResolveButton() {
|
||||
return this.mr.conflictResolutionPath && this.mr.canPushToSourceBranch;
|
||||
return this.mr.conflictResolutionPath && this.canPushToSourceBranch;
|
||||
},
|
||||
showPopover() {
|
||||
return this.showResolveButton && this.mr.sourceBranchProtected;
|
||||
return this.showResolveButton && this.sourceBranchProtected;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.showPopover) {
|
||||
watch: {
|
||||
showPopover: {
|
||||
handler(newVal) {
|
||||
if (newVal) {
|
||||
this.$nextTick(this.initPopover);
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
initPopover() {
|
||||
const $el = $(this.$refs.popover);
|
||||
|
||||
$el
|
||||
|
@ -68,7 +147,7 @@ export default {
|
|||
.on('show.bs.popover', () => {
|
||||
window.addEventListener('scroll', togglePopover.bind($el, false), { once: true });
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -76,17 +155,24 @@ export default {
|
|||
<div class="mr-widget-body media">
|
||||
<status-icon :show-disabled-button="true" status="warning" />
|
||||
|
||||
<div class="media-body space-children">
|
||||
<span v-if="mr.shouldBeRebased" class="bold">
|
||||
<div v-if="isLoading" class="gl-ml-4 gl-w-full mr-conflict-loader">
|
||||
<gl-skeleton-loader :width="334" :height="30">
|
||||
<rect x="0" y="7" width="150" height="16" rx="4" />
|
||||
<rect x="158" y="7" width="84" height="16" rx="4" />
|
||||
<rect x="250" y="7" width="84" height="16" rx="4" />
|
||||
</gl-skeleton-loader>
|
||||
</div>
|
||||
<div v-else class="media-body space-children">
|
||||
<span v-if="shouldBeRebased" class="bold">
|
||||
{{
|
||||
s__(`mrWidget|Fast-forward merge is not possible.
|
||||
To merge this request, first rebase locally.`)
|
||||
To merge this request, first rebase locally.`)
|
||||
}}
|
||||
</span>
|
||||
<template v-else>
|
||||
<span class="bold">
|
||||
{{ s__('mrWidget|There are merge conflicts') }}<span v-if="!mr.canMerge">.</span>
|
||||
<span v-if="!mr.canMerge">
|
||||
{{ s__('mrWidget|There are merge conflicts') }}<span v-if="!canMerge">.</span>
|
||||
<span v-if="!canMerge">
|
||||
{{
|
||||
s__(`mrWidget|Resolve these conflicts or ask someone
|
||||
with write access to this repository to merge it locally`)
|
||||
|
@ -95,15 +181,15 @@ To merge this request, first rebase locally.`)
|
|||
</span>
|
||||
<span v-if="showResolveButton" ref="popover">
|
||||
<gl-button
|
||||
:href="mr.conflictResolutionPath"
|
||||
:disabled="mr.sourceBranchProtected"
|
||||
:href="!sourceBranchProtected && mr.conflictResolutionPath"
|
||||
:disabled="sourceBranchProtected"
|
||||
class="js-resolve-conflicts-button"
|
||||
>
|
||||
{{ s__('mrWidget|Resolve conflicts') }}
|
||||
</gl-button>
|
||||
</span>
|
||||
<gl-button
|
||||
v-if="mr.canMerge"
|
||||
v-if="canMerge"
|
||||
v-gl-modal-directive="'modal-merge-info'"
|
||||
class="js-merge-locally-button"
|
||||
>
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
query userPermissionsQuery($projectPath: ID!, $iid: String!) {
|
||||
project(fullPath: $projectPath) {
|
||||
mergeRequest(iid: $iid) {
|
||||
userPermissions {
|
||||
canMerge
|
||||
pushToSourceBranch
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
query workInProgressQuery($projectPath: ID!, $iid: String!) {
|
||||
project(fullPath: $projectPath) {
|
||||
mergeRequest(iid: $iid) {
|
||||
shouldBeRebased
|
||||
sourceBranchProtected
|
||||
}
|
||||
}
|
||||
}
|
|
@ -468,7 +468,6 @@ $gl-line-height-20: 20px;
|
|||
$gl-line-height-24: 24px;
|
||||
$gl-line-height-14: 14px;
|
||||
|
||||
$issue-box-upcoming-bg: #8f8f8f;
|
||||
$pages-group-name-color: #4c4e54;
|
||||
|
||||
/*
|
||||
|
|
|
@ -693,10 +693,6 @@
|
|||
|
||||
.issuable-list {
|
||||
li {
|
||||
.issue-box {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.issuable-info-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
|
|
|
@ -1039,3 +1039,11 @@ $mr-widget-min-height: 69px;
|
|||
.diff-file-row.is-active {
|
||||
background-color: $gray-50;
|
||||
}
|
||||
|
||||
.mr-conflict-loader {
|
||||
max-width: 334px;
|
||||
|
||||
> svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class WhatsNewController < ApplicationController
|
||||
include Gitlab::WhatsNew
|
||||
|
||||
skip_before_action :authenticate_user!
|
||||
|
||||
before_action :check_feature_flag, :check_valid_page_param, :set_pagination_headers
|
||||
|
@ -12,7 +10,7 @@ class WhatsNewController < ApplicationController
|
|||
def index
|
||||
respond_to do |format|
|
||||
format.js do
|
||||
render json: whats_new_release_items(page: current_page)
|
||||
render json: most_recent_items
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -27,18 +25,19 @@ class WhatsNewController < ApplicationController
|
|||
render_404 if current_page < 1
|
||||
end
|
||||
|
||||
def set_pagination_headers
|
||||
response.set_header('X-Next-Page', next_page)
|
||||
end
|
||||
|
||||
def current_page
|
||||
params[:page]&.to_i || 1
|
||||
end
|
||||
|
||||
def next_page
|
||||
next_page = current_page + 1
|
||||
next_index = next_page - 1
|
||||
def most_recent
|
||||
@most_recent ||= ReleaseHighlight.paginated(page: current_page)
|
||||
end
|
||||
|
||||
next_page if whats_new_file_paths[next_index]
|
||||
def most_recent_items
|
||||
most_recent[:items].map {|item| Gitlab::WhatsNew::ItemPresenter.present(item) }
|
||||
end
|
||||
|
||||
def set_pagination_headers
|
||||
response.set_header('X-Next-Page', most_recent[:next_page])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -49,6 +49,8 @@ module Types
|
|||
description: 'ID of the merge request target project'
|
||||
field :source_branch, GraphQL::STRING_TYPE, null: false,
|
||||
description: 'Source branch of the merge request'
|
||||
field :source_branch_protected, GraphQL::BOOLEAN_TYPE, null: false, calls_gitaly: true,
|
||||
description: 'Indicates if the source branch is protected'
|
||||
field :target_branch, GraphQL::STRING_TYPE, null: false,
|
||||
description: 'Target branch of the merge request'
|
||||
field :work_in_progress, GraphQL::BOOLEAN_TYPE, method: :work_in_progress?, null: false,
|
||||
|
@ -194,6 +196,10 @@ module Types
|
|||
def commit_count
|
||||
object&.metrics&.commits_count
|
||||
end
|
||||
|
||||
def source_branch_protected
|
||||
object.source_project.present? && ProtectedBranch.protected?(object.source_project, object.source_branch)
|
||||
end
|
||||
end
|
||||
end
|
||||
Types::MergeRequestType.prepend_if_ee('::EE::Types::MergeRequestType')
|
||||
|
|
|
@ -1,25 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module WhatsNewHelper
|
||||
include Gitlab::WhatsNew
|
||||
|
||||
def whats_new_most_recent_release_items_count
|
||||
Gitlab::ProcessMemoryCache.cache_backend.fetch('whats_new:release_items_count', expires_in: CACHE_DURATION) do
|
||||
whats_new_release_items&.count
|
||||
end
|
||||
ReleaseHighlight.most_recent_item_count
|
||||
end
|
||||
|
||||
def whats_new_storage_key
|
||||
return unless whats_new_most_recent_version
|
||||
most_recent_version = ReleaseHighlight.most_recent_version
|
||||
|
||||
['display-whats-new-notification', whats_new_most_recent_version].join('-')
|
||||
end
|
||||
return unless most_recent_version
|
||||
|
||||
private
|
||||
|
||||
def whats_new_most_recent_version
|
||||
Gitlab::ProcessMemoryCache.cache_backend.fetch('whats_new:release_version', expires_in: CACHE_DURATION) do
|
||||
whats_new_release_items&.first&.[]('release')
|
||||
end
|
||||
['display-whats-new-notification', most_recent_version].join('-')
|
||||
end
|
||||
end
|
||||
|
|
67
app/models/release_highlight.rb
Normal file
67
app/models/release_highlight.rb
Normal file
|
@ -0,0 +1,67 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ReleaseHighlight
|
||||
CACHE_DURATION = 1.hour
|
||||
FILES_PATH = Rails.root.join('data', 'whats_new', '*.yml')
|
||||
|
||||
def self.paginated(page: 1)
|
||||
Rails.cache.fetch(cache_key(page), expires_in: CACHE_DURATION) do
|
||||
items = self.load_items(page: page)
|
||||
|
||||
next if items.nil?
|
||||
|
||||
{
|
||||
items: items,
|
||||
next_page: next_page(current_page: page)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def self.load_items(page:)
|
||||
index = page - 1
|
||||
file_path = file_paths[index]
|
||||
|
||||
return if file_path.nil?
|
||||
|
||||
file = File.read(file_path)
|
||||
|
||||
items = YAML.safe_load(file, permitted_classes: [Date])
|
||||
|
||||
platform = Gitlab.com? ? 'gitlab-com' : 'self-managed'
|
||||
items&.select {|item| item[platform] }
|
||||
rescue Psych::Exception => e
|
||||
Gitlab::ErrorTracking.track_exception(e, file_path: file_path)
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
def self.file_paths
|
||||
@file_paths ||= Rails.cache.fetch('release_highlight:file_paths', expires_in: CACHE_DURATION) do
|
||||
Dir.glob(FILES_PATH).sort.reverse
|
||||
end
|
||||
end
|
||||
|
||||
def self.cache_key(page)
|
||||
filename = /\d*\_\d*\_\d*/.match(self.file_paths&.first)
|
||||
"release_highlight:items:file-#{filename}:page-#{page}"
|
||||
end
|
||||
|
||||
def self.next_page(current_page: 1)
|
||||
next_page = current_page + 1
|
||||
next_index = next_page - 1
|
||||
|
||||
next_page if self.file_paths[next_index]
|
||||
end
|
||||
|
||||
def self.most_recent_version
|
||||
Gitlab::ProcessMemoryCache.cache_backend.fetch('release_highlight:release_version', expires_in: CACHE_DURATION) do
|
||||
self.paginated&.[](:items)&.first&.[]('release')
|
||||
end
|
||||
end
|
||||
|
||||
def self.most_recent_item_count
|
||||
Gitlab::ProcessMemoryCache.cache_backend.fetch('release_highlight:recent_item_count', expires_in: CACHE_DURATION) do
|
||||
self.paginated&.[](:items)&.count
|
||||
end
|
||||
end
|
||||
end
|
22
app/presenters/gitlab/whats_new/item_presenter.rb
Normal file
22
app/presenters/gitlab/whats_new/item_presenter.rb
Normal file
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module WhatsNew
|
||||
class ItemPresenter
|
||||
DICTIONARY = {
|
||||
free: 'Free',
|
||||
starter: 'Bronze',
|
||||
premium: 'Silver',
|
||||
ultimate: 'Gold'
|
||||
}.freeze
|
||||
|
||||
def self.present(item)
|
||||
if Gitlab.com?
|
||||
item['packages'] = item['packages'].map { |p| DICTIONARY[p.downcase.to_sym] }
|
||||
end
|
||||
|
||||
item
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
5
changelogs/unreleased/284602-remove-issue-box-css.yml
Normal file
5
changelogs/unreleased/284602-remove-issue-box-css.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Remove unused .issue-box CSS
|
||||
merge_request: 48002
|
||||
author: Takuya Noguchi
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add packages_size to ProjectStatistics API entity
|
||||
merge_request: 47156
|
||||
author: Roger Meier
|
||||
type: added
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIndexOnPackageSizeAndProjectIdToProjectStatistics < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
INDEX_NAME = 'index_project_statistics_on_packages_size_and_project_id'
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_index :project_statistics, [:packages_size, :project_id],
|
||||
name: INDEX_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index_by_name :project_statistics, INDEX_NAME
|
||||
end
|
||||
end
|
1
db/schema_migrations/20201117153333
Normal file
1
db/schema_migrations/20201117153333
Normal file
|
@ -0,0 +1 @@
|
|||
008f3a69d23abbd513336c5a48b2448e470a9413920beeb6a1684d0c6840d6a4
|
|
@ -21779,6 +21779,8 @@ CREATE UNIQUE INDEX index_project_settings_on_push_rule_id ON project_settings U
|
|||
|
||||
CREATE INDEX index_project_statistics_on_namespace_id ON project_statistics USING btree (namespace_id);
|
||||
|
||||
CREATE INDEX index_project_statistics_on_packages_size_and_project_id ON project_statistics USING btree (packages_size, project_id);
|
||||
|
||||
CREATE UNIQUE INDEX index_project_statistics_on_project_id ON project_statistics USING btree (project_id);
|
||||
|
||||
CREATE INDEX index_project_statistics_on_repository_size_and_project_id ON project_statistics USING btree (repository_size, project_id);
|
||||
|
|
|
@ -133,12 +133,6 @@ recorded:
|
|||
- A user's personal access token was successfully created or revoked ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/276921) in GitLab 13.6)
|
||||
- A failed attempt to create or revoke a user's personal access token ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/276921) in GitLab 13.6)
|
||||
|
||||
It's possible to filter particular actions by choosing an audit data type from
|
||||
the filter dropdown box. You can further filter by specific group, project, or user
|
||||
(for authentication events).
|
||||
|
||||
![audit log](img/audit_log.png)
|
||||
|
||||
Instance events can also be accessed via the [Instance Audit Events API](../api/audit_events.md#instance-audit-events).
|
||||
|
||||
### Missing events
|
||||
|
@ -180,6 +174,19 @@ the steps bellow.
|
|||
Feature.enable(:repository_push_audit_event)
|
||||
```
|
||||
|
||||
## Search
|
||||
|
||||
The search filters you can see depends on which audit level you are at.
|
||||
|
||||
| Filter | Available options |
|
||||
| ------ | ----------------- |
|
||||
| Scope (Project level) | A specific user who performed the action. |
|
||||
| Scope (Group level) | A specific user (in a group) who performed the action. |
|
||||
| Scope (Instance level) | A specific group, project, or user that the action was scoped to. |
|
||||
| Date range | Either via the date range buttons or pickers (maximum range of 31 days). Default is from the first day of the month to today's date. |
|
||||
|
||||
![audit log](img/audit_log_v13_6.png)
|
||||
|
||||
## Export to CSV **(PREMIUM ONLY)**
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/1449) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.4.
|
||||
|
@ -193,23 +200,18 @@ This feature might not be available to you. Check the **version history** note a
|
|||
If available, you can enable it with a [feature flag](#enable-or-disable-audit-log-export-to-csv).
|
||||
|
||||
Export to CSV allows customers to export the current filter view of your audit log as a
|
||||
CSV file,
|
||||
which stores tabular data in plain text. The data provides a comprehensive view with respect to
|
||||
CSV file, which stores tabular data in plain text. The data provides a comprehensive view with respect to
|
||||
audit events.
|
||||
|
||||
To export the Audit Log to CSV, navigate to
|
||||
**{monitor}** **Admin Area > Monitoring > Audit Log**
|
||||
|
||||
1. Click in the field **Search**.
|
||||
1. In the dropdown menu that appears, select the event type that you want to filter by.
|
||||
1. Select the preferred date range.
|
||||
1. Select the available search [filters](#search).
|
||||
1. Click **Export as CSV**.
|
||||
|
||||
![Export Audit Log](img/export_audit_log_v13_4.png)
|
||||
|
||||
### Sort
|
||||
|
||||
Exported events are always sorted by `ID` in ascending order.
|
||||
Exported events are always sorted by `created_at` in ascending order.
|
||||
|
||||
### Format
|
||||
|
||||
|
@ -222,8 +224,8 @@ The first row contains the headers, which are listed in the following table alon
|
|||
| Author ID | ID of the author |
|
||||
| Author Name | Full name of the author |
|
||||
| Entity ID | ID of the scope |
|
||||
| Entity Type | Type of the entity (`Project`/`Group`/`User`) |
|
||||
| Entity Path | Path of the entity |
|
||||
| Entity Type | Type of the scope (`Project`/`Group`/`User`) |
|
||||
| Entity Path | Path of the scope |
|
||||
| Target ID | ID of the target |
|
||||
| Target Type | Type of the target |
|
||||
| Target Details | Details of the target |
|
||||
|
@ -233,7 +235,7 @@ The first row contains the headers, which are listed in the following table alon
|
|||
|
||||
### Limitation
|
||||
|
||||
The Audit Log CSV file size is limited to a maximum of `100,000` events.
|
||||
The Audit Log CSV file is limited to a maximum of `100,000` events.
|
||||
The remaining records are truncated when this limit is reached.
|
||||
|
||||
### Enable or disable Audit Log Export to CSV
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 25 KiB |
BIN
doc/administration/img/audit_log_v13_6.png
Normal file
BIN
doc/administration/img/audit_log_v13_6.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 70 KiB |
Binary file not shown.
Before Width: | Height: | Size: 46 KiB |
|
@ -25,8 +25,9 @@ you want using steps 1 and 2 from the GitLab downloads page.
|
|||
|
||||
## Optional: Enable extra Sidekiq processes
|
||||
sidekiq_cluster['enable'] = true
|
||||
sidekiq_cluster['enable'] = true
|
||||
"elastic_indexer"
|
||||
sidekiq['queue_groups'] = [
|
||||
"elastic_indexer",
|
||||
"*"
|
||||
]
|
||||
```
|
||||
|
||||
|
|
|
@ -12780,6 +12780,11 @@ type MergeRequest implements CurrentUserTodos & Noteable {
|
|||
"""
|
||||
sourceBranchExists: Boolean!
|
||||
|
||||
"""
|
||||
Indicates if the source branch is protected
|
||||
"""
|
||||
sourceBranchProtected: Boolean!
|
||||
|
||||
"""
|
||||
Source project of the merge request
|
||||
"""
|
||||
|
|
|
@ -35042,6 +35042,24 @@
|
|||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "sourceBranchProtected",
|
||||
"description": "Indicates if the source branch is protected",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Boolean",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "sourceProject",
|
||||
"description": "Source project of the merge request",
|
||||
|
|
|
@ -1950,6 +1950,7 @@ Autogenerated return type of MarkAsSpamSnippet.
|
|||
| `shouldRemoveSourceBranch` | Boolean | Indicates if the source branch of the merge request will be deleted after merge |
|
||||
| `sourceBranch` | String! | Source branch of the merge request |
|
||||
| `sourceBranchExists` | Boolean! | Indicates if the source branch of the merge request exists |
|
||||
| `sourceBranchProtected` | Boolean! | Indicates if the source branch is protected |
|
||||
| `sourceProject` | Project | Source project of the merge request |
|
||||
| `sourceProjectId` | Int | ID of the merge request source project |
|
||||
| `state` | MergeRequestState! | State of the merge request |
|
||||
|
|
|
@ -49,7 +49,7 @@ GET /projects
|
|||
| `last_activity_before` | datetime | **{dotted-circle}** No | Limit results to projects with last_activity before specified time. Format: ISO 8601 YYYY-MM-DDTHH:MM:SSZ |
|
||||
| `membership` | boolean | **{dotted-circle}** No | Limit by projects that the current user is a member of. |
|
||||
| `min_access_level` | integer | **{dotted-circle}** No | Limit by current user minimal [access level](members.md#valid-access-levels). |
|
||||
| `order_by` | string | **{dotted-circle}** No | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. `repository_size`, `storage_size`, or `wiki_size` fields are only allowed for admins. Default is `created_at`. |
|
||||
| `order_by` | string | **{dotted-circle}** No | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. `repository_size`, `storage_size`, `packages_size` or `wiki_size` fields are only allowed for admins. Default is `created_at`. |
|
||||
| `owned` | boolean | **{dotted-circle}** No | Limit by projects explicitly owned by the current user. |
|
||||
| `repository_checksum_failed` **(PREMIUM)** | boolean | **{dotted-circle}** No | Limit projects where the repository checksum calculation has failed ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/6137) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.2). |
|
||||
| `repository_storage` | string | **{dotted-circle}** No | Limit results to projects stored on `repository_storage`. _(admins only)_ |
|
||||
|
|
|
@ -224,7 +224,7 @@ PUT /runners/:id
|
|||
| `run_untagged`| boolean | no | Flag indicating the runner can execute untagged jobs |
|
||||
| `locked` | boolean | no | Flag indicating the runner is locked |
|
||||
| `access_level` | string | no | The access_level of the runner; `not_protected` or `ref_protected` |
|
||||
| `maximum_timeout` | integer | no | Maximum timeout set when this runner will handle the job |
|
||||
| `maximum_timeout` | integer | no | Maximum timeout set when this runner handles the job |
|
||||
|
||||
```shell
|
||||
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/runners/6" --form "description=test-1-20150125-test" --form "tag_list=ruby,mysql,tag1,tag2"
|
||||
|
@ -559,7 +559,7 @@ POST /runners
|
|||
| `run_untagged` | boolean | no | Whether the runner should handle untagged jobs |
|
||||
| `tag_list` | string array | no | List of runner's tags |
|
||||
| `access_level` | string | no | The access_level of the runner; `not_protected` or `ref_protected` |
|
||||
| `maximum_timeout` | integer | no | Maximum timeout set when this runner will handle the job |
|
||||
| `maximum_timeout` | integer | no | Maximum timeout set when this runner handles the job |
|
||||
|
||||
```shell
|
||||
curl --request POST "https://gitlab.example.com/api/v4/runners" --form "token=<registration_token>" --form "description=test-1-20150125-test" --form "tag_list=ruby,mysql,tag1,tag2"
|
||||
|
|
|
@ -589,6 +589,147 @@ The configuration is picked up by the `dind` service.
|
|||
sub_path = "daemon.json"
|
||||
```
|
||||
|
||||
## Authenticating with registry in Docker-in-Docker
|
||||
|
||||
When you use Docker-in-Docker, the [normal authentication
|
||||
methods](using_docker_images.html#define-an-image-from-a-private-container-registry)
|
||||
won't work because a fresh Docker daemon is started with the service.
|
||||
|
||||
### Option 1: Run `docker login`
|
||||
|
||||
In [`before_script`](../yaml/README.md#before_script) run `docker
|
||||
login`:
|
||||
|
||||
```yaml
|
||||
image: docker:19.03.13
|
||||
|
||||
variables:
|
||||
DOCKER_TLS_CERTDIR: "/certs"
|
||||
|
||||
services:
|
||||
- docker:19.03.13-dind
|
||||
|
||||
build:
|
||||
stage: build
|
||||
before_script:
|
||||
- echo "$DOCKER_REGISTRY_PASS" | docker login $DOCKER_REGISTRY --username $DOCKER_REGISTRY_USER --password-stdin
|
||||
script:
|
||||
- docker build -t my-docker-image .
|
||||
- docker run my-docker-image /script/to/run/tests
|
||||
```
|
||||
|
||||
To log in to Docker Hub, leave `$DOCKER_REGISTRY`
|
||||
empty or remove it.
|
||||
|
||||
### Option 2: Mount `~/.docker/config.json` on each job
|
||||
|
||||
If you are an administrator for GitLab Runner, you can mount a file
|
||||
with the authentication configuration to `~/.docker/config.json`.
|
||||
Then every job that the runner picks up will be authenticated already. If you
|
||||
are using the official `docker:19.03.13` image, the home directory is
|
||||
under `/root`.
|
||||
|
||||
If you mount the config file, any `docker` command
|
||||
that modifies the `~/.docker/config.json` (for example, `docker login`)
|
||||
fails, because the file is mounted as read-only. Do not change it from
|
||||
read-only, because other problems will occur.
|
||||
|
||||
Here is an example of `/opt/.docker/config.json` that follows the
|
||||
[`DOCKER_AUTH_CONFIG`](using_docker_images.md#determining-your-docker_auth_config-data)
|
||||
documentation:
|
||||
|
||||
```json
|
||||
{
|
||||
"auths": {
|
||||
"https://index.docker.io/v1/": {
|
||||
"auth": "bXlfdXNlcm5hbWU6bXlfcGFzc3dvcmQ="
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Docker
|
||||
|
||||
Update the [volume
|
||||
mounts](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#volumes-in-the-runnersdocker-section)
|
||||
to include the file.
|
||||
|
||||
```toml
|
||||
[[runners]]
|
||||
...
|
||||
executor = "docker"
|
||||
[runners.docker]
|
||||
...
|
||||
privileged = true
|
||||
volumes = ["/opt/.docker/config.json:/root/.docker/config.json:ro"]
|
||||
```
|
||||
|
||||
#### Kubernetes
|
||||
|
||||
Create a [ConfigMap](https://kubernetes.io/docs/concepts/configuration/configmap/) with the content
|
||||
of this file. You can do this with a command like:
|
||||
|
||||
```shell
|
||||
kubectl create configmap docker-client-config --namespace gitlab-runner --from-file /opt/.docker/config.json
|
||||
```
|
||||
|
||||
Update the [volume
|
||||
mounts](https://docs.gitlab.com/runner/executors/kubernetes.html#using-volumes)
|
||||
to include the file.
|
||||
|
||||
```toml
|
||||
[[runners]]
|
||||
...
|
||||
executor = "kubernetes"
|
||||
[runners.kubernetes]
|
||||
image = "alpine:3.12"
|
||||
privileged = true
|
||||
[[runners.kubernetes.volumes.config_map]]
|
||||
name = "docker-client-config"
|
||||
mount_path = "/root/.docker/config.json"
|
||||
# If you are running GitLab Runner 13.5
|
||||
# or lower you can remove this
|
||||
sub_path = "config.json"
|
||||
```
|
||||
|
||||
### Option 3: Use `DOCKER_ATUH_CONFIG`
|
||||
|
||||
If you already have
|
||||
[`DOCKER_AUTH_CONFIG`](using_docker_images.md#determining-your-docker_auth_config-data)
|
||||
defined, you can use the variable and save it in
|
||||
`~/.docker/config.json`.
|
||||
|
||||
There are multiple ways to define this. For example:
|
||||
|
||||
- Inside
|
||||
[`pre_build_script`](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runners-section)
|
||||
inside of the runner config file.
|
||||
- Inside [`before_script`](../yaml/README.md#before_script).
|
||||
- Inside of [`script`](../yaml/README.md#script).
|
||||
|
||||
Below is an example of
|
||||
[`before_script`](../yaml/README.md#before_script). The same commands
|
||||
apply for any solution you implement.
|
||||
|
||||
```yaml
|
||||
image: docker:19.03.13
|
||||
|
||||
variables:
|
||||
DOCKER_TLS_CERTDIR: "/certs"
|
||||
|
||||
services:
|
||||
- docker:19.03.13-dind
|
||||
|
||||
build:
|
||||
stage: build
|
||||
before_script:
|
||||
- mkdir -p $HOME/.docker
|
||||
- echo $DOCKER_AUTH_CONFIG > $HOME/.docker/config.json
|
||||
script:
|
||||
- docker build -t my-docker-image .
|
||||
- docker run my-docker-image /script/to/run/tests
|
||||
```
|
||||
|
||||
## Making Docker-in-Docker builds faster with Docker layer caching
|
||||
|
||||
When using Docker-in-Docker, Docker downloads all layers of your image every
|
||||
|
|
53
doc/development/bulk_import.md
Normal file
53
doc/development/bulk_import.md
Normal file
|
@ -0,0 +1,53 @@
|
|||
---
|
||||
stage: Manage
|
||||
group: Import
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
---
|
||||
|
||||
# GitLab Group Migration
|
||||
|
||||
[Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2771) in GitLab 13.7.
|
||||
|
||||
CAUTION: **Caution:**
|
||||
This feature is [under construction](https://gitlab.com/groups/gitlab-org/-/epics/2771) and its API/Architecture might change in the future.
|
||||
|
||||
GitLab Group Migration is the evolution of Project and Group Import functionality. The
|
||||
goal is to have an easier way to the user migrate a whole Group, including
|
||||
Projects, from one GitLab instance to another.
|
||||
|
||||
## Design decisions
|
||||
|
||||
### Overview
|
||||
|
||||
The following architectural diagram illustrates how the Group Migration
|
||||
works with a set of [ETL](#etl) Pipelines leveraging from the current [GitLab APIs](#api).
|
||||
|
||||
![Simplified Component Overview](img/bulk_imports_overview_v13_7.png)
|
||||
|
||||
### [ETL](https://www.ibm.com/cloud/learn/etl)
|
||||
|
||||
<!-- Direct quote from the IBM URL link -->
|
||||
|
||||
> ETL, for extract, transform and load, is a data integration process that
|
||||
> combines data from multiple data sources into a single, consistent data store
|
||||
> that is loaded into a data warehouse or other target system.
|
||||
|
||||
Using ETL architecture makes the code more explicit and easier to follow, test and extend. The
|
||||
idea is to have one ETL pipeline for each relation to be imported.
|
||||
|
||||
### API
|
||||
|
||||
The current [Project](../user/project/settings/import_export.md) and [Group](../user/group/settings/import_export.md) Import are file based, so they require an export
|
||||
step to generate the file to be imported.
|
||||
|
||||
GitLab Group migration leverages on [GitLab API](../api/README.md) to speed the migration.
|
||||
|
||||
And, because we're on the road to [GraphQL](../api/README.md#road-to-graphql),
|
||||
GitLab Group Migration will be contributing towards to expand the GraphQL API coverage, which benefits both GitLab
|
||||
and its users.
|
||||
|
||||
### Namespace
|
||||
|
||||
The migration process starts with the creation of a [`BulkImport`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/models/bulk_import.rb)
|
||||
record to keep track of the migration. From there all the code related to the
|
||||
GitLab Group Migration can be found under the new `BulkImports` namespace in all the application layers.
|
|
@ -32,7 +32,8 @@ should be leveraged:
|
|||
requests, you can use the following workflow:
|
||||
|
||||
1. [Create a new feature flag](development.md#create-a-new-feature-flag)
|
||||
which is **off** by default, in the first merge request.
|
||||
which is **off** by default, in the first merge request which uses the flag.
|
||||
Flags [should not be added separately](development.md#risk-of-a-broken-master-main-branch).
|
||||
1. Submit incremental changes via one or more merge requests, ensuring that any
|
||||
new code added can only be reached if the feature flag is **on**.
|
||||
You can keep the feature flag enabled on your local GDK during development.
|
||||
|
|
BIN
doc/development/img/bulk_imports_overview_v13_7.png
Normal file
BIN
doc/development/img/bulk_imports_overview_v13_7.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 104 KiB |
|
@ -161,23 +161,9 @@ gitops:
|
|||
```
|
||||
|
||||
GitLab [versions 13.7 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/259669) also
|
||||
supports manifest projects containing multiple directories (or subdirectories)
|
||||
of YAML files. To use multiple YAML files, specify a `paths` attribute:
|
||||
|
||||
```yaml
|
||||
gitops:
|
||||
manifest_projects:
|
||||
- id: "path-to/your-manifest-project-number1"
|
||||
paths:
|
||||
# Read all .yaml files from team1/app1 directory.
|
||||
# See https://github.com/bmatcuk/doublestar#about and
|
||||
# https://pkg.go.dev/github.com/bmatcuk/doublestar/v2#Match for globbing rules.
|
||||
- glob: '/team1/app1/*.yaml'
|
||||
# Read all .yaml files from team2/apps and all subdirectories
|
||||
- glob: '/team2/apps/**/*.yaml'
|
||||
# If 'paths' is not specified or is an empty list, the configuration below is used
|
||||
- glob: '/**/*.{yaml,yml,json}'
|
||||
```
|
||||
supports manifest projects containing
|
||||
multiple directories (or subdirectories) of YAML files. For more information see our
|
||||
documentation on the [Kubernetes Agent configuration respository](repository.md).
|
||||
|
||||
### Create an Agent record in GitLab
|
||||
|
||||
|
|
93
doc/user/clusters/agent/repository.md
Normal file
93
doc/user/clusters/agent/repository.md
Normal file
|
@ -0,0 +1,93 @@
|
|||
---
|
||||
stage: Configure
|
||||
group: Configure
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
---
|
||||
|
||||
# Kubernetes Agent configuration repository **(PREMIUM ONLY)**
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/259669) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.7.
|
||||
> - It's disabled on GitLab.com. Rolling this feature out to GitLab.com is [planned](https://gitlab.com/groups/gitlab-org/-/epics/3834).
|
||||
|
||||
CAUTION: **Warning:**
|
||||
This feature might not be available to you. Check the **version history** note above for details.
|
||||
|
||||
The [GitLab Kubernetes Agent integration](index.md) supports hosting your configuration for
|
||||
multiple GitLab Kubernetes Agents in a single repository. These agents can be running
|
||||
in the same cluster or in multiple clusters, and potentially with more than one Agent per cluster.
|
||||
|
||||
The Agent bootstraps with the GitLab installation URL and an authentication token,
|
||||
and you provide the rest of the configuration in your repository, following
|
||||
Infrastructure as Code (IaaC) best practices.
|
||||
|
||||
A minimal repository layout looks like this, with `my_agent_1` as the name
|
||||
of your Agent:
|
||||
|
||||
```plaintext
|
||||
|- .gitlab
|
||||
|- agents
|
||||
|- my_agent_1
|
||||
|- config.yaml
|
||||
```
|
||||
|
||||
## Synchronize manifest projects
|
||||
|
||||
Your `config.yaml` file contains a `gitops` section, which contains a `manifest_projects`
|
||||
section. Each `id` in the `manifest_projects` section is the path to a Git repository
|
||||
with Kubernetes resource definitions in YAML or JSON format. The Agent monitors
|
||||
each project you declare, and when the project changes, GitLab deploys the changes
|
||||
using the Agent.
|
||||
|
||||
To use multiple YAML files, specify a `paths` attribute in the `gitops` section.
|
||||
|
||||
By default, the Agent monitors all types of resources. You can exclude some types of resources
|
||||
from monitoring. This enables you to reduce the permissions needed by the GitOps feature,
|
||||
through `resource_exclusions`.
|
||||
|
||||
To enable a specific named resource, first use `resource_inclusions` to enable desired resources.
|
||||
The following file excerpt includes specific `api_groups` and `kinds`. The `resource_exclusions`
|
||||
which follow excludes all other `api_groups` and `kinds`:
|
||||
|
||||
```yaml
|
||||
gitops:
|
||||
# Manifest projects are watched by the agent. Whenever a project changes,
|
||||
# GitLab deploys the changes using the agent.
|
||||
manifest_projects:
|
||||
# No authentication mechanisms are currently supported.
|
||||
# The `id` is a path to a Git repository with Kubernetes resource definitions
|
||||
# in YAML or JSON format.
|
||||
- id: gitlab-org/cluster-integration/gitlab-agent
|
||||
# Holds the only API groups and kinds of resources that gitops will monitor.
|
||||
# Inclusion rules are evaluated first, then exclusion rules.
|
||||
# If there is still no match, resource is monitored.
|
||||
resource_inclusions:
|
||||
- api_groups:
|
||||
- apps
|
||||
kinds:
|
||||
- '*'
|
||||
- api_groups:
|
||||
- ''
|
||||
kinds:
|
||||
- 'ConfigMap'
|
||||
# Holds the API groups and kinds of resources to exclude from gitops watch.
|
||||
# Inclusion rules are evaluated first, then exclusion rules.
|
||||
# If there is still no match, resource is monitored.
|
||||
resource_exclusions:
|
||||
- api_groups:
|
||||
- '*'
|
||||
kinds:
|
||||
- '*'
|
||||
# Namespace to use if not set explicitly in object manifest.
|
||||
default_namespace: my-ns
|
||||
# Paths inside of the repository to scan for manifest files.
|
||||
# Directories with names starting with a dot are ignored.
|
||||
paths:
|
||||
# Read all .yaml files from team1/app1 directory.
|
||||
# See https://github.com/bmatcuk/doublestar#about and
|
||||
# https://pkg.go.dev/github.com/bmatcuk/doublestar/v2#Match for globbing rules.
|
||||
- glob: '/team1/app1/*.yaml'
|
||||
# Read all .yaml files from team2/apps and all subdirectories
|
||||
- glob: '/team2/apps/**/*.yaml'
|
||||
# If 'paths' is not specified or is an empty list, the configuration below is used
|
||||
- glob: '/**/*.{yaml,yml,json}'
|
||||
```
|
|
@ -707,23 +707,23 @@ You can create a new package each time the `master` branch is updated.
|
|||
|
||||
1. Make sure your `pom.xml` file includes the following.
|
||||
You can either let Maven use the CI environment variables, as shown in this example,
|
||||
or you can hard code your project's ID.
|
||||
or you can hard code your server's hostname and project's ID.
|
||||
|
||||
```xml
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>gitlab-maven</id>
|
||||
<url>https://gitlab.example.com/api/v4/projects/${env.CI_PROJECT_ID}/packages/maven</url>
|
||||
<url>${env.CI_SERVER_URL}/api/v4/projects/${env.CI_PROJECT_ID}/packages/maven</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
<distributionManagement>
|
||||
<repository>
|
||||
<id>gitlab-maven</id>
|
||||
<url>https://gitlab.example.com/api/v4/projects/${env.CI_PROJECT_ID}/packages/maven</url>
|
||||
<url>${env.CI_SERVER_URL}/api/v4/projects/${env.CI_PROJECT_ID}/packages/maven</url>
|
||||
</repository>
|
||||
<snapshotRepository>
|
||||
<id>gitlab-maven</id>
|
||||
<url>https://gitlab.example.com/api/v4/projects/${env.CI_PROJECT_ID}/packages/maven</url>
|
||||
<url>${env.CI_SERVER_URL}/api/v4/projects/${env.CI_PROJECT_ID}/packages/maven</url>
|
||||
</snapshotRepository>
|
||||
</distributionManagement>
|
||||
```
|
||||
|
|
|
@ -10,6 +10,7 @@ module API
|
|||
expose :lfs_objects_size
|
||||
expose :build_artifacts_size, as: :job_artifacts_size
|
||||
expose :snippets_size
|
||||
expose :packages_size
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,7 +6,7 @@ module API
|
|||
extend ActiveSupport::Concern
|
||||
extend Grape::API::Helpers
|
||||
|
||||
STATISTICS_SORT_PARAMS = %w[storage_size repository_size wiki_size].freeze
|
||||
STATISTICS_SORT_PARAMS = %w[storage_size repository_size wiki_size packages_size].freeze
|
||||
|
||||
params :optional_project_params_ce do
|
||||
optional :description, type: String, desc: 'The description of the project'
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module WhatsNew
|
||||
CACHE_DURATION = 1.hour
|
||||
WHATS_NEW_FILES_PATH = Rails.root.join('data', 'whats_new', '*.yml')
|
||||
|
||||
private
|
||||
|
||||
def whats_new_release_items(page: 1)
|
||||
Rails.cache.fetch(whats_new_items_cache_key(page), expires_in: CACHE_DURATION) do
|
||||
index = page - 1
|
||||
file_path = whats_new_file_paths[index]
|
||||
|
||||
next if file_path.nil?
|
||||
|
||||
file = File.read(file_path)
|
||||
|
||||
items = YAML.safe_load(file, permitted_classes: [Date])
|
||||
|
||||
items if items.is_a?(Array)
|
||||
end
|
||||
rescue => e
|
||||
Gitlab::ErrorTracking.track_exception(e, page: page)
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
def whats_new_file_paths
|
||||
@whats_new_file_paths ||= Rails.cache.fetch('whats_new:file_paths', expires_in: CACHE_DURATION) do
|
||||
Dir.glob(WHATS_NEW_FILES_PATH).sort.reverse
|
||||
end
|
||||
end
|
||||
|
||||
def whats_new_items_cache_key(page)
|
||||
filename = /\d*\_\d*\_\d*/.match(whats_new_file_paths&.first)
|
||||
"whats_new:release_items:file-#{filename}:page-#{page}"
|
||||
end
|
||||
end
|
||||
end
|
|
@ -44,7 +44,7 @@ RSpec.describe 'Mermaid rendering', :js do
|
|||
expect(page.html.scan(expected).count).to be(4)
|
||||
end
|
||||
|
||||
it 'renders only 2 Mermaid blocks and ', :js, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/234081' } do
|
||||
it 'renders only 2 Mermaid blocks and ', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/234081' do
|
||||
description = <<~MERMAID
|
||||
```mermaid
|
||||
graph LR
|
||||
|
@ -71,7 +71,7 @@ RSpec.describe 'Mermaid rendering', :js do
|
|||
end
|
||||
end
|
||||
|
||||
it 'correctly sizes mermaid diagram inside <details> block', :js do
|
||||
it 'correctly sizes mermaid diagram inside <details> block' do
|
||||
description = <<~MERMAID
|
||||
<details>
|
||||
<summary>Click to show diagram</summary>
|
||||
|
@ -102,7 +102,7 @@ RSpec.describe 'Mermaid rendering', :js do
|
|||
end
|
||||
end
|
||||
|
||||
it 'correctly sizes mermaid diagram block', :js do
|
||||
it 'correctly sizes mermaid diagram block' do
|
||||
description = <<~MERMAID
|
||||
```mermaid
|
||||
graph TD;
|
||||
|
@ -121,7 +121,7 @@ RSpec.describe 'Mermaid rendering', :js do
|
|||
expect(page).to have_css('svg.mermaid[style*="max-width"][width="100%"]')
|
||||
end
|
||||
|
||||
it 'display button when diagram exceeds length', :js do
|
||||
it 'display button when diagram exceeds length', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/287806' do
|
||||
graph_edges = "A-->B;B-->A;" * 420
|
||||
|
||||
description = <<~MERMAID
|
||||
|
|
3
spec/fixtures/whats_new/20201225_01_01.yml
vendored
3
spec/fixtures/whats_new/20201225_01_01.yml
vendored
|
@ -1,2 +1,5 @@
|
|||
---
|
||||
- title: It's gonna be a bright
|
||||
self-managed: true
|
||||
gitlab-com: false
|
||||
packages: ["Premium", "Ultimate"]
|
||||
|
|
3
spec/fixtures/whats_new/20201225_01_02.yml
vendored
3
spec/fixtures/whats_new/20201225_01_02.yml
vendored
|
@ -1,2 +1,5 @@
|
|||
---
|
||||
- title: bright
|
||||
self-managed: true
|
||||
gitlab-com: false
|
||||
packages: ["Premium", "Ultimate"]
|
||||
|
|
7
spec/fixtures/whats_new/20201225_01_05.yml
vendored
7
spec/fixtures/whats_new/20201225_01_05.yml
vendored
|
@ -1,3 +1,10 @@
|
|||
---
|
||||
- title: bright and sunshinin' day
|
||||
self-managed: true
|
||||
gitlab-com: false
|
||||
packages: ["Premium", "Ultimate"]
|
||||
release: '01.05'
|
||||
- title: I think I can make it now the pain is gone
|
||||
self-managed: false
|
||||
gitlab-com: true
|
||||
packages: ["Premium", "Ultimate"]
|
||||
|
|
|
@ -6,6 +6,7 @@ import ConflictsComponent from '~/vue_merge_request_widget/components/states/mr_
|
|||
|
||||
describe('MRWidgetConflicts', () => {
|
||||
let vm;
|
||||
let mergeRequestWidgetGraphql = null;
|
||||
const path = '/conflicts';
|
||||
|
||||
function createComponent(propsData = {}) {
|
||||
|
@ -13,7 +14,35 @@ describe('MRWidgetConflicts', () => {
|
|||
|
||||
vm = shallowMount(localVue.extend(ConflictsComponent), {
|
||||
propsData,
|
||||
provide: {
|
||||
glFeatures: {
|
||||
mergeRequestWidgetGraphql,
|
||||
},
|
||||
},
|
||||
mocks: {
|
||||
$apollo: {
|
||||
queries: {
|
||||
userPermissions: { loading: false },
|
||||
stateData: { loading: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (mergeRequestWidgetGraphql) {
|
||||
vm.setData({
|
||||
userPermissions: {
|
||||
canMerge: propsData.mr.canMerge,
|
||||
pushToSourceBranch: propsData.mr.canPushToSourceBranch,
|
||||
},
|
||||
stateData: {
|
||||
shouldBeRebased: propsData.mr.shouldBeRebased,
|
||||
sourceBranchProtected: propsData.mr.sourceBranchProtected,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return vm.vm.$nextTick();
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -21,9 +50,16 @@ describe('MRWidgetConflicts', () => {
|
|||
});
|
||||
|
||||
afterEach(() => {
|
||||
mergeRequestWidgetGraphql = null;
|
||||
vm.destroy();
|
||||
});
|
||||
|
||||
[false, true].forEach(featureEnabled => {
|
||||
describe(`with GraphQL feature flag ${featureEnabled ? 'enabled' : 'disabled'}`, () => {
|
||||
beforeEach(() => {
|
||||
mergeRequestWidgetGraphql = featureEnabled;
|
||||
});
|
||||
|
||||
// There are two permissions we need to consider:
|
||||
//
|
||||
// 1. Is the user allowed to merge to the target branch?
|
||||
|
@ -34,8 +70,8 @@ describe('MRWidgetConflicts', () => {
|
|||
// branch should be allowed to resolve conflicts. This is
|
||||
// consistent with what the backend does.
|
||||
describe('when allowed to merge but not allowed to push to source branch', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
beforeEach(async () => {
|
||||
await createComponent({
|
||||
mr: {
|
||||
canMerge: true,
|
||||
canPushToSourceBranch: false,
|
||||
|
@ -62,8 +98,8 @@ describe('MRWidgetConflicts', () => {
|
|||
});
|
||||
|
||||
describe('when not allowed to merge but allowed to push to source branch', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
beforeEach(async () => {
|
||||
await createComponent({
|
||||
mr: {
|
||||
canMerge: false,
|
||||
canPushToSourceBranch: true,
|
||||
|
@ -91,8 +127,8 @@ describe('MRWidgetConflicts', () => {
|
|||
});
|
||||
|
||||
describe('when allowed to merge and push to source branch', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
beforeEach(async () => {
|
||||
await createComponent({
|
||||
mr: {
|
||||
canMerge: true,
|
||||
canPushToSourceBranch: true,
|
||||
|
@ -122,8 +158,8 @@ describe('MRWidgetConflicts', () => {
|
|||
});
|
||||
|
||||
describe('when user does not have permission to push to source branch', () => {
|
||||
it('should show proper message', () => {
|
||||
createComponent({
|
||||
it('should show proper message', async () => {
|
||||
await createComponent({
|
||||
mr: {
|
||||
canMerge: false,
|
||||
canPushToSourceBranch: false,
|
||||
|
@ -139,8 +175,8 @@ describe('MRWidgetConflicts', () => {
|
|||
).toContain('ask someone with write access');
|
||||
});
|
||||
|
||||
it('should not have action buttons', () => {
|
||||
createComponent({
|
||||
it('should not have action buttons', async () => {
|
||||
await createComponent({
|
||||
mr: {
|
||||
canMerge: false,
|
||||
canPushToSourceBranch: false,
|
||||
|
@ -152,8 +188,8 @@ describe('MRWidgetConflicts', () => {
|
|||
expect(vm.find('.js-merge-locally-button').exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('should not have resolve button when no conflict resolution path', () => {
|
||||
createComponent({
|
||||
it('should not have resolve button when no conflict resolution path', async () => {
|
||||
await createComponent({
|
||||
mr: {
|
||||
canMerge: true,
|
||||
conflictResolutionPath: null,
|
||||
|
@ -166,8 +202,8 @@ describe('MRWidgetConflicts', () => {
|
|||
});
|
||||
|
||||
describe('when fast-forward or semi-linear merge enabled', () => {
|
||||
it('should tell you to rebase locally', () => {
|
||||
createComponent({
|
||||
it('should tell you to rebase locally', async () => {
|
||||
await createComponent({
|
||||
mr: {
|
||||
shouldBeRebased: true,
|
||||
conflictsDocsPath: '',
|
||||
|
@ -181,8 +217,8 @@ describe('MRWidgetConflicts', () => {
|
|||
});
|
||||
|
||||
describe('when source branch protected', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
beforeEach(async () => {
|
||||
await createComponent({
|
||||
mr: {
|
||||
canMerge: true,
|
||||
canPushToSourceBranch: true,
|
||||
|
@ -203,8 +239,8 @@ describe('MRWidgetConflicts', () => {
|
|||
});
|
||||
|
||||
describe('when source branch not protected', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
beforeEach(async () => {
|
||||
await createComponent({
|
||||
mr: {
|
||||
canMerge: true,
|
||||
canPushToSourceBranch: true,
|
||||
|
@ -223,4 +259,6 @@ describe('MRWidgetConflicts', () => {
|
|||
expect($.fn.popover).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -27,7 +27,7 @@ RSpec.describe GitlabSchema.types['MergeRequest'] do
|
|||
upvotes downvotes head_pipeline pipelines task_completion_status
|
||||
milestone assignees participants subscribed labels discussion_locked time_estimate
|
||||
total_time_spent reference author merged_at commit_count current_user_todos
|
||||
conflicts auto_merge_enabled approved_by
|
||||
conflicts auto_merge_enabled approved_by source_branch_protected
|
||||
]
|
||||
|
||||
if Gitlab.ee?
|
||||
|
|
|
@ -3,22 +3,22 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe WhatsNewHelper do
|
||||
let(:fixture_dir_glob) { Dir.glob(File.join('spec', 'fixtures', 'whats_new', '*.yml')) }
|
||||
|
||||
describe '#whats_new_storage_key' do
|
||||
subject { helper.whats_new_storage_key }
|
||||
|
||||
context 'when version exist' do
|
||||
let(:release_item) { double(:item) }
|
||||
|
||||
before do
|
||||
allow(Dir).to receive(:glob).with(Rails.root.join('data', 'whats_new', '*.yml')).and_return(fixture_dir_glob)
|
||||
allow(ReleaseHighlight).to receive(:most_recent_version).and_return(84.0)
|
||||
end
|
||||
|
||||
it { is_expected.to eq('display-whats-new-notification-01.05') }
|
||||
it { is_expected.to eq('display-whats-new-notification-84.0') }
|
||||
end
|
||||
|
||||
context 'when recent release items do NOT exist' do
|
||||
context 'when most recent release highlights do NOT exist' do
|
||||
before do
|
||||
allow(helper).to receive(:whats_new_release_items).and_return(nil)
|
||||
allow(ReleaseHighlight).to receive(:most_recent_version).and_return(nil)
|
||||
end
|
||||
|
||||
it { is_expected.to be_nil }
|
||||
|
@ -30,31 +30,18 @@ RSpec.describe WhatsNewHelper do
|
|||
|
||||
context 'when recent release items exist' do
|
||||
it 'returns the count from the most recent file' do
|
||||
expect(Dir).to receive(:glob).with(Rails.root.join('data', 'whats_new', '*.yml')).and_return(fixture_dir_glob)
|
||||
allow(ReleaseHighlight).to receive(:most_recent_item_count).and_return(1)
|
||||
|
||||
expect(subject).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when recent release items do NOT exist' do
|
||||
before do
|
||||
allow(YAML).to receive(:safe_load).and_raise
|
||||
it 'returns nil' do
|
||||
allow(ReleaseHighlight).to receive(:most_recent_item_count).and_return(nil)
|
||||
|
||||
expect(Gitlab::ErrorTracking).to receive(:track_exception)
|
||||
end
|
||||
|
||||
it 'fails gracefully and logs an error' do
|
||||
expect(subject).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Testing this important private method here because the request spec required multiple confusing mocks and felt wrong and overcomplicated
|
||||
describe '#whats_new_items_cache_key' do
|
||||
it 'returns a key containing the most recent file name and page parameter' do
|
||||
allow(Dir).to receive(:glob).with(Rails.root.join('data', 'whats_new', '*.yml')).and_return(fixture_dir_glob)
|
||||
|
||||
expect(helper.send(:whats_new_items_cache_key, 2)).to eq('whats_new:release_items:file-20201225_01_05:page-2')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
135
spec/models/release_highlight_spec.rb
Normal file
135
spec/models/release_highlight_spec.rb
Normal file
|
@ -0,0 +1,135 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe ReleaseHighlight do
|
||||
describe '#paginated' do
|
||||
let(:fixture_dir_glob) { Dir.glob(File.join('spec', 'fixtures', 'whats_new', '*.yml')) }
|
||||
let(:cache_mock) { double(:cache_mock) }
|
||||
let(:dot_com) { false }
|
||||
|
||||
before do
|
||||
allow(Gitlab).to receive(:com?).and_return(dot_com)
|
||||
allow(Dir).to receive(:glob).with(Rails.root.join('data', 'whats_new', '*.yml')).and_return(fixture_dir_glob)
|
||||
|
||||
expect(Rails).to receive(:cache).twice.and_return(cache_mock)
|
||||
expect(cache_mock).to receive(:fetch).with('release_highlight:file_paths', expires_in: 1.hour).and_yield
|
||||
end
|
||||
|
||||
after do
|
||||
ReleaseHighlight.instance_variable_set(:@file_paths, nil)
|
||||
end
|
||||
|
||||
context 'with page param' do
|
||||
subject { ReleaseHighlight.paginated(page: page) }
|
||||
|
||||
before do
|
||||
allow(cache_mock).to receive(:fetch).and_yield
|
||||
end
|
||||
|
||||
context 'when there is another page of results' do
|
||||
let(:page) { 2 }
|
||||
|
||||
it 'responds with paginated results' do
|
||||
expect(subject[:items].first['title']).to eq('bright')
|
||||
expect(subject[:next_page]).to eq(3)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is NOT another page of results' do
|
||||
let(:page) { 3 }
|
||||
|
||||
it 'responds with paginated results and no next_page' do
|
||||
expect(subject[:items].first['title']).to eq("It's gonna be a bright")
|
||||
expect(subject[:next_page]).to eq(nil)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when that specific page does not exist' do
|
||||
let(:page) { 84 }
|
||||
|
||||
it 'returns nil' do
|
||||
expect(subject).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with no page param' do
|
||||
subject { ReleaseHighlight.paginated }
|
||||
|
||||
before do
|
||||
expect(cache_mock).to receive(:fetch).with('release_highlight:items:file-20201225_01_05:page-1', expires_in: 1.hour).and_yield
|
||||
end
|
||||
|
||||
it 'returns platform specific items and uses a cache key' do
|
||||
expect(subject[:items].count).to eq(1)
|
||||
expect(subject[:items].first['title']).to eq("bright and sunshinin' day")
|
||||
expect(subject[:next_page]).to eq(2)
|
||||
end
|
||||
|
||||
context 'when Gitlab.com' do
|
||||
let(:dot_com) { true }
|
||||
|
||||
it 'responds with a different set of data' do
|
||||
expect(subject[:items].count).to eq(1)
|
||||
expect(subject[:items].first['title']).to eq("I think I can make it now the pain is gone")
|
||||
end
|
||||
end
|
||||
|
||||
context 'when recent release items do NOT exist' do
|
||||
before do
|
||||
allow(YAML).to receive(:safe_load).and_raise(Psych::Exception)
|
||||
|
||||
expect(Gitlab::ErrorTracking).to receive(:track_exception)
|
||||
end
|
||||
|
||||
it 'fails gracefully and logs an error' do
|
||||
expect(subject).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.most_recent_version' do
|
||||
subject { ReleaseHighlight.most_recent_version }
|
||||
|
||||
context 'when version exist' do
|
||||
let(:release_item) { double(:item) }
|
||||
|
||||
before do
|
||||
allow(ReleaseHighlight).to receive(:paginated).and_return({ items: [release_item] })
|
||||
allow(release_item).to receive(:[]).with('release').and_return(84.0)
|
||||
end
|
||||
|
||||
it { is_expected.to eq(84.0) }
|
||||
end
|
||||
|
||||
context 'when most recent release highlights do NOT exist' do
|
||||
before do
|
||||
allow(ReleaseHighlight).to receive(:paginated).and_return(nil)
|
||||
end
|
||||
|
||||
it { is_expected.to be_nil }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#most_recent_item_count' do
|
||||
subject { ReleaseHighlight.most_recent_item_count }
|
||||
|
||||
context 'when recent release items exist' do
|
||||
it 'returns the count from the most recent file' do
|
||||
allow(ReleaseHighlight).to receive(:paginated).and_return({ items: [double(:item)] })
|
||||
|
||||
expect(subject).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when recent release items do NOT exist' do
|
||||
it 'returns nil' do
|
||||
allow(ReleaseHighlight).to receive(:paginated).and_return(nil)
|
||||
|
||||
expect(subject).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
29
spec/presenters/gitlab/whats_new/item_presenter_spec.rb
Normal file
29
spec/presenters/gitlab/whats_new/item_presenter_spec.rb
Normal file
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::WhatsNew::ItemPresenter do
|
||||
let(:present) { Gitlab::WhatsNew::ItemPresenter.present(item) }
|
||||
let(:item) { { "packages" => %w(Premium Ultimate) } }
|
||||
let(:gitlab_com) { true }
|
||||
|
||||
before do
|
||||
allow(Gitlab).to receive(:com?).and_return(gitlab_com)
|
||||
end
|
||||
|
||||
describe '.present' do
|
||||
context 'when on Gitlab.com' do
|
||||
it 'transforms package names to gitlab.com friendly package names' do
|
||||
expect(present).to eq({ "packages" => %w(Silver Gold) })
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not on Gitlab.com' do
|
||||
let(:gitlab_com) { false }
|
||||
|
||||
it 'does not transform package names' do
|
||||
expect(present).to eq({ "packages" => %w(Premium Ultimate) })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -254,7 +254,7 @@ RSpec.describe API::Projects do
|
|||
|
||||
statistics = json_response.find { |p| p['id'] == project.id }['statistics']
|
||||
expect(statistics).to be_present
|
||||
expect(statistics).to include('commit_count', 'storage_size', 'repository_size', 'wiki_size', 'lfs_objects_size', 'job_artifacts_size', 'snippets_size')
|
||||
expect(statistics).to include('commit_count', 'storage_size', 'repository_size', 'wiki_size', 'lfs_objects_size', 'job_artifacts_size', 'snippets_size', 'packages_size')
|
||||
end
|
||||
|
||||
it "does not include license by default" do
|
||||
|
@ -619,7 +619,7 @@ RSpec.describe API::Projects do
|
|||
end
|
||||
|
||||
context 'sorting by project statistics' do
|
||||
%w(repository_size storage_size wiki_size).each do |order_by|
|
||||
%w(repository_size storage_size wiki_size packages_size).each do |order_by|
|
||||
context "sorting by #{order_by}" do
|
||||
before do
|
||||
ProjectStatistics.update_all(order_by => 100)
|
||||
|
|
|
@ -5,28 +5,30 @@ require 'spec_helper'
|
|||
RSpec.describe WhatsNewController do
|
||||
describe 'whats_new_path' do
|
||||
context 'with whats_new_drawer feature enabled' do
|
||||
let(:fixture_dir_glob) { Dir.glob(File.join('spec', 'fixtures', 'whats_new', '*.yml')) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(whats_new_drawer: true)
|
||||
allow(Dir).to receive(:glob).with(Rails.root.join('data', 'whats_new', '*.yml')).and_return(fixture_dir_glob)
|
||||
end
|
||||
|
||||
context 'with no page param' do
|
||||
let(:most_recent) { { items: [item], next_page: 2 } }
|
||||
let(:item) { double(:item) }
|
||||
|
||||
it 'responds with paginated data and headers' do
|
||||
allow(ReleaseHighlight).to receive(:paginated).with(page: 1).and_return(most_recent)
|
||||
allow(Gitlab::WhatsNew::ItemPresenter).to receive(:present).with(item).and_return(item)
|
||||
|
||||
get whats_new_path, xhr: true
|
||||
|
||||
expect(response.body).to eq([{ title: "bright and sunshinin' day", release: "01.05" }].to_json)
|
||||
expect(response.body).to eq(most_recent[:items].to_json)
|
||||
expect(response.headers['X-Next-Page']).to eq(2)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with page param' do
|
||||
it 'responds with paginated data and headers' do
|
||||
get whats_new_path(page: 2), xhr: true
|
||||
it 'passes the page parameter' do
|
||||
expect(ReleaseHighlight).to receive(:paginated).with(page: 2).and_call_original
|
||||
|
||||
expect(response.body).to eq([{ title: 'bright' }].to_json)
|
||||
expect(response.headers['X-Next-Page']).to eq(3)
|
||||
get whats_new_path(page: 2), xhr: true
|
||||
end
|
||||
|
||||
it 'returns a 404 if page param is negative' do
|
||||
|
@ -34,14 +36,6 @@ RSpec.describe WhatsNewController do
|
|||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
|
||||
context 'when there are no more paginated results' do
|
||||
it 'responds with nil X-Next-Page header' do
|
||||
get whats_new_path(page: 3), xhr: true
|
||||
expect(response.body).to eq([{ title: "It's gonna be a bright" }].to_json)
|
||||
expect(response.headers['X-Next-Page']).to be nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in a new issue