Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-10-27 18:08:59 +00:00
parent 2b1e7f7dac
commit f3b9e205bb
39 changed files with 690 additions and 571 deletions

View File

@ -15,7 +15,7 @@ code_quality:
stage: test
needs: []
variables:
CODE_QUALITY_IMAGE: "registry.gitlab.com/gitlab-org/ci-cd/codequality:0.85.10-gitlab.1"
CODE_QUALITY_IMAGE: "registry.gitlab.com/gitlab-org/ci-cd/codequality:0.85.18"
script:
- |
if ! docker info &>/dev/null; then

View File

@ -14,6 +14,7 @@ tasks:
set -e
cd /workspace/gitlab-development-kit
[[ ! -L /workspace/gitlab-development-kit/gitlab ]] && ln -fs /workspace/gitlab /workspace/gitlab-development-kit/gitlab
mv /workspace/gitlab-development-kit/secrets.yml /workspace/gitlab-development-kit/gitlab/config
# make webpack static, prevents that GitLab tries to connect to localhost webpack from browser outside the workspace
echo "webpack:" >> gdk.yml
echo " static: true" >> gdk.yml
@ -48,14 +49,6 @@ tasks:
if [ "$GITLAB_RUN_DB_MIGRATIONS" == true ]; then
make gitlab-db-migrate
fi
# Fix DB key
if [ "$GITLAB_FIX_DB_KEY" = true ]; then
echo "$(date) Fixing DB key" | tee -a /workspace/startup.log
cd gitlab
# see https://gitlab.com/gitlab-org/gitlab-foss/-/issues/56403#note_132515069
printf 'ApplicationSetting.last.update_column(:runners_registration_token_encrypted, nil)\nexit\n' | bundle exec rails c
cd -
fi
# Waiting for GitLab ...
gp await-port 3000
printf "Waiting for GitLab at $(gp url 3000) ..."

View File

@ -5,11 +5,11 @@ import { uniq } from 'lodash';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import Cookies from 'js-cookie';
import { __ } from './locale';
import { updateTooltipTitle } from './lib/utils/common_utils';
import { isInVueNoteablePage } from './lib/utils/dom_utils';
import { deprecatedCreateFlash as flash } from './flash';
import axios from './lib/utils/axios_utils';
import * as Emoji from '~/emoji';
import { dispose, fixTitle } from '~/tooltips';
const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd';
const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd';
@ -374,7 +374,7 @@ export class AwardsHandler {
counter.text(counterNumber - 1);
this.removeYouFromUserList($emojiButton);
} else if (emoji === 'thumbsup' || emoji === 'thumbsdown') {
$emojiButton.tooltip('dispose');
dispose($emojiButton);
counter.text('0');
this.removeYouFromUserList($emojiButton);
if ($emojiButton.parents('.note').length) {
@ -387,7 +387,8 @@ export class AwardsHandler {
}
removeEmoji($emojiButton) {
$emojiButton.tooltip('dispose');
dispose($emojiButton);
$emojiButton.remove();
const $votesBlock = this.getVotesBlock();
if ($votesBlock.find('.js-emoji-btn').length === 0) {
@ -415,13 +416,17 @@ export class AwardsHandler {
const originalTitle = this.getAwardTooltip(awardBlock);
const authors = originalTitle.split(FROM_SENTENCE_REGEX);
authors.splice(authors.indexOf('You'), 1);
return awardBlock
awardBlock
.closest('.js-emoji-btn')
.removeData('title')
.removeAttr('data-title')
.removeAttr('data-original-title')
.attr('title', this.toSentence(authors))
.tooltip('_fixTitle');
.attr('title', this.toSentence(authors));
fixTitle(awardBlock);
return awardBlock;
}
addYouToUserList(votesBlock, emoji) {
@ -432,7 +437,12 @@ export class AwardsHandler {
users = origTitle.trim().split(FROM_SENTENCE_REGEX);
}
users.unshift('You');
return awardBlock.attr('title', this.toSentence(users)).tooltip('_fixTitle');
awardBlock.attr('title', this.toSentence(users));
fixTitle(awardBlock);
return awardBlock;
}
createAwardButtonForVotesBlock(votesBlock, emojiName) {
@ -448,7 +458,7 @@ export class AwardsHandler {
.find('.emoji-icon')
.data('name', emojiName);
this.animateEmoji($emojiButton);
$('.award-control').tooltip();
votesBlock.removeClass('current');
}
@ -487,17 +497,6 @@ export class AwardsHandler {
return votesBlock.find(`.js-emoji-btn [data-name="${emoji}"]`);
}
userAuthored($emojiButton) {
const oldTitle = this.getAwardTooltip($emojiButton);
const newTitle = 'You cannot vote on your own issue, MR and note';
updateTooltipTitle($emojiButton, newTitle).tooltip('show');
// Restore tooltip back to award list
return setTimeout(() => {
$emojiButton.tooltip('hide');
updateTooltipTitle($emojiButton, oldTitle);
}, 2800);
}
scrollToAwards() {
const options = {
scrollTop: $('.awards').offset().top - 110,

View File

@ -61,9 +61,6 @@ export const rstrip = val => {
return val;
};
export const updateTooltipTitle = ($tooltipEl, newTitle) =>
$tooltipEl.attr('title', newTitle).tooltip('_fixTitle');
export const disableButtonIfEmptyField = (fieldSelector, buttonSelector, eventName = 'input') => {
const field = $(fieldSelector);
const closestSubmit = field.closest('form').find(buttonSelector);

View File

@ -6,6 +6,13 @@ class Projects::StaticSiteEditorController < Projects::ApplicationController
layout 'fullscreen'
content_security_policy do |policy|
next if policy.directives.blank?
frame_src_values = Array.wrap(policy.directives['frame-src']) | ['https://www.youtube.com']
policy.frame_src(*frame_src_values)
end
prepend_before_action :authenticate_user!, only: [:show]
before_action :assign_ref_and_path, only: [:show]
before_action :authorize_edit_tree!, only: [:show]

View File

@ -377,7 +377,12 @@ module IssuablesHelper
end
def issuable_display_type(issuable)
issuable.model_name.human.downcase
case issuable
when Issue
issuable.issue_type.downcase
when MergeRequest
issuable.model_name.human.downcase
end
end
def has_filter_bar_param?

View File

@ -293,9 +293,7 @@ class Snippet < ApplicationRecord
@storage ||= Storage::Hashed.new(self, prefix: Storage::Hashed::SNIPPET_REPOSITORY_PATH_PREFIX)
end
# This is the full_path used to identify the
# the snippet repository. It will be used mostly
# for logging purposes.
# This is the full_path used to identify the the snippet repository.
override :full_path
def full_path
return unless persisted?
@ -303,7 +301,7 @@ class Snippet < ApplicationRecord
@full_path ||= begin
components = []
components << project.full_path if project_id?
components << '@snippets'
components << 'snippets'
components << self.id
components.join('/')
end

View File

@ -1 +1,6 @@
= render template: 'projects/issues/show'
- @content_class = "limit-container-width" unless fluid_layout
- add_to_breadcrumbs _("Incidents"), project_incidents_path(@project)
- breadcrumb_title @issue.to_reference
- page_title "#{@issue.title} (#{@issue.to_reference})", _("Incidents")
= render 'projects/issuable/show', issuable: @issue

View File

@ -0,0 +1,10 @@
- page_description issuable.description_html
- page_card_attributes issuable.card_attributes
- if issuable.relocation_target
- page_canonical_link issuable.relocation_target.present(current_user: current_user).web_url
= render_if_exists "projects/issues/alert_blocked", issue: issuable, current_user: current_user
= render "projects/issues/alert_moved_from_service_desk", issue: issuable
= render 'shared/issue_type/details_header', issuable: issuable
= render 'shared/issue_type/details_content', issuable: issuable

View File

@ -1,103 +1,6 @@
- @content_class = "limit-container-width" unless fluid_layout
- add_to_breadcrumbs _("Issues"), project_issues_path(@project)
- breadcrumb_title @issue.to_reference
- page_title "#{@issue.title} (#{@issue.to_reference})", _("Issues")
- page_description @issue.description_html
- page_card_attributes @issue.card_attributes
- if @issue.relocation_target
- page_canonical_link @issue.relocation_target.present(current_user: current_user).web_url
- if @issue.sentry_issue.present?
- add_page_specific_style 'page_bundles/error_tracking_details'
- page_title "#{@issue.title} (#{@issue.to_reference})", _("Issues")
- can_update_issue = can?(current_user, :update_issue, @issue)
- can_reopen_issue = can?(current_user, :reopen_issue, @issue)
- can_report_spam = @issue.submittable_as_spam_by?(current_user)
- can_create_issue = show_new_issue_link?(@project)
- related_branches_path = related_branches_project_issue_path(@project, @issue)
= render_if_exists "projects/issues/alert_blocked", issue: @issue, current_user: current_user
= render "projects/issues/alert_moved_from_service_desk", issue: @issue
.detail-page-header
.detail-page-header-body
.issuable-status-box.status-box.status-box-issue-closed{ class: issue_status_visibility(@issue, status_box: :closed) }
= sprite_icon('mobile-issue-close', css_class: 'd-block d-sm-none')
.d-none.d-sm-block
= issue_closed_text(@issue, current_user)
.issuable-status-box.status-box.status-box-open{ class: issue_status_visibility(@issue, status_box: :open) }
= sprite_icon('issue-open-m', css_class: 'd-block d-sm-none')
%span.d-none.d-sm-block Open
.issuable-meta
#js-issuable-header-warnings
= issuable_meta(@issue, @project, "Issue")
%a.btn.btn-default.float-right.d-block.d-sm-none.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" }
= sprite_icon('chevron-double-lg-left')
.detail-page-header-actions.js-issuable-actions.js-issuable-buttons{ data: { "action": "close-reopen" } }
.clearfix.issue-btn-group.dropdown
%button.btn.btn-default.float-left.d-md-none{ type: "button", data: { toggle: "dropdown" } }
Options
= icon('caret-down')
.dropdown-menu.dropdown-menu-right.d-lg-none
%ul
- unless current_user == @issue.author
%li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue))
- if can_update_issue
%li= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), class: "btn-close js-btn-issue-action #{issue_button_visibility(@issue, true)}", title: 'Close issue'
- if can_reopen_issue
%li= link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), class: "btn-reopen js-btn-issue-action #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
- if can_report_spam
%li= link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam'
- if can_create_issue
- if can_update_issue || can_report_spam
%li.divider
%li= link_to 'New issue', new_project_issue_path(@project), id: 'new_issue_link'
= render 'shared/issuable/close_reopen_button', issuable: @issue, can_update: can_update_issue, can_reopen: can_reopen_issue, warn_before_close: defined?(@issue.blocked?) && @issue.blocked?
- if can_report_spam
= link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'd-none d-md-block gl-button btn btn-grouped btn-spam', title: 'Submit as spam'
- if can_create_issue
= link_to new_project_issue_path(@project), class: 'd-none d-md-block gl-button btn btn-grouped btn-success btn-inverted', title: 'New issue', id: 'new_issue_link' do
New issue
.issue-details.issuable-details
.detail-page-description.content-block
#js-issuable-app{ data: { initial: issuable_initial_data(@issue).to_json} }
.title-container
%h2.title= markdown_field(@issue, :title)
- if @issue.description.present?
.description
.md= markdown_field(@issue, :description)
= edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue-edited-ago js-issue-edited-ago')
- if @issue.sentry_issue.present?
#js-sentry-error-stack-trace{ data: error_details_data(@project, @issue.sentry_issue.sentry_issue_identifier) }
= render 'projects/issues/design_management'
= render_if_exists 'projects/issues/related_issues'
#js-related-merge-requests{ data: { endpoint: expose_path(api_v4_projects_issues_related_merge_requests_path(id: @project.id, issue_iid: @issue.iid)), project_namespace: @project.namespace.path, project_path: @project.path } }
- if can?(current_user, :download_code, @project)
- add_page_startup_api_call related_branches_path
#related-branches{ data: { url: related_branches_path } }
-# This element is filled in using JavaScript.
.content-block.emoji-block.emoji-block-sticky
.row.gl-m-0.gl-justify-content-space-between
.js-noteable-awards
= render 'award_emoji/awards_block', awardable: @issue, inline: true
.new-branch-col
= render_if_exists "projects/issues/timeline_toggle", issue: @issue
#js-vue-sort-issue-discussions
#js-vue-discussion-filter{ data: { default_filter: current_user&.notes_filter_for(@issue), notes_filters: UserPreference.notes_filters.to_json } }
= render 'new_branch' if show_new_branch_button?
= render 'projects/issues/discussion'
= render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @issue.assignees
= render 'projects/issuable/show', issuable: @issue

View File

@ -0,0 +1,31 @@
- related_branches_path = related_branches_project_issue_path(@project, issuable)
.issue-details.issuable-details
.detail-page-description.content-block
#js-issuable-app{ data: { initial: issuable_initial_data(issuable).to_json} }
.title-container
%h2.title= markdown_field(issuable, :title)
- if issuable.description.present?
.description
.md= markdown_field(issuable, :description)
= edited_time_ago_with_tooltip(issuable, placement: 'bottom', html_class: 'issue-edited-ago js-issue-edited-ago')
= render 'shared/issue_type/sentry_stack_trace', issuable: issuable
= render 'projects/issues/design_management'
= render_if_exists 'projects/issues/related_issues'
#js-related-merge-requests{ data: { endpoint: expose_path(api_v4_projects_issues_related_merge_requests_path(id: @project.id, issue_iid: issuable.iid)), project_namespace: @project.namespace.path, project_path: @project.path } }
- if can?(current_user, :download_code, @project)
- add_page_startup_api_call related_branches_path
#related-branches{ data: { url: related_branches_path } }
-# This element is filled in using JavaScript.
= render 'shared/issue_type/emoji_block', issuable: issuable
= render 'projects/issues/discussion'
= render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @issue.assignees

View File

@ -0,0 +1,52 @@
- can_update_issue = can?(current_user, :update_issue, issuable)
- can_reopen_issue = can?(current_user, :reopen_issue, issuable)
- can_report_spam = issuable.submittable_as_spam_by?(current_user)
- can_create_issue = show_new_issue_link?(@project)
- display_issuable_type = issuable_display_type(issuable)
- new_issuable_params = ({ issuable_template: 'incident', issue: { issue_type: 'incident' } } if issuable.incident?)
.detail-page-header
.detail-page-header-body
.issuable-status-box.status-box.status-box-issue-closed{ class: issue_status_visibility(issuable, status_box: :closed) }
= sprite_icon('mobile-issue-close', css_class: 'gl-display-block gl-display-sm-none!')
.gl-display-none.gl-display-sm-block!
= issue_closed_text(issuable, current_user)
.issuable-status-box.status-box.status-box-open{ class: issue_status_visibility(issuable, status_box: :open) }
= sprite_icon('issue-open-m', css_class: 'gl-display-block gl-display-sm-none!')
%span.gl-display-none.gl-display-sm-block!
= _('Open')
.issuable-meta
#js-issuable-header-warnings
= issuable_meta(issuable, @project, display_issuable_type)
%a.btn.gl-button.btn-default.float-right.gl-display-block.d-sm-none.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" }
= sprite_icon('chevron-double-lg-left')
.detail-page-header-actions.js-issuable-actions.js-issuable-buttons{ data: { "action": "close-reopen" } }
.clearfix.issue-btn-group.dropdown
%button.btn.gl-button.btn-default.float-left.d-md-none.d-lg-none.d-xl-none{ type: "button", data: { toggle: "dropdown" } }
= _('Options')
= icon('caret-down')
.dropdown-menu.dropdown-menu-right.d-lg-none.d-xl-none
%ul
- unless current_user == issuable.author
%li= link_to _('Report abuse'), new_abuse_report_path(user_id: issuable.author.id, ref_url: issue_url(issuable))
- if can_update_issue
%li= link_to _('Close %{display_issuable_type}') % { display_issuable_type: display_issuable_type }, issue_path(issuable, issue: { state_event: :close }, format: 'json'), class: "btn-close js-btn-issue-action #{issue_button_visibility(issuable, true)}", title: _('Close %{display_issuable_type}') % { display_issuable_type: display_issuable_type }
- if can_reopen_issue
%li= link_to _('Reopen %{display_issuable_type}') % { display_issuable_type: display_issuable_type }, issue_path(issuable, issue: { state_event: :reopen }, format: 'json'), class: "btn-reopen js-btn-issue-action #{issue_button_visibility(issuable, false)}", title: _('Reopen %{display_issuable_type}') % { display_issuable_type: display_issuable_type }
- if can_report_spam
%li= link_to _('Submit as spam'), mark_as_spam_project_issue_path(@project, issuable), method: :post, class: 'btn-spam', title: 'Submit as spam'
- if can_create_issue
- if can_update_issue || can_report_spam
%li.divider
%li= link_to _('New %{display_issuable_type}') % { display_issuable_type: display_issuable_type }, new_project_issue_path(@project, new_issuable_params), id: 'new_%{display_issuable_type}_link' % { display_issuable_type: display_issuable_type }
= render 'shared/issuable/close_reopen_button', issuable: issuable, can_update: can_update_issue, can_reopen: can_reopen_issue, warn_before_close: defined?(issuable.blocked?) && issuable.blocked?
- if can_report_spam
= link_to _('Submit as spam'), mark_as_spam_project_issue_path(@project, issuable), method: :post, class: 'gl-display-none gl-display-sm-none gl-display-md-block gl-button btn btn-grouped btn-spam', title: 'Submit as spam'
- if can_create_issue
= link_to new_project_issue_path(@project, new_issuable_params), class: 'gl-display-none gl-display-sm-none gl-display-md-block gl-button btn btn-grouped btn-success btn-inverted', title: _('New %{display_issuable_type}') % { display_issuable_type: display_issuable_type }, id: 'new_%{display_issuable_type}_link' % { display_issuable_type: display_issuable_type } do
= _('New %{display_issuable_type}') % { display_issuable_type: display_issuable_type }

View File

@ -0,0 +1,9 @@
.content-block.emoji-block.emoji-block-sticky
.row.gl-m-0.gl-justify-content-space-between
.js-noteable-awards
= render 'award_emoji/awards_block', awardable: issuable, inline: true
.new-branch-col
= render_if_exists "projects/issues/timeline_toggle", issuable: issuable
#js-vue-sort-issue-discussions
#js-vue-discussion-filter{ data: { default_filter: current_user&.notes_filter_for(issuable), notes_filters: UserPreference.notes_filters.to_json } }
= render 'new_branch' if show_new_branch_button?

View File

@ -0,0 +1,4 @@
- return unless issuable.sentry_issue.present?
- add_page_specific_style 'page_bundles/error_tracking_details'
#js-sentry-error-stack-trace{ data: error_details_data(@project, issuable.sentry_issue.sentry_issue_identifier) }

View File

@ -0,0 +1,5 @@
---
title: Configure CSP for displaying Youtube videos in the Static Site Editor
merge_request: 45767
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Use CodeQuality 0.85.18 in the CI template
merge_request: 46253
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Add environment variables to override backup/restore DB settings
merge_request: 45855
author:
type: added

View File

@ -1,7 +0,0 @@
---
name: oj_json
introduced_by_url:
rollout_issue_url:
group:
type: development
default_enabled: true

View File

@ -3,7 +3,7 @@ comments: false
description: 'Learn how to use and administer GitLab, the most scalable Git-based fully integrated platform for software development.'
---
<div class="display-none">
<div class="d-none">
<em>Visit <a href="https://docs.gitlab.com/ee/">docs.gitlab.com</a> for optimized
navigation, discoverability, and readability.</em>
</div>

View File

@ -96,7 +96,7 @@ POST /projects/:id/protected_environments
```
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/protected_environments?name=staging&deploy_access_levels%5B%5D%5Buser_id%5D=1"
curl --header 'Content-Type: application/json' --request POST --data '{"name": "production", "deploy_access_levels": [{"group_id": 9899826}]}' --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/22034114/protected_environments"
```
| Attribute | Type | Required | Description |
@ -105,21 +105,22 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitla
| `name` | string | yes | The name of the environment. |
| `deploy_access_levels` | array | yes | Array of access levels allowed to deploy, with each described by a hash. |
Elements in the `deploy_access_levels` array should take the
form `{user_id: integer}`, `{group_id: integer}` or `{access_level: integer}`.
Elements in the `deploy_access_levels` array should be one of `user_id`, `group_id` or
`access_level`, and take the form `{user_id: integer}`, `{group_id: integer}` or
`{access_level: integer}`.
Each user must have access to the project and each group must [have this project shared](../user/project/members/share_project_with_groups.md).
Example response:
```json
{
"name":"staging",
"name":"production",
"deploy_access_levels":[
{
"access_level":null,
"access_level_description":"Administrator",
"user_id":1,
"group_id":null
"access_level":40,
"access_level_description":"protected-access-group",
"user_id":null,
"group_id":9899826
}
]
}

View File

@ -940,9 +940,7 @@ message. Install the [correct GitLab version](https://packages.gitlab.com/gitlab
and then try again.
NOTE: **Note:**
There is a known issue with restore not working with `pgbouncer`. The [workaround is to bypass
`pgbouncer` and connect directly to the primary database node](../administration/postgresql/pgbouncer.md#procedure-for-bypassing-pgbouncer).
[Read more about backup and restore with `pgbouncer`](#backup-and-restore-for-installations-using-pgbouncer).
There is a known issue with restore not working with `pgbouncer`. [Read more about backup and restore with `pgbouncer`](#backup-and-restore-for-installations-using-pgbouncer).
### Restore for Docker image and GitLab Helm chart installations
@ -1039,26 +1037,60 @@ practical use.
## Backup and restore for installations using PgBouncer
PgBouncer can cause the following errors when performing backups and restores:
Do NOT backup or restore GitLab through a PgBouncer connection. These
tasks must [bypass PgBouncer and connect directly to the PostgreSQL primary database node](#bypassing-pgbouncer),
or they will cause a GitLab outage.
When the GitLab backup or restore task is used with PgBouncer, the
following error message is shown:
```ruby
ActiveRecord::StatementInvalid: PG::UndefinedTable
```
There is a [known issue](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/3470) for restore not working
with `pgbouncer`.
This happens because the task uses `pg_dump`, which [sets a null search
path and explicitly includes the schema in every SQL query](https://gitlab.com/gitlab-org/gitlab/-/issues/23211)
to address [CVE-2018-1058](https://www.postgresql.org/about/news/postgresql-103-968-9512-9417-and-9322-released-1834/).
To workaround this issue, the GitLab server will need to bypass `pgbouncer` and
[connect directly to the primary database node](../administration/postgresql/pgbouncer.md#procedure-for-bypassing-pgbouncer)
to perform the database restore.
Since connections are reused with PgBouncer in transaction pooling mode,
PostgreSQL fails to search the default `public` schema. As a result,
this clearing of the search path causes tables and columns to appear
missing.
There is also a [known issue](https://gitlab.com/gitlab-org/gitlab/-/issues/23211)
with PostgreSQL 9 and running a database backup through PgBouncer that can cause
an outage to GitLab. If you're still on PostgreSQL 9 and upgrading PostgreSQL isn't
an option, workarounds include having a dedicated application node just for backups,
configured to connect directly the primary database node as noted above. You're
advised to upgrade your PostgreSQL version though, GitLab 11.11 shipped with PostgreSQL
10.7, and that is the recommended version for GitLab 12+.
### Bypassing PgBouncer
There are two ways to fix this:
1. [Use environment variables to override the database settings](#environment-variable-overrides) for the backup task.
1. Reconfigure a node to [connect directly to the PostgreSQL primary database node](../administration/postgresql/pgbouncer.md#procedure-for-bypassing-pgbouncer).
#### Environment variable overrides
By default, GitLab uses the database configuration stored in a
configuration file (`database.yml`). However, you can override the database settings
for the backup and restore task by setting environment
variables that are prefixed with `GITLAB_BACKUP_`:
- `GITLAB_BACKUP_PGHOST`
- `GITLAB_BACKUP_PGUSER`
- `GITLAB_BACKUP_PGPORT`
- `GITLAB_BACKUP_PGPASSWORD`
- `GITLAB_BACKUP_PGSSLMODE`
- `GITLAB_BACKUP_PGSSLKEY`
- `GITLAB_BACKUP_PGSSLCERT`
- `GITLAB_BACKUP_PGSSLROOTCERT`
- `GITLAB_BACKUP_PGSSLCRL`
- `GITLAB_BACKUP_PGSSLCOMPRESSION`
For example, to override the database host and port to use 192.168.1.10
and port 5432 with the Omnibus package:
```shell
sudo GITLAB_BACKUP_PGHOST=192.168.1.10 GITLAB_BACKUP_PGPORT=5432 /opt/gitlab/bin/gitlab-backup create
```
See the [PostgreSQL documentation](https://www.postgresql.org/docs/12/libpq-envars.html)
for more details on what these parameters do.
## Additional notes

View File

@ -30,6 +30,7 @@ Docker image with the fuzz engine to run your app.
| Swift | [libfuzzer](https://github.com/apple/swift/blob/master/docs/libFuzzerIntegration.md) | [swift-fuzzing-example](https://gitlab.com/gitlab-org/security-products/demos/coverage-fuzzing/swift-fuzzing-example) |
| Rust | [cargo-fuzz (libFuzzer support)](https://github.com/rust-fuzz/cargo-fuzz) | [rust-fuzzing-example](https://gitlab.com/gitlab-org/security-products/demos/coverage-fuzzing/rust-fuzzing-example) |
| Java | [JQF](https://github.com/rohanpadhye/JQF) | [java-fuzzing-example](https://gitlab.com/gitlab-org/security-products/demos/coverage-fuzzing/java-fuzzing-example) |
| Java | [javafuzz](https://gitlab.com/gitlab-org/security-products/analyzers/fuzzers/javafuzz) (recommended) | [javafuzz-fuzzing-example](https://gitlab.com/gitlab-org/security-products/demos/coverage-fuzzing/javafuzz-fuzzing-example) |
## Configuration

View File

@ -30,31 +30,33 @@ The Package Registry supports the following formats:
You can also use the [API](../../api/packages.md) to administer the Package Registry.
The GitLab [Container Registry](container_registry/index.md) is a secure and private registry for container images.
It's built on open source software and completely integrated within GitLab.
Use GitLab CI/CD to create and publish images. Use the GitLab [API](../../api/container_registry.md) to
manage the registry across groups and projects.
## Accepting contributions
The [Dependency Proxy](dependency_proxy/index.md) is a local proxy for frequently-used upstream images and packages.
## Suggested contributions
Consider contributing to GitLab. This [development documentation](../../development/packages.md) will
The below table lists formats that are not supported, but are accepting Community contributions for. Consider contributing to GitLab. This [development documentation](../../development/packages.md) will
guide you through the process. Or check out how other members of the community
are adding support for [PHP](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/17417) or [Terraform](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/18834).
| Format | Use case |
| Format | Status |
| ------ | ------ |
| [Cargo](https://gitlab.com/gitlab-org/gitlab/-/issues/33060) | Cargo is the Rust package manager. Build, publish and share Rust packages |
| [Chef](https://gitlab.com/gitlab-org/gitlab/-/issues/36889) | Configuration management with Chef using all the benefits of a repository manager. |
| [CocoaPods](https://gitlab.com/gitlab-org/gitlab/-/issues/36890) | Speed up development with Xcode and CocoaPods. |
| [Conda](https://gitlab.com/gitlab-org/gitlab/-/issues/36891) | Secure and private local Conda repositories. |
| [CRAN](https://gitlab.com/gitlab-org/gitlab/-/issues/36892) | Deploy and resolve CRAN packages for the R language. |
| [Debian](https://gitlab.com/gitlab-org/gitlab/-/issues/5835) | Host and provision Debian packages. |
| [Opkg](https://gitlab.com/gitlab-org/gitlab/-/issues/36894) | Optimize your work with OpenWrt using Opkg repositories. |
| [P2](https://gitlab.com/gitlab-org/gitlab/-/issues/36895) | Host all your Eclipse plugins in your own GitLab P2 repository. |
| [Puppet](https://gitlab.com/gitlab-org/gitlab/-/issues/36897) | Configuration management meets repository management with Puppet repositories. |
| [RPM](https://gitlab.com/gitlab-org/gitlab/-/issues/5932) | Distribute RPMs directly from GitLab. |
| [RubyGems](https://gitlab.com/gitlab-org/gitlab/-/issues/803) | Use GitLab to host your own gems. |
| [SBT](https://gitlab.com/gitlab-org/gitlab/-/issues/36898) | Resolve dependencies from and deploy build output to SBT repositories when running SBT builds. |
| [Vagrant](https://gitlab.com/gitlab-org/gitlab/-/issues/36899) | Securely host your Vagrant boxes in local repositories. |
| Chef | [#36889](https://gitlab.com/gitlab-org/gitlab/-/issues/36889) |
| CocoaPods | [#36890](https://gitlab.com/gitlab-org/gitlab/-/issues/36890) |
| CocoaPods | [#36891](https://gitlab.com/gitlab-org/gitlab/-/issues/36891) |
| Conda | [#36891](https://gitlab.com/gitlab-org/gitlab/-/issues/36891) |
| CRAN | [#36892](https://gitlab.com/gitlab-org/gitlab/-/issues/36892) |
| Debian | [WIP: Merge Request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44746) |
| Opkg | [#36894](https://gitlab.com/gitlab-org/gitlab/-/issues/36894) |
| P2 | [#36895](https://gitlab.com/gitlab-org/gitlab/-/issues/36895) |
| Puppet | [#36897](https://gitlab.com/gitlab-org/gitlab/-/issues/36897) |
| RPM | [#5932](https://gitlab.com/gitlab-org/gitlab/-/issues/5932) |
| RubyGems | [#803](https://gitlab.com/gitlab-org/gitlab/-/issues/803) |
| SBT | [#36898](https://gitlab.com/gitlab-org/gitlab/-/issues/36898) |
| Terraform | [WIP: Merge Request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/18834) |
| Vagrant | [#36899](https://gitlab.com/gitlab-org/gitlab/-/issues/36899) |
## Container Registry
The GitLab [Container Registry](container_registry/index.md) is a secure and private registry for container images. It's built on open source software and completely integrated within GitLab. Use GitLab CI/CD to create and publish images. Use the GitLab [API](../../api/container_registry.md) to manage the registry across groups and projects.
## Dependency Proxy
The [Dependency Proxy](dependency_proxy/index.md) is a local proxy for frequently-used upstream images and packages.

View File

@ -140,7 +140,14 @@ module Backup
'sslcrl' => 'PGSSLCRL',
'sslcompression' => 'PGSSLCOMPRESSION'
}
args.each { |opt, arg| ENV[arg] = config[opt].to_s if config[opt] }
args.each do |opt, arg|
# This enables the use of different PostgreSQL settings in
# case PgBouncer is used. PgBouncer clears the search path,
# which wreaks havoc on Rails if connections are reused.
override = "GITLAB_BACKUP_#{arg}"
val = ENV[override].presence || config[opt].to_s.presence
ENV[arg] = val if val
end
end
def report_success(success)

View File

@ -7,7 +7,7 @@ code_quality:
variables:
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: ""
CODE_QUALITY_IMAGE: "registry.gitlab.com/gitlab-org/ci-cd/codequality:0.85.10-gitlab.1"
CODE_QUALITY_IMAGE: "registry.gitlab.com/gitlab-org/ci-cd/codequality:0.85.18"
needs: []
script:
- export SOURCE_CODE=$PWD

View File

@ -67,15 +67,6 @@ module Gitlab
::JSON.pretty_generate(object, opts)
end
# Feature detection for using Oj instead of the `json` gem.
#
# @return [Boolean]
def enable_oj?
return false unless feature_table_exists?
Feature.enabled?(:oj_json, default_enabled: true)
end
private
# Convert JSON string into Ruby through toggleable adapters.
@ -91,11 +82,7 @@ module Gitlab
def adapter_load(string, *args, **opts)
opts = standardize_opts(opts)
if enable_oj?
Oj.load(string, opts)
else
::JSON.parse(string, opts)
end
Oj.load(string, opts)
rescue Oj::ParseError, Encoding::UndefinedConversionError => ex
raise parser_error.new(ex)
end
@ -120,11 +107,7 @@ module Gitlab
#
# @return [String]
def adapter_dump(object, *args, **opts)
if enable_oj?
Oj.dump(object, opts)
else
::JSON.dump(object, *args)
end
Oj.dump(object, opts)
end
# Generates JSON for an object but with fewer options, using toggleable adapters.
@ -135,11 +118,7 @@ module Gitlab
def adapter_generate(object, opts = {})
opts = standardize_opts(opts)
if enable_oj?
Oj.generate(object, opts)
else
::JSON.generate(object, opts)
end
Oj.generate(object, opts)
end
# Take a JSON standard options hash and standardize it to work across adapters
@ -149,11 +128,8 @@ module Gitlab
# @return [Hash]
def standardize_opts(opts)
opts ||= {}
if enable_oj?
opts[:mode] = :rails
opts[:symbol_keys] = opts[:symbolize_keys] || opts[:symbolize_names]
end
opts[:mode] = :rails
opts[:symbol_keys] = opts[:symbolize_keys] || opts[:symbolize_names]
opts
end
@ -213,7 +189,7 @@ module Gitlab
# @param object [Object]
# @return [String]
def self.call(object, env = nil)
if Gitlab::Json.enable_oj? && Feature.enabled?(:grape_gitlab_json, default_enabled: true)
if Feature.enabled?(:grape_gitlab_json, default_enabled: true)
Gitlab::Json.dump(object)
else
Grape::Formatter::Json.call(object, env)

View File

@ -32,7 +32,7 @@ module Gitlab
def changes_will_exceed_size_limit?(change_size)
return false unless enabled?
change_size > limit || exceeded_size(change_size) > 0
above_size_limit? || exceeded_size(change_size) > 0
end
# @param change_size [int] in bytes

View File

@ -4,9 +4,6 @@ module Gitlab
module RepositoryUrlBuilder
class << self
def build(path, protocol: :ssh)
# TODO: See https://gitlab.com/gitlab-org/gitlab/-/issues/213021
path = path.sub('@snippets', 'snippets')
case protocol
when :ssh
ssh_url(path)

View File

@ -17716,6 +17716,9 @@ msgstr ""
msgid "New"
msgstr ""
msgid "New %{display_issuable_type}"
msgstr ""
msgid "New Application"
msgstr ""
@ -25457,6 +25460,9 @@ msgstr ""
msgid "Submit a review"
msgstr ""
msgid "Submit as spam"
msgstr ""
msgid "Submit changes"
msgstr ""

View File

@ -0,0 +1,80 @@
# frozen_string_literal: true
require "spec_helper"
RSpec.describe "User views incident" do
let_it_be(:project) { create(:project_empty_repo, :public) }
let_it_be(:user) { create(:user) }
let_it_be(:incident) { create(:incident, project: project, description: "# Description header\n\n**Lorem** _ipsum_ dolor sit [amet](https://example.com)", author: user) }
let_it_be(:note) { create(:note, noteable: incident, project: project, author: user) }
before_all do
project.add_developer(user)
end
before do
sign_in(user)
visit(project_issues_incident_path(project, incident))
end
it { expect(page).to have_header_with_correct_id_and_link(1, "Description header", "description-header") }
it_behaves_like 'page meta description', ' Description header Lorem ipsum dolor sit amet'
it 'shows the merge request and incident actions', :aggregate_failures do
expect(page).to have_link('New incident')
expect(page).to have_button('Create merge request')
expect(page).to have_link('Close incident')
end
context 'when the project is archived' do
before do
project.update!(archived: true)
visit(project_issues_incident_path(project, incident))
end
it 'hides the merge request and incident actions', :aggregate_failures do
expect(page).not_to have_link('New incident')
expect(page).not_to have_button('Create merge request')
expect(page).not_to have_link('Close incident')
end
end
describe 'user status' do
subject { visit(project_issues_incident_path(project, incident)) }
context 'when showing status of the author of the incident' do
it_behaves_like 'showing user status' do
let(:user_with_status) { user }
end
end
context 'when showing status of a user who commented on an incident', :js do
it_behaves_like 'showing user status' do
let(:user_with_status) { user }
end
end
context 'when status message has an emoji', :js do
let_it_be(:message) { 'My status with an emoji' }
let_it_be(:message_emoji) { 'basketball' }
let_it_be(:status) { create(:user_status, user: user, emoji: 'smirk', message: "#{message} :#{message_emoji}:") }
it 'correctly renders the emoji' do
wait_for_requests
tooltip_span = page.first(".user-status-emoji[title^='#{message}']")
tooltip_span.hover
wait_for_requests
tooltip = page.find('.tooltip .tooltip-inner')
page.within(tooltip) do
expect(page).to have_emoji(message_emoji)
end
end
end
end
end

View File

@ -68,7 +68,7 @@ RSpec.describe 'User interacts with awards' do
page.within('.awards') do
expect(page).to have_selector('.js-emoji-btn')
expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1')
expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']")
expect(page).to have_css(".js-emoji-btn.active[title='You']")
expect do
page.find('.js-emoji-btn.active').click

View File

@ -34,7 +34,7 @@ RSpec.describe 'User uses header search field', :js do
wait_for_all_requests
end
it 'shows the category search dropdown' do
it 'shows the category search dropdown', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/250285' do
expect(page).to have_selector('.dropdown-header', text: /#{scope_name}/i)
end
end
@ -44,7 +44,7 @@ RSpec.describe 'User uses header search field', :js do
page.find('#search').click
end
it 'shows category search dropdown' do
it 'shows category search dropdown', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/250285' do
expect(page).to have_selector('.dropdown-header', text: /#{scope_name}/i)
end
@ -104,7 +104,7 @@ RSpec.describe 'User uses header search field', :js do
let(:scope_name) { 'All GitLab' }
end
it 'displays search options' do
it 'displays search options', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/251076' do
fill_in_search('test')
expect(page).to have_selector(scoped_search_link('test'))

View File

@ -73,4 +73,44 @@ RSpec.describe 'Static Site Editor' do
expect(node['data-static-site-generator']).to eq('middleman')
end
end
describe 'Static Site Editor Content Security Policy' do
subject { response_headers['Content-Security-Policy'] }
context 'when no global CSP config exists' do
before do
expect_next_instance_of(Projects::StaticSiteEditorController) do |controller|
expect(controller).to receive(:current_content_security_policy)
.and_return(ActionDispatch::ContentSecurityPolicy.new)
end
end
it 'does not add CSP directives' do
visit sse_path
is_expected.to be_blank
end
end
context 'when a global CSP config exists' do
let_it_be(:cdn_url) { 'https://some-cdn.test' }
let_it_be(:youtube_url) { 'https://www.youtube.com' }
before do
csp = ActionDispatch::ContentSecurityPolicy.new do |p|
p.frame_src :self, cdn_url
end
expect_next_instance_of(Projects::StaticSiteEditorController) do |controller|
expect(controller).to receive(:current_content_security_policy).and_return(csp)
end
end
it 'appends youtube to the CSP frame-src policy' do
visit sse_path
is_expected.to eql("frame-src 'self' #{cdn_url} #{youtube_url}")
end
end
end
end

View File

@ -169,29 +169,6 @@ describe('AwardsHandler', () => {
});
});
describe('::userAuthored', () => {
it('should update tooltip to user authored title', () => {
const $votesBlock = $('.js-awards-block').eq(0);
const $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent();
$thumbsUpEmoji.attr('data-title', 'sam');
awardsHandler.userAuthored($thumbsUpEmoji);
expect($thumbsUpEmoji.data('originalTitle')).toBe(
'You cannot vote on your own issue, MR and note',
);
});
it('should restore tooltip back to initial vote list', () => {
const $votesBlock = $('.js-awards-block').eq(0);
const $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent();
$thumbsUpEmoji.attr('data-title', 'sam');
awardsHandler.userAuthored($thumbsUpEmoji);
jest.advanceTimersByTime(2801);
expect($thumbsUpEmoji.data('originalTitle')).toBe('sam');
});
});
describe('::getAwardUrl', () => {
it('returns the url for request', () => {
expect(awardsHandler.getAwardUrl()).toBe('http://test.host/-/snippets/1/toggle_award_emoji');

View File

@ -345,6 +345,24 @@ RSpec.describe IssuablesHelper do
end
end
describe '#issuable_display_type' do
using RSpec::Parameterized::TableSyntax
where(:issuable_type, :issuable_display_type) do
:issue | 'issue'
:incident | 'incident'
:merge_request | 'merge request'
end
with_them do
let(:issuable) { build_stubbed(issuable_type) }
subject { helper.issuable_display_type(issuable) }
it { is_expected.to eq(issuable_display_type) }
end
end
describe '#sidebar_milestone_tooltip_label' do
it 'escapes HTML in the milestone title' do
milestone = build(:milestone, title: '&lt;img onerror=alert(1)&gt;')

View File

@ -48,5 +48,26 @@ RSpec.describe Backup::Database do
expect(output).to include(visible_error)
end
end
context 'with PostgreSQL settings defined in the environment' do
let(:cmd) { %W[#{Gem.ruby} -e] + ["$stderr.puts ENV.to_h.select { |k, _| k.start_with?('PG') }"] }
let(:config) { YAML.load_file(File.join(Rails.root, 'config', 'database.yml'))['test'] }
before do
stub_const 'ENV', ENV.to_h.merge({
'GITLAB_BACKUP_PGHOST' => 'test.example.com',
'PGPASSWORD' => 'donotchange'
})
end
it 'overrides default config values' do
subject.restore
expect(output).to include(%("PGHOST"=>"test.example.com"))
expect(output).to include(%("PGPASSWORD"=>"donotchange"))
expect(output).to include(%("PGPORT"=>"#{config['port']}")) if config['port']
expect(output).to include(%("PGUSER"=>"#{config['username']}")) if config['username']
end
end
end
end

View File

@ -7,342 +7,306 @@ RSpec.describe Gitlab::Json do
stub_feature_flags(json_wrapper_legacy_mode: true)
end
shared_examples "json" do
describe ".parse" do
context "legacy_mode is disabled by default" do
it "parses an object" do
expect(subject.parse('{ "foo": "bar" }')).to eq({ "foo" => "bar" })
end
it "parses an array" do
expect(subject.parse('[{ "foo": "bar" }]')).to eq([{ "foo" => "bar" }])
end
it "parses a string" do
expect(subject.parse('"foo"', legacy_mode: false)).to eq("foo")
end
it "parses a true bool" do
expect(subject.parse("true", legacy_mode: false)).to be(true)
end
it "parses a false bool" do
expect(subject.parse("false", legacy_mode: false)).to be(false)
end
describe ".parse" do
context "legacy_mode is disabled by default" do
it "parses an object" do
expect(subject.parse('{ "foo": "bar" }')).to eq({ "foo" => "bar" })
end
context "legacy_mode is enabled" do
it "parses an object" do
expect(subject.parse('{ "foo": "bar" }', legacy_mode: true)).to eq({ "foo" => "bar" })
end
it "parses an array" do
expect(subject.parse('[{ "foo": "bar" }]', legacy_mode: true)).to eq([{ "foo" => "bar" }])
end
it "raises an error on a string" do
expect { subject.parse('"foo"', legacy_mode: true) }.to raise_error(JSON::ParserError)
end
it "raises an error on a true bool" do
expect { subject.parse("true", legacy_mode: true) }.to raise_error(JSON::ParserError)
end
it "raises an error on a false bool" do
expect { subject.parse("false", legacy_mode: true) }.to raise_error(JSON::ParserError)
end
it "parses an array" do
expect(subject.parse('[{ "foo": "bar" }]')).to eq([{ "foo" => "bar" }])
end
context "feature flag is disabled" do
before do
stub_feature_flags(json_wrapper_legacy_mode: false)
end
it "parses a string" do
expect(subject.parse('"foo"', legacy_mode: false)).to eq("foo")
end
it "parses an object" do
expect(subject.parse('{ "foo": "bar" }', legacy_mode: true)).to eq({ "foo" => "bar" })
end
it "parses a true bool" do
expect(subject.parse("true", legacy_mode: false)).to be(true)
end
it "parses an array" do
expect(subject.parse('[{ "foo": "bar" }]', legacy_mode: true)).to eq([{ "foo" => "bar" }])
end
it "parses a string" do
expect(subject.parse('"foo"', legacy_mode: true)).to eq("foo")
end
it "parses a true bool" do
expect(subject.parse("true", legacy_mode: true)).to be(true)
end
it "parses a false bool" do
expect(subject.parse("false", legacy_mode: true)).to be(false)
end
it "parses a false bool" do
expect(subject.parse("false", legacy_mode: false)).to be(false)
end
end
describe ".parse!" do
context "legacy_mode is disabled by default" do
it "parses an object" do
expect(subject.parse!('{ "foo": "bar" }')).to eq({ "foo" => "bar" })
end
it "parses an array" do
expect(subject.parse!('[{ "foo": "bar" }]')).to eq([{ "foo" => "bar" }])
end
it "parses a string" do
expect(subject.parse!('"foo"', legacy_mode: false)).to eq("foo")
end
it "parses a true bool" do
expect(subject.parse!("true", legacy_mode: false)).to be(true)
end
it "parses a false bool" do
expect(subject.parse!("false", legacy_mode: false)).to be(false)
end
context "legacy_mode is enabled" do
it "parses an object" do
expect(subject.parse('{ "foo": "bar" }', legacy_mode: true)).to eq({ "foo" => "bar" })
end
context "legacy_mode is enabled" do
it "parses an object" do
expect(subject.parse!('{ "foo": "bar" }', legacy_mode: true)).to eq({ "foo" => "bar" })
end
it "parses an array" do
expect(subject.parse!('[{ "foo": "bar" }]', legacy_mode: true)).to eq([{ "foo" => "bar" }])
end
it "raises an error on a string" do
expect { subject.parse!('"foo"', legacy_mode: true) }.to raise_error(JSON::ParserError)
end
it "raises an error on a true bool" do
expect { subject.parse!("true", legacy_mode: true) }.to raise_error(JSON::ParserError)
end
it "raises an error on a false bool" do
expect { subject.parse!("false", legacy_mode: true) }.to raise_error(JSON::ParserError)
end
it "parses an array" do
expect(subject.parse('[{ "foo": "bar" }]', legacy_mode: true)).to eq([{ "foo" => "bar" }])
end
context "feature flag is disabled" do
before do
stub_feature_flags(json_wrapper_legacy_mode: false)
end
it "raises an error on a string" do
expect { subject.parse('"foo"', legacy_mode: true) }.to raise_error(JSON::ParserError)
end
it "parses an object" do
expect(subject.parse!('{ "foo": "bar" }', legacy_mode: true)).to eq({ "foo" => "bar" })
end
it "raises an error on a true bool" do
expect { subject.parse("true", legacy_mode: true) }.to raise_error(JSON::ParserError)
end
it "parses an array" do
expect(subject.parse!('[{ "foo": "bar" }]', legacy_mode: true)).to eq([{ "foo" => "bar" }])
end
it "parses a string" do
expect(subject.parse!('"foo"', legacy_mode: true)).to eq("foo")
end
it "parses a true bool" do
expect(subject.parse!("true", legacy_mode: true)).to be(true)
end
it "parses a false bool" do
expect(subject.parse!("false", legacy_mode: true)).to be(false)
end
it "raises an error on a false bool" do
expect { subject.parse("false", legacy_mode: true) }.to raise_error(JSON::ParserError)
end
end
describe ".dump" do
it "dumps an object" do
expect(subject.dump({ "foo" => "bar" })).to eq('{"foo":"bar"}')
context "feature flag is disabled" do
before do
stub_feature_flags(json_wrapper_legacy_mode: false)
end
it "dumps an array" do
expect(subject.dump([{ "foo" => "bar" }])).to eq('[{"foo":"bar"}]')
it "parses an object" do
expect(subject.parse('{ "foo": "bar" }', legacy_mode: true)).to eq({ "foo" => "bar" })
end
it "dumps a string" do
expect(subject.dump("foo")).to eq('"foo"')
it "parses an array" do
expect(subject.parse('[{ "foo": "bar" }]', legacy_mode: true)).to eq([{ "foo" => "bar" }])
end
it "dumps a true bool" do
expect(subject.dump(true)).to eq("true")
it "parses a string" do
expect(subject.parse('"foo"', legacy_mode: true)).to eq("foo")
end
it "dumps a false bool" do
expect(subject.dump(false)).to eq("false")
it "parses a true bool" do
expect(subject.parse("true", legacy_mode: true)).to be(true)
end
it "parses a false bool" do
expect(subject.parse("false", legacy_mode: true)).to be(false)
end
end
end
describe ".parse!" do
context "legacy_mode is disabled by default" do
it "parses an object" do
expect(subject.parse!('{ "foo": "bar" }')).to eq({ "foo" => "bar" })
end
it "parses an array" do
expect(subject.parse!('[{ "foo": "bar" }]')).to eq([{ "foo" => "bar" }])
end
it "parses a string" do
expect(subject.parse!('"foo"', legacy_mode: false)).to eq("foo")
end
it "parses a true bool" do
expect(subject.parse!("true", legacy_mode: false)).to be(true)
end
it "parses a false bool" do
expect(subject.parse!("false", legacy_mode: false)).to be(false)
end
end
describe ".generate" do
let(:obj) do
{ test: true, "foo.bar" => "baz", is_json: 1, some: [1, 2, 3] }
context "legacy_mode is enabled" do
it "parses an object" do
expect(subject.parse!('{ "foo": "bar" }', legacy_mode: true)).to eq({ "foo" => "bar" })
end
it "generates JSON" do
expected_string = <<~STR.chomp
{"test":true,"foo.bar":"baz","is_json":1,"some":[1,2,3]}
STR
expect(subject.generate(obj)).to eq(expected_string)
it "parses an array" do
expect(subject.parse!('[{ "foo": "bar" }]', legacy_mode: true)).to eq([{ "foo" => "bar" }])
end
it "allows you to customise the output" do
opts = {
indent: " ",
space: " ",
space_before: " ",
object_nl: "\n",
array_nl: "\n"
}
it "raises an error on a string" do
expect { subject.parse!('"foo"', legacy_mode: true) }.to raise_error(JSON::ParserError)
end
json = subject.generate(obj, opts)
it "raises an error on a true bool" do
expect { subject.parse!("true", legacy_mode: true) }.to raise_error(JSON::ParserError)
end
expected_string = <<~STR.chomp
{
"test" : true,
"foo.bar" : "baz",
"is_json" : 1,
"some" : [
1,
2,
3
]
}
STR
expect(json).to eq(expected_string)
it "raises an error on a false bool" do
expect { subject.parse!("false", legacy_mode: true) }.to raise_error(JSON::ParserError)
end
end
describe ".pretty_generate" do
let(:obj) do
context "feature flag is disabled" do
before do
stub_feature_flags(json_wrapper_legacy_mode: false)
end
it "parses an object" do
expect(subject.parse!('{ "foo": "bar" }', legacy_mode: true)).to eq({ "foo" => "bar" })
end
it "parses an array" do
expect(subject.parse!('[{ "foo": "bar" }]', legacy_mode: true)).to eq([{ "foo" => "bar" }])
end
it "parses a string" do
expect(subject.parse!('"foo"', legacy_mode: true)).to eq("foo")
end
it "parses a true bool" do
expect(subject.parse!("true", legacy_mode: true)).to be(true)
end
it "parses a false bool" do
expect(subject.parse!("false", legacy_mode: true)).to be(false)
end
end
end
describe ".dump" do
it "dumps an object" do
expect(subject.dump({ "foo" => "bar" })).to eq('{"foo":"bar"}')
end
it "dumps an array" do
expect(subject.dump([{ "foo" => "bar" }])).to eq('[{"foo":"bar"}]')
end
it "dumps a string" do
expect(subject.dump("foo")).to eq('"foo"')
end
it "dumps a true bool" do
expect(subject.dump(true)).to eq("true")
end
it "dumps a false bool" do
expect(subject.dump(false)).to eq("false")
end
end
describe ".generate" do
let(:obj) do
{ test: true, "foo.bar" => "baz", is_json: 1, some: [1, 2, 3] }
end
it "generates JSON" do
expected_string = <<~STR.chomp
{"test":true,"foo.bar":"baz","is_json":1,"some":[1,2,3]}
STR
expect(subject.generate(obj)).to eq(expected_string)
end
it "allows you to customise the output" do
opts = {
indent: " ",
space: " ",
space_before: " ",
object_nl: "\n",
array_nl: "\n"
}
json = subject.generate(obj, opts)
expected_string = <<~STR.chomp
{
test: true,
"foo.bar" => "baz",
is_json: 1,
some: [1, 2, 3],
more: { test: true },
multi_line_empty_array: [],
multi_line_empty_obj: {}
"test" : true,
"foo.bar" : "baz",
"is_json" : 1,
"some" : [
1,
2,
3
]
}
end
STR
it "generates pretty JSON" do
expected_string = <<~STR.chomp
{
"test": true,
"foo.bar": "baz",
"is_json": 1,
"some": [
1,
2,
3
],
"more": {
"test": true
},
"multi_line_empty_array": [
],
"multi_line_empty_obj": {
}
}
STR
expect(subject.pretty_generate(obj)).to eq(expected_string)
end
it "allows you to customise the output" do
opts = {
space_before: " "
}
json = subject.pretty_generate(obj, opts)
expected_string = <<~STR.chomp
{
"test" : true,
"foo.bar" : "baz",
"is_json" : 1,
"some" : [
1,
2,
3
],
"more" : {
"test" : true
},
"multi_line_empty_array" : [
],
"multi_line_empty_obj" : {
}
}
STR
expect(json).to eq(expected_string)
end
end
context "the feature table is missing" do
before do
allow(Feature::FlipperFeature).to receive(:table_exists?).and_return(false)
end
it "skips legacy mode handling" do
expect(Feature).not_to receive(:enabled?).with(:json_wrapper_legacy_mode, default_enabled: true)
subject.send(:handle_legacy_mode!, {})
end
it "skips oj feature detection" do
expect(Feature).not_to receive(:enabled?).with(:oj_json, default_enabled: true)
subject.send(:enable_oj?)
end
end
context "the database is missing" do
before do
allow(Feature::FlipperFeature).to receive(:table_exists?).and_raise(PG::ConnectionBad)
end
it "still parses json" do
expect(subject.parse("{}")).to eq({})
end
it "still generates json" do
expect(subject.dump({})).to eq("{}")
end
expect(json).to eq(expected_string)
end
end
context "oj gem" do
before do
stub_feature_flags(oj_json: true)
describe ".pretty_generate" do
let(:obj) do
{
test: true,
"foo.bar" => "baz",
is_json: 1,
some: [1, 2, 3],
more: { test: true },
multi_line_empty_array: [],
multi_line_empty_obj: {}
}
end
it_behaves_like "json"
it "generates pretty JSON" do
expected_string = <<~STR.chomp
{
"test": true,
"foo.bar": "baz",
"is_json": 1,
"some": [
1,
2,
3
],
"more": {
"test": true
},
"multi_line_empty_array": [
describe "#enable_oj?" do
it "returns true" do
expect(subject.enable_oj?).to be(true)
end
],
"multi_line_empty_obj": {
}
}
STR
expect(subject.pretty_generate(obj)).to eq(expected_string)
end
it "allows you to customise the output" do
opts = {
space_before: " "
}
json = subject.pretty_generate(obj, opts)
expected_string = <<~STR.chomp
{
"test" : true,
"foo.bar" : "baz",
"is_json" : 1,
"some" : [
1,
2,
3
],
"more" : {
"test" : true
},
"multi_line_empty_array" : [
],
"multi_line_empty_obj" : {
}
}
STR
expect(json).to eq(expected_string)
end
end
context "json gem" do
context "the feature table is missing" do
before do
stub_feature_flags(oj_json: false)
allow(Feature::FlipperFeature).to receive(:table_exists?).and_return(false)
end
it_behaves_like "json"
it "skips legacy mode handling" do
expect(Feature).not_to receive(:enabled?).with(:json_wrapper_legacy_mode, default_enabled: true)
describe "#enable_oj?" do
it "returns false" do
expect(subject.enable_oj?).to be(false)
end
subject.send(:handle_legacy_mode!, {})
end
end
context "the database is missing" do
before do
allow(Feature::FlipperFeature).to receive(:table_exists?).and_raise(PG::ConnectionBad)
end
it "still parses json" do
expect(subject.parse("{}")).to eq({})
end
it "still generates json" do
expect(subject.dump({})).to eq("{}")
end
end
@ -353,47 +317,25 @@ RSpec.describe Gitlab::Json do
let(:env) { {} }
let(:result) { "{\"test\":true}" }
context "oj is enabled" do
context "grape_gitlab_json flag is enabled" do
before do
stub_feature_flags(oj_json: true)
stub_feature_flags(grape_gitlab_json: true)
end
context "grape_gitlab_json flag is enabled" do
before do
stub_feature_flags(grape_gitlab_json: true)
end
it "generates JSON" do
expect(subject).to eq(result)
end
it "uses Gitlab::Json" do
expect(Gitlab::Json).to receive(:dump).with(obj)
subject
end
it "generates JSON" do
expect(subject).to eq(result)
end
context "grape_gitlab_json flag is disabled" do
before do
stub_feature_flags(grape_gitlab_json: false)
end
it "uses Gitlab::Json" do
expect(Gitlab::Json).to receive(:dump).with(obj)
it "generates JSON" do
expect(subject).to eq(result)
end
it "uses Grape::Formatter::Json" do
expect(Grape::Formatter::Json).to receive(:call).with(obj, env)
subject
end
subject
end
end
context "oj is disabled" do
context "grape_gitlab_json flag is disabled" do
before do
stub_feature_flags(oj_json: false)
stub_feature_flags(grape_gitlab_json: false)
end
it "generates JSON" do

View File

@ -20,9 +20,8 @@ RSpec.describe PersonalSnippet do
it_behaves_like 'model with repository' do
let_it_be(:container) { create(:personal_snippet, :repository) }
let(:stubbed_container) { build_stubbed(:personal_snippet) }
let(:expected_full_path) { "@snippets/#{container.id}" }
let(:expected_full_path) { "snippets/#{container.id}" }
let(:expected_web_url_path) { "-/snippets/#{container.id}" }
let(:expected_repo_url_path) { "snippets/#{container.id}" }
end
describe '#parent_user' do

View File

@ -36,8 +36,7 @@ RSpec.describe ProjectSnippet do
it_behaves_like 'model with repository' do
let_it_be(:container) { create(:project_snippet, :repository) }
let(:stubbed_container) { build_stubbed(:project_snippet) }
let(:expected_full_path) { "#{container.project.full_path}/@snippets/#{container.id}" }
let(:expected_full_path) { "#{container.project.full_path}/snippets/#{container.id}" }
let(:expected_web_url_path) { "#{container.project.full_path}/-/snippets/#{container.id}" }
let(:expected_repo_url_path) { "#{container.project.full_path}/snippets/#{container.id}" }
end
end